fix(browser): retain optional source-map requires (#2886)
Carry the require callee from dependency extraction into lexical scope validation so optional calls remain visible without admitting shadowed loaders. Fixes #2883
This commit is contained in:
@@ -743,8 +743,8 @@ const originalModuleEdges = (
|
||||
check();
|
||||
const dependency = originalDependency(node);
|
||||
if (dependency === null) return;
|
||||
const { kind, specifier } = dependency;
|
||||
if (kind === "require" && t.isCallExpression(node)) {
|
||||
const { kind, specifier, requireCallee } = dependency;
|
||||
if (requireCallee !== undefined) {
|
||||
unboundRequires ??= new Set(
|
||||
analyzeParsedJavaScriptReferences(parsed, "require")
|
||||
.filter(
|
||||
@@ -756,7 +756,7 @@ const originalModuleEdges = (
|
||||
`${String(location.start.line)}:${String(location.start.column)}`,
|
||||
),
|
||||
);
|
||||
const location = node.callee.loc?.start;
|
||||
const location = requireCallee.loc?.start;
|
||||
if (
|
||||
location === undefined ||
|
||||
!unboundRequires.has(
|
||||
@@ -1039,6 +1039,7 @@ const originalDependency = (
|
||||
): {
|
||||
readonly kind: ParsedSourceMapItem["original_module_edges"][number]["kind"];
|
||||
readonly specifier: string;
|
||||
readonly requireCallee?: t.Identifier;
|
||||
} | null => {
|
||||
if (
|
||||
t.isImportDeclaration(node) ||
|
||||
@@ -1051,11 +1052,15 @@ const originalDependency = (
|
||||
if (t.isImportExpression(node) && t.isStringLiteral(node.source))
|
||||
return { kind: "dynamic_import", specifier: node.source.value };
|
||||
if (
|
||||
t.isCallExpression(node) &&
|
||||
(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 };
|
||||
return {
|
||||
kind: "require",
|
||||
specifier: node.arguments[0].value,
|
||||
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
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { createServer } from "node:http";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { fetchWebSourceMaps } from "../../../src/browser/WebSourceMapFetcher.js";
|
||||
import { analyzeWebBundleInputSchema } from "../../../src/domain/webBundleAnalysis.js";
|
||||
|
||||
it("retains unshadowed optional require dependencies from HTTP source-map originals", async () => {
|
||||
const source = [
|
||||
'require("./normal.cjs");',
|
||||
'require?.("./optional.cjs");',
|
||||
'function local(require) { require?.("./shadowed.cjs"); }',
|
||||
"require?.(selectedModule);",
|
||||
].join("\n");
|
||||
const text = JSON.stringify({
|
||||
version: 3,
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
sources: ["main.cjs"],
|
||||
sourcesContent: [source],
|
||||
});
|
||||
const server = createServer((_request, response) => response.end(text));
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
if (address === null || typeof address === "string")
|
||||
throw new TypeError("Expected a TCP listener address");
|
||||
const origin = `http://127.0.0.1:${String(address.port)}`;
|
||||
const url = `${origin}/app.js.map`;
|
||||
const result = await fetchWebSourceMaps(
|
||||
[{ scriptKey: `scr_${"1".repeat(64)}`, declaredUrl: url, fetchUrl: url }],
|
||||
analyzeWebBundleInputSchema.parse({
|
||||
cdp_endpoint: origin,
|
||||
target_id: "fixture",
|
||||
allowed_origins: [origin],
|
||||
fetch_source_maps: true,
|
||||
}),
|
||||
);
|
||||
expect(result.items[0]).toMatchObject({
|
||||
status: "included",
|
||||
artifact: {
|
||||
text,
|
||||
bytes: Buffer.byteLength(text),
|
||||
sha256: createHash("sha256").update(text).digest("hex"),
|
||||
},
|
||||
original_sources: [{ artifact: { text: source } }],
|
||||
original_module_edges: ["./normal.cjs", "./optional.cjs"].map(
|
||||
(specifier) => ({
|
||||
from_source: `${origin}/main.cjs`,
|
||||
kind: "require",
|
||||
specifier,
|
||||
resolved_source: new URL(specifier, origin).href,
|
||||
}),
|
||||
),
|
||||
});
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
if (server.listening)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) =>
|
||||
error === undefined ? resolve() : reject(error),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user