-
Notifications
You must be signed in to change notification settings - Fork 84
Description
Environment
Node 20.19.2
jiti 2.4.2
Reproduction
https://stackblitz.com/~/edit/jiti-issue-386
npm install
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:
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.
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);
}
}