From f974353c329ebfa58ba5f268078da30952a82ec0 Mon Sep 17 00:00:00 2001
From: Chaoqi <136579807+Chaoqi31@users.noreply.github.com>
Date: Tue, 13 Oct 2026 08:10:10 -0400
Subject: [PATCH] fix(javascript): keep paths after __dirname inside the source
directory (#2366)
Node's path.join does not reset at an absolute segment, so
path.join(__dirname, '/preload.js') is
/preload.js. The static path
projection skipped the __dirname anchor and joined the remaining parts,
which turned that common spelling into the root path /preload.js. In an
app whose main script lives in src/, the BrowserWindow preload was then
reported not-found, or resolved to an unrelated preload.js at the artifact
root, and the utilityProcess.fork target was never resolved. String
concatenation had the same defect: path.dirname(__filename) +
'/preload.js' projected to /preload.js, and __dirname + '/preload.js'
stayed unknown.
A source directory now contributes "." to a join, so path.join(__dirname)
projects to "." like path.resolve(__dirname). A concatenation that starts
with __dirname or the source file's dirname keeps text that begins with a
separator inside that directory; other text extends the directory's own
name and stays unknown. The empty-string projection of
path.dirname(__filename), which produced the wrong concatenation, is
removed, so a bare path.dirname(__filename) is unknown like a bare
__dirname.
---
.../javascriptStaticAnalysisHelpers.ts | 60 ++++++++++++++-----
.../javascriptStaticPathResolution.test.ts | 17 +++++-
2 files changed, 60 insertions(+), 17 deletions(-)
diff --git a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts
index 71562e6ca2..f2045a1eba 100644
--- a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts
+++ b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts
@@ -181,18 +181,37 @@ export const staticPath = (node: t.Node): string | undefined => {
if (t.isStringLiteral(node)) return node.value;
if (t.isTemplateLiteral(node) && node.expressions.length === 0)
return stringValue(node);
- if (t.isBinaryExpression(node, { operator: "+" })) {
- const left = staticPath(node.left);
- const right = staticPath(node.right);
- return left === undefined || right === undefined
- ? undefined
- : `${left}${right}`;
- }
+ if (t.isBinaryExpression(node, { operator: "+" }))
+ return staticConcatenation(node);
if (t.isCallExpression(node) || t.isNewExpression(node))
return staticCallPath(node);
return undefined;
};
+/**
+ * Concatenate inert text. A leading source directory contributes no text:
+ * `__dirname + "/preload.js"` names a file inside it, while text without a
+ * leading separator extends the directory's own name and stays unknown.
+ */
+const staticConcatenation = (node: t.BinaryExpression): string | undefined => {
+ const operands: t.Node[] = [];
+ let current: t.Node = node;
+ while (t.isBinaryExpression(current, { operator: "+" })) {
+ operands.push(current.right);
+ current = current.left;
+ }
+ const anchored = isDirectoryPath(current);
+ if (!anchored) operands.push(current);
+ let text = "";
+ for (const operand of operands.reverse()) {
+ const value = staticPath(operand);
+ if (value === undefined) return undefined;
+ text += value;
+ }
+ if (!anchored) return text;
+ return text.startsWith("/") ? posix.join(".", text) : undefined;
+};
+
const staticCallPath = (
node: t.CallExpression | t.NewExpression,
): string | undefined => {
@@ -204,11 +223,6 @@ const staticCallPath = (
if (name === "fileURLToPath" || name.endsWith(".fileURLToPath")) {
return staticFileUrlPath(argumentNode(node.arguments[0]));
}
- if (
- (name === "dirname" || name.endsWith(".dirname")) &&
- isFileIdentity(argumentNode(node.arguments[0]))
- )
- return "";
if (name.endsWith(".resolve"))
return name === "win32.resolve" || name.endsWith(".win32.resolve")
? undefined
@@ -216,7 +230,12 @@ const staticCallPath = (
if (!name.endsWith(".join") && name !== "join") return undefined;
const parts: string[] = [];
for (const argument of node.arguments) {
- if (isDirectoryIdentity(argumentNode(argument))) continue;
+ if (isDirectoryIdentity(argumentNode(argument))) {
+ // join never resets at an absolute segment, so `/preload.js` after the
+ // source directory stays inside it.
+ parts.push(".");
+ continue;
+ }
const value = t.isNode(argument) ? staticPath(argument) : undefined;
if (value === undefined) return undefined;
parts.push(value);
@@ -331,11 +350,20 @@ const isFilesystemPathExpression = (node: t.Node): boolean => {
};
const isDirectoryIdentity = (node: t.Node | undefined): boolean => {
- if (t.isIdentifier(node, { name: "__dirname" })) return true;
+ if (isDirectoryPath(node)) return true;
if (!t.isCallExpression(node) && !t.isNewExpression(node)) return false;
const name = calleeName(node.callee);
- if (name === "URL" || name.endsWith(".URL"))
- return isFileIdentity(argumentNode(node.arguments[1]));
+ return (
+ (name === "URL" || name.endsWith(".URL")) &&
+ isFileIdentity(argumentNode(node.arguments[1]))
+ );
+};
+
+/** `__dirname` or the dirname of the source file, as a string. */
+const isDirectoryPath = (node: t.Node | undefined): boolean => {
+ if (t.isIdentifier(node, { name: "__dirname" })) return true;
+ if (!t.isCallExpression(node)) return false;
+ const name = calleeName(node.callee);
return (
(name === "dirname" || name.endsWith(".dirname")) &&
isFileIdentity(argumentNode(node.arguments[0]))
diff --git a/src/domain/javascript/javascriptStaticPathResolution.test.ts b/src/domain/javascript/javascriptStaticPathResolution.test.ts
index e54122440f..30bbc317ff 100644
--- a/src/domain/javascript/javascriptStaticPathResolution.test.ts
+++ b/src/domain/javascript/javascriptStaticPathResolution.test.ts
@@ -38,6 +38,9 @@ const anchoredExpressions = [
["path.resolve(__dirname)", "."],
['path.posix.resolve(`/app`, `/shared`, "preload.js")', "/shared/preload.js"],
['path.posix.resolve("" + "/app", "preload.js")', "/app/preload.js"],
+ ['__dirname + "/preload.js"', "preload.js"],
+ ['__dirname + "/" + "preload.js"', "preload.js"],
+ ['path.dirname(__filename) + "/lib" + "/preload.js"', "lib/preload.js"],
] as const;
const unknownExpressions = [
@@ -58,6 +61,8 @@ const unknownExpressions = [
'path.win32.resolve(__dirname, "preload.js")',
'win32.resolve("/app", "preload.js")',
'resolve("/app", "preload.js")',
+ '__dirname + "preload.js"',
+ 'path.dirname(__filename) + "preload.js"',
];
describe("inert JavaScript path resolution", () => {
@@ -100,6 +105,16 @@ describe("inert JavaScript path resolution", () => {
"preload.js",
"filesystem-expression",
],
+ [
+ 'path.join(__dirname, "/preload.js")',
+ "preload.js",
+ "filesystem-expression",
+ ],
+ [
+ 'path.join(dirname(fileURLToPath(import.meta.url)), "/lib", "/preload.js")',
+ "lib/preload.js",
+ "filesystem-expression",
+ ],
[
'path.join("ignored", __dirname, "preload.js")',
"ignored/preload.js",
@@ -111,6 +126,7 @@ describe("inert JavaScript path resolution", () => {
"filesystem-expression",
],
['join("sub", "preload.js")', "sub/preload.js", "module-specifier"],
+ ["path.join(__dirname)", ".", "filesystem-expression"],
] as const)("preserves join behavior in %s", (expression, path, context) => {
assertPathProjection(expression, path, context);
});
@@ -182,7 +198,6 @@ describe("inert JavaScript path resolution", () => {
it.each([
"path.join()",
- "path.join(__dirname)",
'path.join("/app", runtimeFile)',
"path.join(...parts)",
])("preserves unresolved join syntax in %s", (expression) =>