diff --git a/src/browser/WebSourceMapFetcher.ts b/src/browser/WebSourceMapFetcher.ts index 5c8f86ea0..5dbbe2ad1 100644 --- a/src/browser/WebSourceMapFetcher.ts +++ b/src/browser/WebSourceMapFetcher.ts @@ -11,6 +11,7 @@ import { isUrlLikeModuleSpecifier } from "../domain/webBundleAnalyzerAst.js"; import { analyzeParsedJavaScriptReferences } from "../domain/javascript/javascriptSemanticAnalysis.js"; import { traverseJavaScriptAst } from "../domain/javascript/javascriptSemanticTraversal.js"; import { parseJavaScriptSource } from "../domain/javascript/javascriptSourceParser.js"; +import { stringValue } from "../domain/javascript/javascriptStaticAnalysisHelpers.js"; import type { AnalyzeWebBundleInput, WebSourceMapItem, @@ -1049,18 +1050,21 @@ const originalDependency = ( return node.source === null || node.source === undefined ? null : { kind: "static_import", specifier: node.source.value }; - if (t.isImportExpression(node) && t.isStringLiteral(node.source)) - return { kind: "dynamic_import", specifier: node.source.value }; + if (t.isImportExpression(node)) { + const specifier = stringValue(node.source); + return specifier === undefined + ? null + : { kind: "dynamic_import", specifier }; + } if ( (t.isCallExpression(node) || t.isOptionalCallExpression(node)) && - t.isIdentifier(node.callee, { name: "require" }) && - t.isStringLiteral(node.arguments[0]) - ) - return { - kind: "require", - specifier: node.arguments[0].value, - requireCallee: node.callee, - }; + t.isIdentifier(node.callee, { name: "require" }) + ) { + const specifier = stringValue(node.arguments[0]); + return specifier === undefined + ? null + : { kind: "require", specifier, requireCallee: node.callee }; + } // `import x = require("m")` is only legal at module top level and always // refers to the host loader, so it needs no unbound-`require` check. The // `moduleReference` is an entity name for a local alias instead, which diff --git a/src/domain/webBundleAnalyzer.test.ts b/src/domain/webBundleAnalyzer.test.ts index edcb37a20..93bb24fdc 100644 --- a/src/domain/webBundleAnalyzer.test.ts +++ b/src/domain/webBundleAnalyzer.test.ts @@ -662,6 +662,28 @@ describe("web metadata key semantics", () => { }); }); +it("reads expression-free template dynamic imports as chunk edges", () => { + const result = analyzeCapturedWebBundle( + inspection(` + import(\`./lazy.js\`); + import("./quoted.js"); + import(\`./\${name}.js\`); + `), + ); + + expect(result.observations.chunks.edges).toEqual([ + expect.objectContaining({ + kind: "dynamic_import", + specifier: "./lazy.js", + resolved_url: `${origin}/assets/lazy.js`, + }), + expect.objectContaining({ + kind: "dynamic_import", + specifier: "./quoted.js", + }), + ]); +}); + it("does not infer call names from dynamic computed identifiers", () => { const result = analyzeCapturedWebBundle( inspection(` diff --git a/src/domain/webBundleAnalyzerInspection.ts b/src/domain/webBundleAnalyzerInspection.ts index 12c35109c..f48faf552 100644 --- a/src/domain/webBundleAnalyzerInspection.ts +++ b/src/domain/webBundleAnalyzerInspection.ts @@ -123,14 +123,17 @@ const inspectNode = ( node, accumulator, }); - else if (t.isImportExpression(node) && t.isStringLiteral(node.source)) - addEdge({ - script, - specifier: node.source.value, - kind: "dynamic_import", - node, - accumulator, - }); + else if (t.isImportExpression(node)) { + const specifier = stringValue(node.source); + if (specifier !== undefined) + addEdge({ + script, + specifier, + kind: "dynamic_import", + node, + accumulator, + }); + } if (t.isObjectProperty(node)) inspectRouteProperty(script, node, accumulator); if ( t.isCallExpression(node) || diff --git a/tests/boundary/browser/webSourceMapFetcher.test.ts b/tests/boundary/browser/webSourceMapFetcher.test.ts index 5fab66f58..b5a15e120 100644 --- a/tests/boundary/browser/webSourceMapFetcher.test.ts +++ b/tests/boundary/browser/webSourceMapFetcher.test.ts @@ -988,6 +988,41 @@ describe("source-map original dependency syntax", () => { result.items[0]?.original_module_edges.map(({ specifier }) => specifier), ).toEqual(["./real.js", "./dynamic.js", "./common.cjs"]); }); + + it("reads expression-free template module specifiers as literals", async () => { + const text = [ + "import(`./dep.js`);", + "import(`./dep.${name}.js`);", + "require(`./common.cjs`);", + "require(`./${name}.cjs`);", + ].join("\n"); + const result = await fetchWebSourceMaps([request], input(), undefined, { + fetch: () => + Promise.resolve( + new Response( + JSON.stringify({ + version: 3, + names: [], + sources: ["main.ts"], + sourcesContent: [text], + mappings: "AAAA", + }), + ), + ), + }); + const item = result.items[0]; + if (item?.status !== "included") throw new Error("map not included"); + expect(item.original_module_edges).toEqual([ + expect.objectContaining({ + kind: "dynamic_import", + specifier: "./dep.js", + resolved_source: `${origin}/assets/dep.js`, + }), + expect.objectContaining({ kind: "require", specifier: "./common.cjs" }), + ]); + expect(item.original_sources[0]?.artifact?.text).toBe(text); + expect(item.mappings).toHaveLength(1); + }); }); describe("source-map original syntax boundaries", () => {