diff --git a/docs/javascript-artifact-reconstruction.md b/docs/javascript-artifact-reconstruction.md index 90569cb7f5..825b36c8b0 100644 --- a/docs/javascript-artifact-reconstruction.md +++ b/docs/javascript-artifact-reconstruction.md @@ -419,7 +419,9 @@ establish a browser or Electron API. The shared lexical, ambient-write, and member-mutation owners must establish the receiver before static API facts become roles or storage observations. Path `join`, `resolve`, and `fileURLToPath` use proven Node imports; source-relative URL conversion additionally requires an -unmodified global URL constructor and `import.meta.url`. Other calls preserve +unmodified global or proven Node `url.URL` constructor and `import.meta.url`. +Relative results of native path operations retain filesystem-expression context; +their spelling does not turn them into module specifiers. Other calls preserve their expressions. A property named `preload` outside a proven BrowserWindow configuration does not establish a script entrypoint. Renderer recovery currently covers direct BrowserWindow instances, stable local aliases, and their diff --git a/src/domain/javascript/javascriptStaticApiIdentity.test.ts b/src/domain/javascript/javascriptStaticApiIdentity.test.ts index 0d4c5a5d46..4e49438631 100644 --- a/src/domain/javascript/javascriptStaticApiIdentity.test.ts +++ b/src/domain/javascript/javascriptStaticApiIdentity.test.ts @@ -21,6 +21,21 @@ it.each([ }, ); +it.each([ + ["", "./native.js"], + ["url.URL = class {};", null], +] as const)( + "requires the native URL constructor identity after %s", + (write, expected) => { + const analysis = analyzeJavaScriptStaticSource(`${windowPrefix} + import url from 'node:url';${write} + new BrowserWindow({webPreferences:{preload:fileURLToPath(new (url.URL as typeof URL)("./native.js", import.meta.url))}});`); + expect(analysis.electron.browser_windows).toMatchObject([ + { preload_path: expected }, + ]); + }, +); + it.each([ '["a", "b"].join("/")', 'settings.join("/app", "preload.js")', diff --git a/src/domain/javascript/javascriptStaticPathIntrinsics.ts b/src/domain/javascript/javascriptStaticPathIntrinsics.ts index 899cf5af74..bc363bb4d0 100644 --- a/src/domain/javascript/javascriptStaticPathIntrinsics.ts +++ b/src/domain/javascript/javascriptStaticPathIntrinsics.ts @@ -71,7 +71,8 @@ export const createStaticPathIntrinsicQuery = ( kind === "source-url" ? t.isNewExpression(callee) && callee.arguments.length === 2 && - isUnmodifiedGlobalReference(callee.callee, index.bindings, "URL") && + (isUnmodifiedGlobalReference(callee.callee, index.bindings, "URL") || + nativeMethod(callee.callee, "URL", ["url"])) && isUnmodifiedImportMetaUrl(callee.arguments[1], index.bindings) : kind === "file-url-to-path" ? nativeMethod(callee, "fileURLToPath", ["url"]) diff --git a/src/domain/javascript/javascriptStaticPathResolution.test.ts b/src/domain/javascript/javascriptStaticPathResolution.test.ts index 7256e389bb..ed2a287bd3 100644 --- a/src/domain/javascript/javascriptStaticPathResolution.test.ts +++ b/src/domain/javascript/javascriptStaticPathResolution.test.ts @@ -221,7 +221,7 @@ describe("inert JavaScript path resolution", () => { [ 'path.join("sub", "../preload.js")', posix.join("sub", "../preload.js"), - "module-specifier", + "filesystem-expression", ], [ 'path.join(__dirname, "preload.js")', @@ -253,7 +253,7 @@ describe("inert JavaScript path resolution", () => { "preload.js", "filesystem-expression", ], - ['join("sub", "preload.js")', "sub/preload.js", "module-specifier"], + ['join("sub", "preload.js")', "sub/preload.js", "filesystem-expression"], ["path.join(__dirname)", ".", "filesystem-expression"], ] as const)("preserves join behavior in %s", (expression, path, context) => { assertPathProjection(expression, path, context); @@ -394,7 +394,7 @@ const parsePathExpression = (expression: string) => const analyzePathConsumers = (expression: string) => { const property = `preload: ${expression}`; const genericPrefix = - 'import path, {dirname} from "node:path"; import {fileURLToPath} from "node:url"; const options = { '; + 'import path, {dirname,join} from "node:path"; import url, {fileURLToPath} from "node:url"; const options = { '; const windowPrefix = "new BrowserWindow({ webPreferences: { "; const windowExpression = `${windowPrefix}${property} } })`; const utilityExpression = `utilityProcess.fork(${expression})`; @@ -437,7 +437,11 @@ const assertPathProjection = ( if (fact === "source-url") return true; const name = calleeName(callee); if (fact === "file-url-to-path") return name.endsWith("fileURLToPath"); - return name.endsWith(`.${fact ?? "resolve"}`) && !name.includes("win32"); + return ( + (name === (fact ?? "resolve") || + name.endsWith(`.${fact ?? "resolve"}`)) && + !name.includes("win32") + ); }; expect(staticPath(node, pathIntrinsics)).toBe(path); expect(staticPathResolutionContext(node, pathIntrinsics)).toBe(context);