fix: retain proven createRequire URL bases

Fixes #6612

Fixes #6678
This commit is contained in:
morluto
2026-10-23 21:10:08 +08:00
parent 857a75baac
commit 4bad48d873
7 changed files with 219 additions and 72 deletions
+6 -5
View File
@@ -411,11 +411,12 @@ retain their targets; computed imports remain unknown. TypeScript import-equals
dependencies remain `requires` relationships whether or not they are exported.
CommonJS `require` relationships retain the requested specifier. REA resolves
them against the source file only when Node's default source-relative loader is
proven; non-default `createRequire` bases and unsupported
`require.resolve` search options remain partial with unknown resolution. An
excluded `package.json` also leaves package-directory selection unknown rather
than treating the manifest as absent and applying an `index` fallback.
them only when the loader base is proven: this includes the exact
`createRequire(new URL("./file.cjs", import.meta.url))` form and
`require.resolve` with `{ paths: [__dirname] }`. Other custom bases and search
options remain partial with unknown resolution. An excluded `package.json`
also leaves package-directory selection unknown rather than treating the
manifest as absent and applying an `index` fallback.
Historical-source import requires safe no-follow file opens on Linux or
macOS. Native Windows returns `unsupported_host`; use Linux REA inside WSL
@@ -351,7 +351,8 @@ const appendFile = (
if (parsed === undefined) return;
output.limitations.push(...parsed.limitations);
for (const relationship of parsed.relationships) {
const { resolutionStrategy, ...graphRelationship } = relationship;
const { resolutionStrategy, resolutionBasePath, ...graphRelationship } =
relationship;
if (graphRelationship.resolution !== "internal") {
output.relationships.push(graphRelationship);
continue;
@@ -363,7 +364,7 @@ const appendFile = (
? "source-require"
: "source-import";
const resolved = resolveInternalSpecifier(
graphRelationship.from_path,
resolutionBasePath ?? graphRelationship.from_path,
graphRelationship.to,
context,
mode,
@@ -26,7 +26,10 @@ import {
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import { semanticPropertyPathKeyMatches } from "./javascriptSemanticPropertyPaths.js";
import { stringValue } from "./javascriptStaticAnalysisHelpers.js";
import {
staticSourceRelativeUrlPath,
stringValue,
} from "./javascriptStaticAnalysisHelpers.js";
import {
semanticEffectTiming,
semanticBindingAtRead,
@@ -46,7 +49,10 @@ import {
type SemanticAliasSelectionPath,
} from "./javascriptSemanticAliasSelections.js";
import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js";
import { isUnmodifiedImportMetaUrl } from "./javascriptSemanticAmbientWrites.js";
import {
isUnmodifiedGlobalReference,
isUnmodifiedImportMetaUrl,
} from "./javascriptSemanticAmbientWrites.js";
import { semanticReferenceSourcesInactive } from "./javascriptSemanticReferenceSources.js";
export interface JavaScriptRequireOrigin {
@@ -266,7 +272,12 @@ const createRequireFactoryCallAt = (
initializer: t.Node | undefined,
state: JavaScriptSemanticAnalysisState,
factoryBindingIds: ReadonlySet<string> = new Set(),
): Pick<JavaScriptRequireCallFact, "loader" | "sourceRelative"> | undefined =>
):
| Pick<
JavaScriptRequireCallFact,
"loader" | "sourceRelative" | "sourceRelativePath"
>
| undefined =>
!t.isCallExpression(initializer)
? undefined
: selectedRequireSourcesAt(
@@ -277,6 +288,32 @@ const createRequireFactoryCallAt = (
"factory",
);
const sourceRelativeCreateRequirePath = (
value: t.Node | undefined,
state: JavaScriptSemanticAnalysisState,
): string | undefined => {
if (value === undefined) return undefined;
if (isUnmodifiedImportMetaUrl(value, state)) return ".";
if (!t.isNewExpression(value) || value.arguments.length !== 2)
return undefined;
const callee = unwrapJavaScriptExpression(value.callee).node;
if (
!t.isIdentifier(callee, { name: "URL" }) ||
!isUnmodifiedGlobalReference(callee, state, "URL") ||
!isUnmodifiedImportMetaUrl(
t.isNode(value.arguments[1]) ? value.arguments[1] : undefined,
state,
)
)
return undefined;
const urlPath = staticSourceRelativeUrlPath(value);
return urlPath === undefined ||
urlPath.directory ||
urlPath.path.startsWith("/")
? undefined
: urlPath.path;
};
/** Eligibility for observable loader slots; an unknown factory is not invocation proof. */
export const hasSemanticCreateRequireFactoryOrigin = (
initializer: t.Node | undefined,
@@ -318,7 +355,12 @@ const selectedRequireSourcesAt = (
state: JavaScriptSemanticAnalysisState,
factoryBindingIds: ReadonlySet<string>,
target: "loader" | "factory" = "loader",
): Pick<JavaScriptRequireCallFact, "loader" | "sourceRelative"> | undefined => {
):
| Pick<
JavaScriptRequireCallFact,
"loader" | "sourceRelative" | "sourceRelativePath"
>
| undefined => {
const pending: ModuleObjectSource[] = [{ node: root, path: [] }];
const seen = new WeakMap<t.Node, Set<string>>();
let count = 0;
@@ -329,6 +371,7 @@ const selectedRequireSourcesAt = (
let local = false;
let nativeMemberAlternative = false;
let sourceRelative = true;
let sourceRelativePath: string | undefined;
while (pending.length > 0) {
const current = pending.pop()!;
const node = unwrapJavaScriptExpression(current.node).node;
@@ -514,10 +557,17 @@ const selectedRequireSourcesAt = (
selectedLoader !== "create-require");
selectedLoader ??= "create-require";
unknown ||= state.conditionallySkippedExpressions.has(node);
sourceRelative &&= isUnmodifiedImportMetaUrl(
const factoryPath = sourceRelativeCreateRequirePath(
node.arguments[0],
state,
);
if (factoryPath === undefined) sourceRelative = false;
else if (sourceRelativePath === undefined)
sourceRelativePath = factoryPath;
else if (sourceRelativePath !== factoryPath) {
sourceRelative = false;
sourceRelativePath = undefined;
}
}
} else if (factory?.loader === "local") local = true;
else unknown = true;
@@ -543,6 +593,9 @@ const selectedRequireSourcesAt = (
? (selectedLoader ?? "unknown")
: "local",
sourceRelative: found && sourceRelative && !unknown,
...(found && !unknown && sourceRelativePath !== undefined
? { sourceRelativePath }
: {}),
}
: undefined;
};
@@ -582,6 +635,8 @@ export interface JavaScriptRequireCallFact {
| readonly t.CallExpression["arguments"][number][]
| undefined;
readonly sourceRelative: boolean;
/** A createRequire filename relative to the current source directory, when proven. */
readonly sourceRelativePath?: string;
}
/** Classify the selected loader and normalize invocation arguments once. */
@@ -610,6 +665,9 @@ export const semanticRequireCallAt = (
loader: selected?.loader ?? loader,
arguments: node.arguments,
sourceRelative: selected?.sourceRelative ?? loader !== "create-require",
...(selected?.sourceRelativePath === undefined
? {}
: { sourceRelativePath: selected.sourceRelativePath }),
};
}
const interop = loaderInvocationInterop(state);
@@ -637,7 +695,13 @@ export const semanticRequireCallAt = (
];
return interop.intrinsicPath(callee);
});
if (invocation?.path[0] === "require-loader")
if (invocation?.path[0] === "require-loader") {
const selected = selectedRequireSourcesAt(
node.callee,
node,
state,
factoryBindingIds,
);
return {
loader:
invocation.path[1] === "local"
@@ -648,7 +712,11 @@ export const semanticRequireCallAt = (
arguments: invocation.arguments,
sourceRelative:
invocation.kind === "known" && invocation.path[2] === "relative",
...(selected?.sourceRelativePath === undefined
? {}
: { sourceRelativePath: selected.sourceRelativePath }),
};
}
}
const factory = semanticFactoryRequireLoaderAt(
node,
@@ -255,7 +255,20 @@ const staticCallPath = (
return parts.length === 0 ? undefined : posix.join(...parts);
};
const staticFileUrlPath = (
/** Normalized path facts from a source-relative file URL expression. */
export interface JavaScriptStaticSourceUrlPath {
readonly path: string;
readonly directory: boolean;
}
/** Read a literal `new URL(relative, import.meta.url)` shape without evaluation. */
export const staticSourceRelativeUrlPath = (
node: t.Node,
): JavaScriptStaticSourceUrlPath | undefined =>
isSourceFileUrl(node) ? sourceRelativeFileUrlPath(node) : undefined;
/** Convert an exact static file URL expression to a POSIX path. */
export const staticFileUrlPath = (
node: t.CallExpression | t.NewExpression,
nodePathResolve?: JavaScriptStaticPathIntrinsicQuery,
): string | undefined => {
@@ -263,7 +276,8 @@ const staticFileUrlPath = (
const rawArgument = argumentNode(node.arguments[0]);
if (rawArgument === undefined) return undefined;
const argument = unwrapJavaScriptExpression(rawArgument).node;
if (isSourceFileUrl(argument)) return sourceRelativeFileUrlPath(argument);
if (isSourceFileUrl(argument))
return sourceRelativeFileUrlPath(argument)?.path;
const value = staticPath(argument, nodePathResolve);
const rawPath = value?.split(/[?#]/u, 1)[0] ?? "";
if (
@@ -291,7 +305,7 @@ const staticFileUrlPath = (
/** Project a known source-relative URL through the actual file URL decoder. */
const sourceRelativeFileUrlPath = (
node: t.CallExpression | t.NewExpression,
): string | undefined => {
): JavaScriptStaticSourceUrlPath | undefined => {
const value = stringValue(node.arguments[0]);
if (
value === undefined ||
@@ -310,14 +324,19 @@ const sourceRelativeFileUrlPath = (
try {
const resolved = new URL(value, `file://${directory}entry.js`);
const path = fileURLToPath(resolved, { windows: false });
if (value.startsWith("/")) return path;
if (value.startsWith("/"))
return { path, directory: resolved.pathname.endsWith("/") };
const relative = posix.relative(directory, path) || ".";
// Keep the established explicit source-directory observation spelling.
return value.startsWith("./") &&
relative !== "." &&
!relative.startsWith("../")
? `./${relative}`
: relative;
return {
path:
value.startsWith("./") &&
relative !== "." &&
!relative.startsWith("../")
? `./${relative}`
: relative,
directory: resolved.pathname.endsWith("/"),
};
} catch (cause: unknown) {
// Malformed escapes and non-file URL forms remain unknown.
void cause;
+17 -1
View File
@@ -48,6 +48,8 @@ interface ReferenceSourceImportRelationship {
readonly parse_state: ReferenceSourceImportParseState;
/** Distinguishes call-form CommonJS require from TS import-equals `requires`. */
readonly resolutionStrategy?: "runtime-require";
/** Internal lookup origin for a proven createRequire URL base. */
readonly resolutionBasePath?: string;
}
interface ReferenceSourceParseFailure {
@@ -126,6 +128,7 @@ const appendRelationship = (
readonly parseState: ReferenceSourceImportParseState;
readonly resolution?: ReferenceSourceImportResolution;
readonly resolutionStrategy?: "runtime-require";
readonly resolutionBasePath?: string;
},
): void => {
if (input.to === undefined) return;
@@ -144,6 +147,9 @@ const appendRelationship = (
...(input.resolutionStrategy === undefined
? {}
: { resolutionStrategy: input.resolutionStrategy }),
...(input.resolutionBasePath === undefined
? {}
: { resolutionBasePath: input.resolutionBasePath }),
});
};
@@ -236,7 +242,12 @@ const extractRequireAndDynamicImports = (
from_path: string,
relationships: ReferenceSourceImportRelationship[],
limitations: string[],
requireCallBases: ReadonlyMap<CallExpression, "source-file" | "unknown">,
requireCallBases: ReadonlyMap<
CallExpression,
| "source-file"
| { readonly kind: "source-relative-file"; readonly path: string }
| "unknown"
>,
): void => {
// Single traversal owner: iterative, VISITOR_KEYS-gated, no recursion over
// loc/comment objects. Survives deeply nested generated member chains that
@@ -280,6 +291,11 @@ const extractRequireAndDynamicImports = (
...(uncertainResolution
? {}
: { resolutionStrategy: "runtime-require" as const }),
...(typeof resolutionBase === "object"
? {
resolutionBasePath: resolutionBase.path,
}
: {}),
});
}
continue;
+59 -43
View File
@@ -1,4 +1,5 @@
import * as t from "@babel/types";
import { posix } from "node:path";
import { semanticStaticPropertyKey } from "./javascript/javascriptAstValues.js";
import { isUnmodifiedGlobalReference } from "./javascript/javascriptSemanticAmbientWrites.js";
@@ -20,7 +21,10 @@ import {
type ParsedJavaScriptSource,
} from "./javascript/javascriptSourceParser.js";
export type ReferenceSourceRequireResolutionBase = "source-file" | "unknown";
export type ReferenceSourceRequireResolutionBase =
| "source-file"
| { readonly kind: "source-relative-file"; readonly path: string }
| "unknown";
/** Identify exact calls through a source-owned Node loader and their resolver base. */
export const referenceSourceRequireCallBases = (
@@ -45,7 +49,7 @@ export const referenceSourceRequireCallBases = (
traverseJavaScriptAst(file.program, {
enter: (node) => {
if (!t.isCallExpression(node)) return;
const identifier = requireCalleeIdentifier(node.callee);
const identifier = callReceiverIdentifier(node.callee);
if (identifier === undefined) return;
let fact = semanticRequireLoaderAt(node, index.bindings, factoryBindings);
if (fact === "create-require") {
@@ -64,20 +68,40 @@ export const referenceSourceRequireCallBases = (
index.bindings,
factoryBindings,
);
const base =
const sourceRelativePath = callFact?.sourceRelativePath;
const normalizedSourceRelativePath =
sourceRelativePath === undefined
? undefined
: posix.normalize(
posix.join(posix.dirname(path), sourceRelativePath),
);
const base: ReferenceSourceRequireResolutionBase =
fact === "create-require"
? callFact?.sourceRelative === true
? "source-file"
: "unknown"
? sourceRelativePath === undefined
? callFact?.sourceRelative === true
? "source-file"
: "unknown"
: normalizedSourceRelativePath === undefined ||
normalizedSourceRelativePath === ".." ||
normalizedSourceRelativePath.startsWith("../")
? "unknown"
: normalizedSourceRelativePath === path
? "source-file"
: {
kind: "source-relative-file",
path: normalizedSourceRelativePath,
}
: "source-file";
const searchBase = requireResolveSearchBase(
node,
index.bindings,
base,
wrapperMode === "present",
);
candidates.push({
call: node,
identifier,
resolutionBase:
base === "source-file" &&
isDefaultRequireResolveSearch(node, index.bindings)
? "source-file"
: "unknown",
resolutionBase: searchBase,
});
},
});
@@ -95,38 +119,39 @@ export const referenceSourceRequireCallBases = (
};
/** `require.resolve` options replace the default search paths unless exact current-directory paths are supplied. */
const isDefaultRequireResolveSearch = (
const requireResolveSearchBase = (
call: t.CallExpression,
bindings: JavaScriptSemanticAnalysisState,
): boolean => {
if (!isRequireResolveCall(call) || call.arguments.length < 2) return true;
if (call.arguments.length !== 2) return false;
loaderBase: ReferenceSourceRequireResolutionBase,
hasCommonJsWrapper: boolean,
): ReferenceSourceRequireResolutionBase => {
if (!isRequireResolveCall(call) || call.arguments.length < 2)
return loaderBase;
if (call.arguments.length !== 2) return "unknown";
const options = t.isExpression(call.arguments[1])
? call.arguments[1]
: undefined;
if (options === undefined) return false;
if (!t.isObjectExpression(options)) return false;
if (options.properties.length === 0) return true;
if (options.properties.length !== 1) return false;
if (options === undefined || !t.isObjectExpression(options)) return "unknown";
if (options.properties.length === 0) return loaderBase;
if (options.properties.length !== 1) return "unknown";
const property = options.properties[0];
if (!t.isObjectProperty(property)) return false;
if (!t.isObjectProperty(property)) return "unknown";
if (semanticStaticPropertyKey(property.key, property.computed) !== "paths")
return false;
return "unknown";
const value = t.isExpression(property.value) ? property.value : undefined;
if (!t.isArrayExpression(value) || value.elements.length !== 1) return false;
if (!t.isArrayExpression(value) || value.elements.length !== 1)
return "unknown";
const directory = value.elements[0];
return (
return hasCommonJsWrapper &&
t.isIdentifier(directory, { name: "__dirname" }) &&
isUnmodifiedGlobalReference(directory, bindings, "__dirname")
);
? "source-file"
: "unknown";
};
const isRequireResolveCall = (call: t.CallExpression): boolean => {
const callee = call.callee;
if (
!t.isMemberExpression(callee) ||
!t.isIdentifier(callee.object, { name: "require" })
)
if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee))
return false;
return (
semanticStaticPropertyKey(callee.property, callee.computed) === "resolve"
@@ -164,23 +189,14 @@ const hasPriorMemberMutation = (
);
};
const requireCalleeIdentifier = (
const callReceiverIdentifier = (
callee: t.Node | null | undefined,
): t.Identifier | undefined => {
if (t.isIdentifier(callee, { name: "require" })) return callee;
if (t.isIdentifier(callee)) return callee;
if (
!t.isMemberExpression(callee) ||
!t.isIdentifier(callee.object, { name: "require" }) ||
((!t.isIdentifier(callee.property) || callee.computed) &&
!t.isStringLiteral(callee.property))
(t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
t.isIdentifier(callee.object)
)
return undefined;
const propertyName = t.isIdentifier(callee.property)
? callee.property.name
: t.isStringLiteral(callee.property)
? callee.property.value
: undefined;
return propertyName === "resolve" || propertyName === "main"
? callee.object
: undefined;
return callee.object;
return undefined;
};
@@ -8,7 +8,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest(
"preserves unknown CommonJS resolution when Node is given another base",
"resolves a proven createRequire URL base and preserves unsupported alternatives",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-reference-require-base-");
const config = await createTestTempDirectory(
@@ -24,10 +24,12 @@ cliTest(
'custom("./dep.cjs");',
"const local = createRequire(import.meta.url);",
'local("./dep.cjs");',
'local.resolve("./alias-options.cjs", { paths: [] });',
].join("\n"),
),
writeFile(join(root, "dep.cjs"), "module.exports = 'root';\n"),
writeFile(join(root, "sub", "dep.cjs"), "module.exports = 'sub';\n"),
writeFile(join(root, "sub", "entry.cjs"), "module.exports = {};\n"),
]);
const result = await cli.run({
@@ -46,10 +48,10 @@ cliTest(
expect.arrayContaining([
{
from_path: "main.mjs",
to: "./dep.cjs",
to: "sub/dep.cjs",
kind: "requires",
resolution: "unknown",
parse_state: "partial",
resolution: "internal",
parse_state: "parsed",
},
{
from_path: "main.mjs",
@@ -58,6 +60,13 @@ cliTest(
resolution: "internal",
parse_state: "parsed",
},
{
from_path: "main.mjs",
to: "./alias-options.cjs",
kind: "requires",
resolution: "unknown",
parse_state: "partial",
},
]),
);
},
@@ -78,13 +87,16 @@ cliTest(
join(root, "main.cjs"),
[
'require.resolve("./dep.cjs");',
'require.resolve("./dep.cjs", { paths: [__dirname] });',
'require.resolve("./same-path.cjs", { paths: [__dirname] });',
'require.resolve("./dep.cjs", { paths: [__dirname + "/sub"] });',
'require.resolve("./dep.cjs", { paths: [] });',
'function shadow(__dirname) { require.resolve("./shadow.cjs", { paths: [__dirname] }); }',
].join("\n"),
),
writeFile(join(root, "dep.cjs"), "module.exports = 'root';\n"),
writeFile(join(root, "same-path.cjs"), "module.exports = 'same path';\n"),
writeFile(join(root, "sub", "dep.cjs"), "module.exports = 'sub';\n"),
writeFile(join(root, "shadow.cjs"), "module.exports = 'shadow';\n"),
]);
const result = await cli.run({
@@ -118,8 +130,22 @@ cliTest(
resolution: "unknown",
parse_state: "partial",
},
{
from_path: "main.cjs",
to: "same-path.cjs",
kind: "requires",
resolution: "internal",
parse_state: "parsed",
},
{
from_path: "main.cjs",
to: "./shadow.cjs",
kind: "requires",
resolution: "unknown",
parse_state: "partial",
},
]),
);
expect(relationships).toHaveLength(2);
expect(relationships).toHaveLength(4);
},
);