import { expect } from "vitest"; import type { JavaScriptApplicationGraph } from "../../../src/domain/javascript/javascriptApplicationGraph.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js"; import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js"; const workerReferences = (graph: JavaScriptApplicationGraph) => graph.nodes .filter(({ kind }) => kind === "worker") .flatMap((node) => node.observations.map(({ properties, evidence }) => ({ source: evidence.location.available && evidence.location.value.kind === "source-range" ? evidence.location.value.source : null, declared_specifier: properties.declared_specifier, expression: properties.expression, resolved_path: properties.resolved_path, mapped_paths: graph.edges .filter( ({ relation, source_node_id }) => relation === "maps_to" && source_node_id === node.node_id, ) .map(({ properties: edge }) => edge.resolved_path), })), ); cliTest( "promotes Worker URLs only for known constructor bindings through the CLI", async ({ cli }) => { const root = await createTestTempDirectory("rea-worker-identity-"); const identityCases = [ ...["globalThis", "window", "self"].map((namespace) => ({ prefix: "", constructor: `new ${namespace}.Worker`, suffix: "", resolves: true, })), ...["worker_threads", "node:worker_threads"].flatMap((specifier) => [ { prefix: `import { Worker } from "${specifier}";`, constructor: "new Worker", suffix: "", resolves: true, }, { prefix: `import * as threads from "${specifier}";`, constructor: "new threads.Worker", suffix: "", resolves: true, }, ]), { prefix: 'import { Worker as ThreadWorker } from "node:worker_threads";', constructor: "new ThreadWorker", suffix: "", resolves: true, }, { prefix: 'import { Worker } from "node:worker_threads";', constructor: "new (Worker as typeof Worker)", suffix: "", resolves: true, }, { prefix: 'import * as threads from "node:worker_threads";', constructor: "new ((threads as typeof threads).Worker!)", suffix: "", resolves: true, }, { prefix: 'import { Worker } from "node:worker_threads"; function start(globalThis) { globalThis.URL = Fake;', constructor: "new Worker", suffix: "}", resolves: true, }, { prefix: 'globalThis.unrelated = Fake; URL.extra = "metadata";', constructor: "new Worker", suffix: "", resolves: true, }, ...[ ["class Worker {}", "new Worker", ""], ["globalThis.Worker = Fake;", "new Worker", ""], ["globalThis.Worker = Fake;", "new window.Worker", ""], ["globalThis = replacement;", "new globalThis.Worker", ""], ["function start(Worker) {", "new Worker", "}"], ['import { Worker } from "./custom.js";', "new Worker", ""], ["const custom = { Worker: class {} };", "new custom.Worker", ""], ["function start(window) {", "new window.Worker", "}"], ["function start(globalThis) {", "new globalThis.Worker", "}"], [ 'import type { Worker } from "node:worker_threads";', "new Worker", "", ], [ 'import { type Worker } from "node:worker_threads";', "new Worker", "", ], [ 'import type * as threads from "node:worker_threads";', "new threads.Worker", "", ], [ 'import threads from "node:worker_threads"; threads.Worker = Fake;', "new threads.Worker", "", ], [ 'const threads = require("node:worker_threads"); threads.Worker = Fake;', "new threads.Worker", "", ], [ 'import threads = require("node:worker_threads"); threads.Worker = Fake;', "new threads.Worker", "", ], [ 'import { Worker } from "node:worker_threads"; Worker = Fake;', "new Worker", "", ], ["", "Worker", ""], ].map(([prefix, constructor, suffix]) => ({ prefix, constructor, suffix, resolves: false, })), ].map(({ prefix, constructor, suffix, resolves }, index) => ({ source: `identity-${index}.ts`, prefix, expression: `${constructor}(new URL("./worker.js", import.meta.url))`, suffix, resolves, })); await writeFixtureFiles(root, { ...Object.fromEntries( identityCases.map(({ source, prefix, expression, suffix }) => [ source, `${prefix}\n${expression};${suffix}`, ]), ), "worker.js": "self.postMessage('existing worker file');", "custom.js": "export class Worker {}", "custom-alias.mjs": 'import { Worker as ThreadWorker } from "./custom.js"; new ThreadWorker(new URL("./worker.js", import.meta.url));', }); const result = await analyzeJavaScriptDirectory("CLI", cli, root); expect(result.statistics.parse_failures).toBe(0); const references = workerReferences(result.graph); expect(references).toHaveLength(identityCases.length); expect(references.some(({ source }) => source === "custom-alias.mjs")).toBe( false, ); for (const { source, expression, resolves } of identityCases) expect(references).toContainEqual({ source, declared_specifier: resolves ? "./worker.js" : null, expression: resolves ? null : expression, resolved_path: resolves ? "worker.js" : null, mapped_paths: resolves ? ["worker.js"] : [], }); }, ); cliTest( "resolves module-relative Worker URL constructors through the compiled CLI", async ({ cli }) => { const root = await createTestTempDirectory("rea-worker-module-url-"); const cases = [ { source: "nested/relative.mjs", expression: 'new Worker(new URL("./worker.js", import.meta.url));', specifier: "./worker.js", resolved: "nested/worker.js", }, ...[ 'import.meta.unrelated = "metadata";', 'globalThis["import.meta"] = { url: "file:///other/entry.mjs" };', 'const meta = import.meta; meta.unrelated = "metadata";', 'const ambient = globalThis; ambient.unrelated = "metadata";', 'const copy = { ...import.meta }; Object.defineProperty(copy, "url", { value: "file:///other/entry.mjs" });', "const copy = { ...globalThis }; copy.URL = replacement;", "function update(globalThis) { const ambient = globalThis; ambient.URL = replacement; } update({});", "const define = Object.defineProperty.bind(Object);", 'Object.defineProperty(...[import.meta, "unrelated", { value: "metadata" }]);', 'let meta = import.meta; meta = {}; let base = import.meta.url; base = "file:///other/entry.mjs";', ].map((mutation, index) => ({ source: `nested/unrelated-meta-${index}.mjs`, expression: `import { Worker } from "node:worker_threads"; ${mutation} new Worker(new URL("./worker.js", import.meta.url));`, specifier: "./worker.js", resolved: "nested/worker.js", })), { source: "nested/parent.mjs", expression: 'new Worker(new URL("../parent-worker.js", import.meta.url));', specifier: "../parent-worker.js", resolved: "parent-worker.js", }, { source: "nested/dot-segments.mjs", expression: 'new Worker(new URL("./child/.././worker.js", import.meta.url));', specifier: "./child/.././worker.js", resolved: "nested/worker.js", }, { source: "nested/template.mjs", expression: "new Worker(new URL(`./template-worker.js`, import.meta.url));", specifier: "./template-worker.js", resolved: "nested/template-worker.js", }, ...[ 'new URL("./worker.js", import.meta.url) as URL', 'new URL("./worker.js", import.meta.url)', 'new URL("./worker.js", import.meta.url)!', 'new URL("./worker.js", import.meta.url) satisfies URL', '((new URL("./worker.js", import.meta.url) as URL)!) satisfies URL', 'new (URL as typeof URL)("./worker.js", import.meta.url)', 'new URL("./worker.js" as string, import.meta.url as string)', 'new URL("./worker.js", (import.meta as ImportMeta).url)', 'new globalThis.URL("./worker.js", import.meta.url)', 'new window.URL("./worker.js", import.meta.url)', 'new self.URL("./worker.js", import.meta.url)', 'new global.URL("./worker.js", import.meta.url)', 'new ((globalThis as typeof globalThis)["URL"]!)("./worker.js", import.meta.url)', ].map((urlExpression, index) => ({ source: `nested/typed-${index}.ts`, expression: `new Worker(${urlExpression});`, specifier: "./worker.js", resolved: "nested/worker.js", })), { source: "nested/space.mjs", expression: 'new Worker(new URL("./space%20worker.js?worker=1#entry", import.meta.url));', specifier: "./space%20worker.js?worker=1#entry", resolved: "nested/space worker.js", }, { source: "nested/percent.mjs", expression: 'new Worker(new URL("./percent%2520worker.js", import.meta.url));', specifier: "./percent%2520worker.js", resolved: "nested/percent%20worker.js", }, { source: "nested/literal-percent.mjs", expression: 'new Worker(new URL("./percent%25worker.js", import.meta.url));', specifier: "./percent%25worker.js", resolved: "nested/percent%worker.js", }, ...["?value=", "#value="].map((suffix, index) => { const specifier = `./worker.js${suffix}%2F%2E%5C%00%FF%GG%2%\\\0`; return { source: `nested/suffix-${index}.mjs`, expression: `new Worker(new URL(${JSON.stringify(specifier)}, import.meta.url));`, specifier, resolved: "nested/worker.js", }; }), { source: "bundled/entry.mjs", expression: 'self.webpackChunkdemo.push([[1], {1: function(module, exports, require) { new Worker(new URL("./worker.js", import.meta.url)); }, "./worker.js": function(module) { module.exports = 42; }}]);', specifier: "./worker.js", resolved: "bundled/worker.js", }, { source: "nested/literal.mjs", expression: 'new Worker("./literal-worker.js");', specifier: "./literal-worker.js", resolved: "nested/literal-worker.js", }, ]; await writeFixtureFiles(root, { ...Object.fromEntries( cases.flatMap(({ source, expression, resolved }) => [ [source, expression], [resolved, `self.postMessage(${JSON.stringify(resolved)});`], ]), ), "nested/percent worker.js": "self.postMessage('double-decoding decoy');", }); const result = await analyzeJavaScriptDirectory("CLI", cli, root); expect(result.statistics.parse_failures).toBe(0); expect(workerReferences(result.graph)).toEqual( expect.arrayContaining( cases.map(({ source, specifier, resolved }) => ({ source, declared_specifier: specifier, expression: null, resolved_path: resolved, mapped_paths: [resolved], })), ), ); expect(workerReferences(result.graph)).toHaveLength(cases.length); expect(result.graph.nodes).toContainEqual( expect.objectContaining({ kind: "javascript-module", observations: expect.arrayContaining([ expect.objectContaining({ properties: expect.objectContaining({ module_key: "./worker.js" }), }), ]), }), ); }, ); cliTest( "keeps unsupported Worker URL expressions and missing exact files unresolved through the CLI", async ({ cli }) => { const root = await createTestTempDirectory("rea-worker-url-unknown-"); const malformedPercentPaths = [ ["./worker%.js", "worker%.js"], ["./worker%2.js", "worker%2.js"], ["./worker%GG.js", "worker%GG.js"], ["./space%20worker%GG.js", "space worker%GG.js"], ]; const expressions = { "dynamic-path.mjs": "new Worker(new URL(path, import.meta.url));", "absolute.mjs": 'import { Worker } from "node:worker_threads"; new Worker(new URL("/worker.js", import.meta.url));', "typed-apply-meta.ts": 'import { Worker } from "node:worker_threads"; Object.defineProperty.apply(Object, ([import.meta, "url", { value: "file:///other.mjs" }] as const)); new Worker(new URL("./worker.js", import.meta.url));', "overwritten-qualified-url.mjs": 'globalThis.URL = replacement; new Worker(new globalThis.URL("./worker.js", import.meta.url));', "shadowed-qualified-url.mjs": 'function start(window) { new Worker(new window.URL("./worker.js", import.meta.url)); }', ...Object.fromEntries( ["globalThis.URL", "URL", 'globalThis["URL"]'].map((target, index) => [ `overwritten-url-${index}.mjs`, `import { Worker } from "node:worker_threads"; const RealURL = URL; ${target} = class { constructor() { return new RealURL("./other.js", import.meta.url); } }; new Worker(new URL("./worker.js", import.meta.url));`, ]), ), ...Object.fromEntries( [ "delete globalThis.URL;", "({ URL: globalThis.URL } = replacements);", "globalThis[key] = replacement;", 'Object.defineProperty(globalThis, "URL", { value: replacement });', 'Reflect.set(globalThis, "URL", replacement);', "globalThis.globalThis.URL = replacement;", "globalThis.global.URL = replacement;", "const ambient = globalThis; ambient.URL = replacement;", "const { globalThis: ambient } = globalThis; ambient.URL = replacement;", ].map((mutation, index) => [ `mutated-url-${index}.mjs`, `import { Worker } from "node:worker_threads"; ${mutation} new Worker(new URL("./worker.js", import.meta.url));`, ]), ), ...Object.fromEntries( [ 'import.meta.url = "file:///other/entry.mjs";', "delete import.meta.url;", 'Object.defineProperty(import.meta, "url", { value: "file:///other/entry.mjs" });', "import.meta[key] = replacement;", 'const meta = import.meta; const alias = meta; alias.url = "file:///other/entry.mjs";', 'let meta = import.meta; meta.url = "file:///other/entry.mjs"; meta = {};', 'const define = Object.defineProperty; define(import.meta, "url", { value: "file:///other/entry.mjs" });', 'const set = Reflect.set.bind(Reflect); set(import.meta, "url", "file:///other/entry.mjs");', 'Object.defineProperty.bind(Object, import.meta, "url")({ value: "file:///other/entry.mjs" });', 'Object.assign(...[import.meta, { url: "file:///other/entry.mjs" }]);', 'Object.defineProperty(...[import.meta, "url", { value: "file:///other/entry.mjs" }]);', ].map((mutation, index) => [ `mutated-import-meta-${index}.mjs`, `import { Worker } from "node:worker_threads"; ${mutation} new Worker(new URL("./worker.js", import.meta.url));`, ]), ), ...Object.fromEntries( malformedPercentPaths.map(([specifier], index) => [ `malformed-percent-${index}.mjs`, `import { Worker } from "node:worker_threads"; new Worker(new URL(${JSON.stringify(specifier)}, import.meta.url));`, ]), ), "dynamic-base.mjs": 'new Worker(new URL("./worker.js", base));', "empty-worker-url.mjs": 'new Worker("");', "query-only-worker-url.mjs": 'new Worker("?cache=1");', "fragment-only-worker-url.mjs": 'new Worker("#worker");', "shadowed.mjs": 'function start(URL) { return new Worker(new URL("./worker.js", import.meta.url)); }', "typed-shadowed.ts": 'function start(URL: typeof globalThis.URL) { return new Worker((new (URL as typeof globalThis.URL)("./worker.js", import.meta.url) as URL)!); }', "typed-dynamic-path.ts": "new Worker((new URL(path as string, import.meta.url) as URL)!);", "typed-dynamic-base.ts": 'new Worker((new URL("./worker.js", base as string) as URL)!);', "call.mjs": 'new Worker(URL("./worker.js", import.meta.url));', "custom.mjs": 'new Worker(new custom.URL("./worker.js", import.meta.url));', "extension.mjs": 'new Worker(new URL("./extensionless", import.meta.url));', "directory.mjs": 'new Worker(new URL("./directory", import.meta.url));', "directory-dot.mjs": 'new Worker(new URL("./worker.js/.", import.meta.url));', "directory-parent.mjs": 'new Worker(new URL("./worker.js/child/..", import.meta.url));', "empty-segment.mjs": 'new Worker(new URL("./child//../worker.js", import.meta.url));', "package.mjs": 'new Worker(new URL("./package-worker", import.meta.url));', "external.mjs": 'new Worker(new URL("https://example.test/worker.js", import.meta.url));', "empty.mjs": 'new Worker(new URL("", import.meta.url));', "query.mjs": 'new Worker(new URL("?worker=1", import.meta.url));', "whitespace.mjs": 'new Worker(new URL(" ./worker.js ", import.meta.url));', "control.mjs": 'new Worker(new URL("./worker\\t.js", import.meta.url));', "leading-control.mjs": 'new Worker(new URL("\\u0001./worker.js", import.meta.url));', }; const knownSpecifiers: Readonly> = { "extension.mjs": "./extensionless", "directory.mjs": "./directory", "directory-dot.mjs": "./worker.js/.", "directory-parent.mjs": "./worker.js/child/..", "empty-segment.mjs": "./child//../worker.js", "empty-worker-url.mjs": "", "query-only-worker-url.mjs": "?cache=1", "fragment-only-worker-url.mjs": "#worker", "package.mjs": "./package-worker", "external.mjs": "https://example.test/worker.js", }; await writeFixtureFiles(root, { ...expressions, ...Object.fromEntries( malformedPercentPaths.map(([, decoy]) => [ decoy, "self.postMessage('malformed percent pathname decoy');", ]), ), "worker.js": "self.postMessage('existing decoy');", "other.js": "self.postMessage('custom URL result decoy');", "extensionless.js": "self.postMessage('extension decoy');", "directory/index.js": "self.postMessage('index decoy');", "package-worker/package.json": JSON.stringify({ main: "selected.js" }), "package-worker/selected.js": "self.postMessage('package main decoy');", }); const result = await analyzeJavaScriptDirectory("CLI", cli, root); expect(result.statistics.parse_failures).toBe(0); const references = workerReferences(result.graph); expect(references).toHaveLength(Object.keys(expressions).length); for (const [source, expression] of Object.entries(expressions)) { const specifier = knownSpecifiers[source] ?? null; expect(references).toContainEqual( expect.objectContaining({ source, declared_specifier: specifier, expression: specifier === null ? expression.slice( expression.indexOf("new Worker("), expression.lastIndexOf(";"), ) : null, resolved_path: null, mapped_paths: [], }), ); } }, ); cliTest( "bounds repeated ambient alias branches through the compiled CLI", async ({ cli }) => { const root = await createTestTempDirectory("rea-worker-alias-branches-"); const aliases = [ "const alias0 = globalThis;", ...Array.from( { length: 40 }, (_, index) => `const alias${index + 1} = flag${index} ? alias${index} : alias${index};`, ), "alias40.URL = replacement;", ].join("\n"); const splitAliases = [ "const alias0 = globalThis;", ...Array.from( { length: 40 }, (_, index) => `const left${index} = alias${index}; const right${index} = alias${index}; const alias${index + 1} = flag${index} ? left${index} : right${index};`, ), "alias40.URL = replacement;", ].join("\n"); const memberAliases = [ "const alias0 = globalThis;", ...Array.from( { length: 40 }, (_, index) => `const alias${index + 1} = flag${index} ? alias${index}.globalThis : alias${index}.window;`, ), "alias40.URL = replacement;", ].join("\n"); const expression = 'new Worker(new URL("./worker.js", import.meta.url))'; await writeFixtureFiles(root, { "entry.mjs": `import { Worker } from "node:worker_threads";\n${aliases}\n${expression};`, "split.mjs": `import { Worker } from "node:worker_threads";\n${splitAliases}\n${expression};`, "member.mjs": `import { Worker } from "node:worker_threads";\n${memberAliases}\n${expression};`, "worker.js": "self.postMessage('existing alias-mutation decoy');", }); const result = await analyzeJavaScriptDirectory( "CLI", { run: (invocation) => cli.run({ ...invocation, timeoutMs: 20_000 }) }, root, ); expect(result.statistics.parse_failures).toBe(0); const references = workerReferences(result.graph); expect(references).toHaveLength(3); expect(references).toEqual( expect.arrayContaining( ["entry.mjs", "split.mjs", "member.mjs"].map((source) => ({ source, declared_specifier: null, expression, resolved_path: null, mapped_paths: [], })), ), ); }, ); cliTest( "keeps Node Worker filesystem strings unresolved through the compiled CLI", async ({ cli }) => { const root = await createTestTempDirectory("rea-node-worker-filesystem-"); const cases = [ { prefix: 'import { Worker } from "node:worker_threads";', expression: 'new Worker("./worker.js")', }, { prefix: 'import { Worker as ThreadWorker } from "worker_threads";', expression: 'new ThreadWorker("/worker.js")', }, { prefix: 'import * as threads from "node:worker_threads";', expression: 'new threads.Worker("./worker.js")', }, { prefix: 'import { Worker } from "node:worker_threads";', expression: 'new (Worker as typeof Worker)("/worker.js")', }, { prefix: 'import * as threads from "node:worker_threads";', expression: 'new ((threads as typeof threads).Worker!)("./worker.js")', }, ].map((fixture, index) => ({ ...fixture, source: `nested/node-literal-${index}.ts`, })); await writeFixtureFiles(root, { ...Object.fromEntries( cases.map(({ prefix, expression, source }) => [ source, `${prefix}\n${expression};`, ]), ), "worker.js": "self.postMessage('filesystem-root decoy');", "nested/worker.js": "self.postMessage('source-sibling decoy');", }); const result = await analyzeJavaScriptDirectory("CLI", cli, root); expect(result.statistics.parse_failures).toBe(0); const references = workerReferences(result.graph); expect(references).toHaveLength(cases.length); for (const { source, expression } of cases) expect(references).toContainEqual({ source, declared_specifier: null, expression, resolved_path: null, mapped_paths: [], }); }, ); // Windows cannot materialize the pipe-named directory needed for this decoy. cliTest.skipIf(process.platform === "win32")( "keeps file-URL drive letters distinct from encoded POSIX paths through the compiled CLI", async ({ cli }) => { const root = await createTestTempDirectory("rea-worker-url-drive-"); await writeFixtureFiles(root, { "dist/drive.mjs": 'new Worker(new URL("c|/worker.js", import.meta.url));', "dist/encoded.mjs": 'new Worker(new URL("c%7C/worker.js", import.meta.url));', "dist/c|/worker.js": "self.postMessage('existing relative-path decoy');", }); const result = await analyzeJavaScriptDirectory("CLI", cli, root); expect(result.statistics.parse_failures).toBe(0); const references = workerReferences(result.graph); expect(references).toHaveLength(2); expect(references).toEqual( expect.arrayContaining([ { source: "dist/drive.mjs", declared_specifier: "c|/worker.js", expression: null, resolved_path: null, mapped_paths: [], }, { source: "dist/encoded.mjs", declared_specifier: "c%7C/worker.js", expression: null, resolved_path: "dist/c|/worker.js", mapped_paths: ["dist/c|/worker.js"], }, ]), ); }, );