Skip to content

Commit 133d1c7

Browse files
committed
fix(sea): parse ESM files as modules and walk dynamic import() literals
`detector.parse()` called babel with the default `sourceType: 'script'`, so SEA-mode walker runs over `import.meta` / top-level `await` failed to parse and silently skipped the file's dependency traversal. Thread `isESMFile(record.file)` through `stepDetect` → `detect()` → `parse()` so ESM files get `sourceType: 'module'`. Also teach the visitor to recognize `import('literal')` `CallExpression`s so bundler-emitted dynamic imports are bundled like static ones.
1 parent 974df53 commit 133d1c7

8 files changed

Lines changed: 114 additions & 3 deletions

File tree

lib/detector.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,22 @@ function visitorImport(n: babelTypes.Node) {
181181
return { v1: n.source.value, v3: reconstructSpecifiers(n.specifiers) };
182182
}
183183

184+
function visitorDynamicImport(n: babelTypes.Node) {
185+
if (!babelTypes.isCallExpression(n)) {
186+
return null;
187+
}
188+
189+
if (n.callee.type !== 'Import') {
190+
return null;
191+
}
192+
193+
if (!n.arguments || !isLiteral(n.arguments[0])) {
194+
return null;
195+
}
196+
197+
return { v1: getLiteralValue(n.arguments[0] as babelTypes.Literal) };
198+
}
199+
184200
function visitorPathJoin(n: babelTypes.Node) {
185201
if (!babelTypes.isCallExpression(n)) {
186202
return null;
@@ -270,6 +286,16 @@ export function visitorSuccessful(node: babelTypes.Node, test = false) {
270286
return { alias: was.v1, aliasType: ALIAS_AS_RESOLVABLE };
271287
}
272288

289+
was = visitorDynamicImport(node);
290+
291+
if (was) {
292+
if (test) {
293+
return forge('import({v1})', was);
294+
}
295+
296+
return { alias: was.v1, aliasType: ALIAS_AS_RESOLVABLE };
297+
}
298+
273299
was = visitorPathJoin(node);
274300

275301
if (was) {
@@ -495,18 +521,24 @@ function traverse(ast: babelTypes.File, visitor: VisitorFunction) {
495521
}
496522
}
497523

498-
export function parse(body: string) {
524+
export function parse(body: string, isEsm = false) {
499525
return babel.parse(body, {
500526
allowImportExportEverywhere: true,
501527
allowReturnOutsideFunction: true,
528+
sourceType: isEsm ? 'module' : 'script',
502529
});
503530
}
504531

505-
export function detect(body: string, visitor: VisitorFunction, file?: string) {
532+
export function detect(
533+
body: string,
534+
visitor: VisitorFunction,
535+
file?: string,
536+
isEsm = false,
537+
) {
506538
let json;
507539

508540
try {
509-
json = parse(body);
541+
json = parse(body, isEsm);
510542
} catch (error) {
511543
const fileInfo = file ? ` in ${file}` : '';
512544
log.warn(`Babel parse has failed: ${(error as Error).message}${fileInfo}`);

lib/walker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ function stepDetect(
325325
return true; // can i go inside?
326326
},
327327
record.file,
328+
isESMFile(record.file),
328329
);
329330
} catch (error) {
330331
log.error((error as Error).message, record.file);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// `import.meta` is only valid in `sourceType: "module"`. Before the fix for
2+
// issue #264 the SEA walker parsed this body in script mode, so the parse
3+
// failed and the detector never saw the imports below — neither the static
4+
// one nor the dynamic one ended up in the snapshot.
5+
import { greet } from './lib/helper.mjs';
6+
7+
const here = new URL(import.meta.url).pathname;
8+
console.log('here:' + here.split('/').pop());
9+
console.log('static:' + greet('world'));
10+
11+
const dyn = await import('./lib/dyn.mjs');
12+
console.log('dynamic:' + dyn.shout('world'));
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function shout(name) {
2+
return 'HELLO ' + name.toUpperCase();
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function greet(name) {
2+
return 'hello ' + name;
3+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "test-94-sea-esm-import-meta",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"main": "index.mjs",
6+
"bin": "index.mjs"
7+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env node
2+
3+
'use strict';
4+
5+
const assert = require('assert');
6+
const utils = require('../utils.js');
7+
8+
// Enhanced SEA requires Node.js >= 22
9+
if (utils.getNodeMajorVersion() < 22) {
10+
return;
11+
}
12+
13+
assert(__dirname === process.cwd());
14+
15+
const input = './app/package.json';
16+
const testName = 'test-94-sea-esm-import-meta';
17+
18+
const SEA_PLATFORM_SUFFIX = {
19+
linux: 'linux',
20+
darwin: 'macos',
21+
win32: 'win.exe',
22+
};
23+
const suffix = SEA_PLATFORM_SUFFIX[process.platform];
24+
25+
const newcomers = utils.seaHostOutputs(testName);
26+
const before = utils.filesBefore(newcomers);
27+
28+
// Capture pkg's output so we can assert the Babel parse warning (issue #264)
29+
// never surfaces when walking ESM files that use `import.meta`.
30+
const args = suffix
31+
? [input, '--sea', '--target', 'host', '--output', `${testName}-${suffix}`]
32+
: [input, '--sea'];
33+
34+
const build = utils.pkg.sync(args, { stdio: ['pipe', 'pipe', 'pipe'] });
35+
const buildLog = build.stdout + build.stderr;
36+
37+
assert(
38+
buildLog.indexOf('Babel parse has failed') === -1,
39+
'pkg must parse ESM files as modules (issue #264)\npkg output was:\n' +
40+
buildLog,
41+
);
42+
43+
// A successful parse means both imports were walked and bundled — running the
44+
// binary proves it by printing the imported values. Skip on unsupported hosts.
45+
if (suffix) {
46+
utils.assertSeaOutput(
47+
testName,
48+
'here:index.mjs\nstatic:hello world\ndynamic:HELLO WORLD\n',
49+
);
50+
}
51+
52+
utils.filesAfter(before, newcomers, { tolerateWindowsEbusy: true });

test/test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const npmTests = [
8484
'test-90-sea-worker-threads',
8585
'test-91-sea-esm-entry',
8686
'test-92-sea-tla',
87+
'test-94-sea-esm-import-meta',
8788
];
8889

8990
if (testFilter) {

0 commit comments

Comments
 (0)