Skip to content

Commit ce81d7b

Browse files
committed
[FIX] Bundler: Also detect import.meta as ESM indicator
Modules using import.meta (e.g. import.meta.url, import.meta.resolve) were not detected as ESM because only the 'import'/'export' parse error was checked. Now also handle the 'Cannot use import.meta outside a module' error from espree.
1 parent a6de6eb commit ce81d7b

2 files changed

Lines changed: 130 additions & 1 deletion

File tree

lib/lbt/resources/ResourcePool.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ async function determineDependencyInfo(resource, rawInfo, pool) {
6464
} catch (err) {
6565
log.verbose(`Failed to parse ${resource.name}: ${err.message}`);
6666
log.verbose(err.stack);
67-
if (err.message.includes("'import' and 'export' may appear only with 'sourceType: module'")) {
67+
if (err.message.includes("'import' and 'export' may appear only with 'sourceType: module'") ||
68+
err.message.includes("Cannot use 'import.meta' outside a module")) {
6869
info.format = ModuleInfo.Format.ESM;
6970
}
7071
}

test/lib/lbt/resources/ResourcePool.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,3 +419,131 @@ test("getModuleInfo: determineDependencyInfo for ESM js resource", async (t) =>
419419
);
420420
});
421421

422+
test("getModuleInfo: determineDependencyInfo for ESM js resource using import.meta", async (t) => {
423+
const {ResourcePool, verboseLogStub, errorLogStub} = t.context;
424+
425+
const resourcePool = new ResourcePool();
426+
const esmCode = `const url = import.meta.url;`;
427+
const inputJsResource = {name: "thirdparty/esm-meta-module.js", buffer: async () => esmCode};
428+
resourcePool.addResource(inputJsResource);
429+
430+
const info = await resourcePool.getModuleInfo("thirdparty/esm-meta-module.js");
431+
432+
t.is(info.name, "thirdparty/esm-meta-module.js", "name should be set");
433+
t.is(info.size, esmCode.length, "size should be set");
434+
t.is(info.format, "esm", "format should be set to ESM");
435+
t.deepEqual(info.dependencies, [], "no dependencies should be found since parsing failed");
436+
437+
t.is(errorLogStub.callCount, 0, "log.error should not be called for ESM parse failure");
438+
t.true(verboseLogStub.callCount >= 1, "log.verbose should be called");
439+
t.true(
440+
verboseLogStub.getCalls().some((call) =>
441+
call.args[0].includes("Failed to parse thirdparty/esm-meta-module.js") &&
442+
call.args[0].includes("Cannot use 'import.meta' outside a module")
443+
),
444+
"log.verbose should contain the import.meta parse error message"
445+
);
446+
});
447+
448+
test("getModuleInfo: determineDependencyInfo for ESM js resource using side-effect import", async (t) => {
449+
const {ResourcePool, errorLogStub} = t.context;
450+
451+
const resourcePool = new ResourcePool();
452+
const esmCode = `import "./side-effect.js";`;
453+
const inputJsResource = {name: "thirdparty/esm-side-effect.js", buffer: async () => esmCode};
454+
resourcePool.addResource(inputJsResource);
455+
456+
const info = await resourcePool.getModuleInfo("thirdparty/esm-side-effect.js");
457+
458+
t.is(info.format, "esm", "format should be set to ESM for side-effect import");
459+
t.is(errorLogStub.callCount, 0, "log.error should not be called");
460+
});
461+
462+
test("getModuleInfo: determineDependencyInfo for ESM js resource using re-export", async (t) => {
463+
const {ResourcePool, errorLogStub} = t.context;
464+
465+
const resourcePool = new ResourcePool();
466+
const esmCode = `export { foo } from "./foo.js";`;
467+
const inputJsResource = {name: "thirdparty/esm-reexport.js", buffer: async () => esmCode};
468+
resourcePool.addResource(inputJsResource);
469+
470+
const info = await resourcePool.getModuleInfo("thirdparty/esm-reexport.js");
471+
472+
t.is(info.format, "esm", "format should be set to ESM for re-export");
473+
t.is(errorLogStub.callCount, 0, "log.error should not be called");
474+
});
475+
476+
test("getModuleInfo: determineDependencyInfo for ESM js resource using export *", async (t) => {
477+
const {ResourcePool, errorLogStub} = t.context;
478+
479+
const resourcePool = new ResourcePool();
480+
const esmCode = `export * from "./foo.js";`;
481+
const inputJsResource = {name: "thirdparty/esm-export-star.js", buffer: async () => esmCode};
482+
resourcePool.addResource(inputJsResource);
483+
484+
const info = await resourcePool.getModuleInfo("thirdparty/esm-export-star.js");
485+
486+
t.is(info.format, "esm", "format should be set to ESM for export *");
487+
t.is(errorLogStub.callCount, 0, "log.error should not be called");
488+
});
489+
490+
test("getModuleInfo: determineDependencyInfo for ESM js resource using named export", async (t) => {
491+
const {ResourcePool, errorLogStub} = t.context;
492+
493+
const resourcePool = new ResourcePool();
494+
const esmCode = `export const myVar = 42;`;
495+
const inputJsResource = {name: "thirdparty/esm-named-export.js", buffer: async () => esmCode};
496+
resourcePool.addResource(inputJsResource);
497+
498+
const info = await resourcePool.getModuleInfo("thirdparty/esm-named-export.js");
499+
500+
t.is(info.format, "esm", "format should be set to ESM for named export");
501+
t.is(errorLogStub.callCount, 0, "log.error should not be called");
502+
});
503+
504+
test("getModuleInfo: determineDependencyInfo for ESM js resource using import.meta.resolve", async (t) => {
505+
const {ResourcePool, errorLogStub} = t.context;
506+
507+
const resourcePool = new ResourcePool();
508+
const esmCode = `const resolved = import.meta.resolve("./foo.js");`;
509+
const inputJsResource = {name: "thirdparty/esm-meta-resolve.js", buffer: async () => esmCode};
510+
resourcePool.addResource(inputJsResource);
511+
512+
const info = await resourcePool.getModuleInfo("thirdparty/esm-meta-resolve.js");
513+
514+
t.is(info.format, "esm", "format should be set to ESM for import.meta.resolve");
515+
t.is(errorLogStub.callCount, 0, "log.error should not be called");
516+
});
517+
518+
test("getModuleInfo: determineDependencyInfo for ESM js resource using top-level await", async (t) => {
519+
const {ResourcePool} = t.context;
520+
521+
// Top-level await is ESM-only syntax, but the parser produces a generic "Unexpected token"
522+
// error that cannot be reliably distinguished from actual syntax errors.
523+
// Therefore, the module is NOT detected as ESM.
524+
const resourcePool = new ResourcePool();
525+
const esmCode = `const x = await Promise.resolve(1);`;
526+
const inputJsResource = {name: "thirdparty/esm-top-level-await.js", buffer: async () => esmCode};
527+
resourcePool.addResource(inputJsResource);
528+
529+
const info = await resourcePool.getModuleInfo("thirdparty/esm-top-level-await.js");
530+
531+
t.is(info.format, undefined, "format should not be set for top-level await (not detectable)");
532+
});
533+
534+
test("getModuleInfo: determineDependencyInfo for js resource using dynamic import()", async (t) => {
535+
const {ResourcePool, errorLogStub} = t.context;
536+
537+
// Dynamic import() is valid in both scripts and modules, so it parses successfully
538+
// and should NOT be flagged as ESM.
539+
const resourcePool = new ResourcePool();
540+
const code = `const m = import("./foo.js");`;
541+
const inputJsResource = {name: "thirdparty/dynamic-import.js", buffer: async () => code};
542+
resourcePool.addResource(inputJsResource);
543+
544+
const info = await resourcePool.getModuleInfo("thirdparty/dynamic-import.js");
545+
546+
t.not(info.format, "esm", "format should not be ESM for dynamic import()");
547+
t.is(errorLogStub.callCount, 0, "log.error should not be called");
548+
});
549+

0 commit comments

Comments
 (0)