Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion packages/utils/src/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,30 @@ import { ImportResolveError } from './error/index.ts';

const debug = debuglog('egg/utils/import');

type NativeDynamicImport = (specifier: string) => Promise<any>;

let nativeDynamicImport: NativeDynamicImport | undefined;

/* v8 ignore next -- covered by the spawned Node fixture; Vitest cannot instrument this opaque import. */
function getNativeDynamicImport(): NativeDynamicImport {
if (!nativeDynamicImport) {
// Keep the fallback dynamic import opaque to bundlers. Turbopack rewrites
// dynamic import expressions with non-static specifiers, which breaks the
// bundled runtime when the bundle map intentionally falls through to Node.
try {
// oxlint-disable-next-line typescript-eslint/no-implied-eval
nativeDynamicImport = new Function('specifier', 'return import(specifier);') as NativeDynamicImport;
} catch (err) {
const error = new Error(
'Native dynamic import fallback for bundled module loader misses requires code generation from strings.',
);
(error as Error & { cause?: unknown }).cause = err;
throw error;
}
}
return nativeDynamicImport;
}

export interface ImportResolveOptions {
paths?: string[];
}
Expand Down Expand Up @@ -454,7 +478,12 @@ export async function importModule(filepath: string, options?: ImportModuleOptio
// esm
const fileUrl = pathToFileURL(moduleFilePath).toString();
debug('[importModule:start] await import fileUrl: %s, isESM: %s', fileUrl, isESM);
obj = await import(fileUrl);
/* v8 ignore if -- covered by the spawned Node fixture; Vitest cannot instrument this opaque import. */
if (_bundleModuleLoader) {
obj = await getNativeDynamicImport()(fileUrl);
} else {
obj = await import(fileUrl);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
debug('[importModule:success] await import %o', fileUrl);
// {
// default: { foo: 'bar', one: 1 },
Expand Down
58 changes: 53 additions & 5 deletions packages/utils/test/bundle-import.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { strict as assert } from 'node:assert';
import fs from 'node:fs/promises';
import path from 'node:path';

import coffee from 'coffee';
import { afterEach, describe, it } from 'vitest';

import { importModule, setBundleModuleLoader } from '../src/import.ts';
Expand Down Expand Up @@ -62,12 +64,58 @@ describe('test/bundle-import.test.ts', () => {
assert.deepEqual(result.default, { fn: 'bundled' });
});

it('falls through to normal import when loader returns undefined', async () => {
setBundleModuleLoader(() => undefined);
it('falls through to native dynamic import when loader returns undefined', async () => {
await coffee
.spawn(process.execPath, ['--experimental-strip-types', getFilepath('bundle-native-fallback/run.mjs')])
.expect('stdout', /bar/)
.expect('code', 0)
.end();
});

const result = await importModule(getFilepath('esm'));
assert.ok(result);
assert.equal(result.default.foo, 'bar');
it('does not create the native dynamic import helper at module load time', async () => {
const importUrl = new URL('../src/import.ts', import.meta.url).href;

await coffee
.spawn(process.execPath, [
'--experimental-strip-types',
'--disallow-code-generation-from-strings',
'--input-type=module',
'--eval',
`await import(${JSON.stringify(importUrl)});`,
])
.expect('code', 0)
.end();
});

it('reports hardened runtime failures when bundled fallback needs code generation', async () => {
const importUrl = new URL('../src/import.ts', import.meta.url).href;
const esmFilepath = getFilepath('esm');

await coffee
.spawn(process.execPath, [
'--experimental-strip-types',
'--disallow-code-generation-from-strings',
'--input-type=module',
'--eval',
[
`const { importModule, setBundleModuleLoader } = await import(${JSON.stringify(importUrl)});`,
'setBundleModuleLoader(() => undefined);',
`await importModule(${JSON.stringify(esmFilepath)});`,
].join('\n'),
])
.expect('stderr', /Native dynamic import fallback for bundled module loader misses requires code generation/)
.expect('code', 1)
.end();
});

it('hides dynamic import fallback from bundled expression transforms', async () => {
const source = await fs.readFile(new URL('../src/import.ts', import.meta.url), 'utf8');

assert.match(source, /new Function\('specifier', 'return import\(specifier\);'\)/);
assert.match(
source,
/\/\* v8 ignore if[^\n]*\*\/\r?\n\s+if \(_bundleModuleLoader\) \{\r?\n\s+obj = await getNativeDynamicImport\(\)\(fileUrl\);/,
);
});

it('serves virtual specifiers from the loader without requiring them on disk', async () => {
Expand Down
20 changes: 20 additions & 0 deletions packages/utils/test/fixtures/bundle-native-fallback/run.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { importModule, setBundleModuleLoader } from '../../../src/import.ts';

const dirname = path.dirname(fileURLToPath(import.meta.url));
const seen = [];

setBundleModuleLoader((filepath) => {
seen.push(filepath);
return undefined;
});

const result = await importModule(path.resolve(dirname, '../esm'));

if (!seen.some((filepath) => filepath.endsWith('/fixtures/esm'))) {
throw new Error(`bundle loader miss was not observed: ${JSON.stringify(seen)}`);
}

console.log(result.default.foo);
Loading