Thanks to visit codestin.com
Credit goes to github.com

Skip to content

tryNative doesn't fallback to transpilation on error #386

@JReinhold

Description

@JReinhold

Environment

Node 20.19.2
jiti 2.4.2

Reproduction

https://stackblitz.com/~/edit/jiti-issue-386

  1. npm install
  2. node index.js

See logs:

[jiti] [init] version: 2.4.2 module-cache: true fs-cache: true interop-defaults: true
[jiti] [try-native] [import] ./some-ts-file.ts
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /home/projects/stackblitz-starters-z4b4fafj/some-ts-file.ts
    at Object.getFileProtocolModuleFormat (node:internal/modules/esm/get_format:153:1980)
    at defaultGetFormat (node:internal/modules/esm/get_format:153:2703)
    at defaultLoad (node:internal/modules/esm/load:156:2285)
    at async ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:157:4559)
    at async ModuleJob._link (node:internal/modules/esm/module_job:158:1406) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}

Describe the bug

When setting the tryNative: true option in createJiti, it seems that it doesn't actually try native, as it will actually throw as soon as it doesn't succeed.

There is a try-catch block in the source that I assume is there to handle this, and let the resolver continue if it gets an error:

jiti/src/require.ts

Lines 39 to 72 in b396aec

if (ctx.opts.tryNative && !ctx.opts.transformOptions) {
try {
id = jitiResolve(ctx, id, opts);
if (!id && opts.try) {
return undefined;
}
debug(
ctx,
"[try-native]",
opts.async && ctx.nativeImport ? "[import]" : "[require]",
id,
);
if (opts.async && ctx.nativeImport) {
return ctx.nativeImport(id).then((m: any) => {
if (ctx.opts.moduleCache === false) {
delete ctx.nativeRequire.cache[id];
}
return jitiInteropDefault(ctx, m);
});
} else {
const _mod = ctx.nativeRequire(id);
if (ctx.opts.moduleCache === false) {
delete ctx.nativeRequire.cache[id];
}
return jitiInteropDefault(ctx, _mod);
}
} catch (error: any) {
debug(
ctx,
`[try-native] Using fallback for ${id} because of an error:`,
error,
);
}
}

The problem is that the call to nativeImport(id) is async, so when that throws (which is what happens), it isn't actually caught by the catch block.

jiti/src/require.ts

Lines 52 to 57 in b396aec

return ctx.nativeImport(id).then((m: any) => {
if (ctx.opts.moduleCache === false) {
delete ctx.nativeRequire.cache[id];
}
return jitiInteropDefault(ctx, m);
});

Additional context

Proposed Fix

I'd expect that a fix similar to #325 would work, where we call the function again if the promise is rejected. I tried this out locally and indeed it fixed the problem:

    try {
      id = jitiResolve(ctx, id, opts);
      if (!id && opts.try) {
        return undefined;
      }
      debug(
        ctx,
        "[try-native]",
        opts.async && ctx.nativeImport ? "[import]" : "[require]",
        id,
      );
      if (opts.async && ctx.nativeImport) {
        return ctx.nativeImport(id).then((m: any) => {
          if (ctx.opts.moduleCache === false) {
            delete ctx.nativeRequire.cache[id];
          }
          return jitiInteropDefault(ctx, m);
-       });
+       }).catch((error) => {
+         debug(
+           ctx,
+           `[try-native] Using fallback for ${id} because of an error:`,
+           error,
+         );
+         return jitiRequire({...ctx, opts: {...ctx.opts, tryNative: false}}, id, opts);
+       });
      } else {
        const _mod = ctx.nativeRequire(id);
        if (ctx.opts.moduleCache === false) {
          delete ctx.nativeRequire.cache[id];
        }
        return jitiInteropDefault(ctx, _mod);
      }
    } catch (error: any) {
      debug(
        ctx,
        `[try-native] Using fallback for ${id} because of an error:`,
        error,
      );
    }

Our use case for this, is that we want to only transpile when necessary, and rely on the native import/require calls when we can. We're seeing that CJS-files are being transformed to ESM even though that is unnecessary, as they could just be imported as-is. But using jiti/native will not allow us to transpile TS files when necessary.

Current workaround

Currently you can wrap the call to import/require in userland in a try-catch, and call it again with a non-tryNative jiti:

import { createJiti } from 'jiti';

const jitiNative = createJiti(import.meta.url, { tryNative: true });
const jitiFallback = createJiti(import.meta.url);

export async function myFync(mod) {
  try {
    return await jitiNative.import(mod);
  } catch (error) {
    return await jitiFallback.import(mod);
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions