fix(javascript): distinguish prefix-only builtins from packages

This commit is contained in:
AC authored and morluto committed 2026-10-10 01:45:00 +08:00
1 parent 9367dc13bd
commit 2b4abbf1cc
2 files changed
+68 -9

No files matched your search

@@ -1,4 +1,4 @@
import { builtinModules } from "node:module";
import { isBuiltin } from "node:module";
import { posix } from "node:path";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
@@ -63,9 +63,6 @@ const EXTENSIONS = [
".html",
".node",
];
const NODE_BUILTINS = new Set(
builtinModules.map((name) => name.replace(/^node:/u, "")),
);
type CandidateResolution =
| {
@@ -194,11 +191,7 @@ const bareModuleCandidate = (
declared: string,
): string | ArtifactPathResolution => {
const packageName = barePackageName(declared);
if (
packageName === null ||
NODE_BUILTINS.has(packageName) ||
declared.startsWith("#")
)
if (packageName === null || isBuiltin(declared) || declared.startsWith("#"))
return unresolvedOutcome(input, "external", [
"The bare specifier is a Node builtin, package import map, or invalid package name.",
]);
@@ -27,6 +27,72 @@ const filesFor = (paths: readonly string[]) =>
]),
);
describe("prefix-only Node builtins", () => {
it.each(["import", "require"] as const)(
"resolves ordinary packages named test, sqlite and sea for %s",
async (moduleKind) => {
const root = await createTestTempDirectory("rea-prefix-only-builtins-");
const names = ["test", "sqlite", "sea"];
for (const name of [...names, "fs"]) {
const directory = join(root, "node_modules", name);
await mkdir(directory, { recursive: true });
await writeFile(
join(directory, "index.js"),
`module.exports = ${JSON.stringify(name)};`,
);
}
const main = join(
root,
moduleKind === "import" ? "main.mjs" : "main.cjs",
);
await writeFile(
main,
names
.map((name, index) =>
moduleKind === "import"
? `import value${index} from ${JSON.stringify(name)}; console.log(value${index});`
: `console.log(require(${JSON.stringify(name)}));`,
)
.join("\n") +
(moduleKind === "import"
? '\nimport "fs"; import "node:test";'
: '\nrequire("fs"); require("node:test");'),
);
const native = await promisify(execFile)(process.execPath, [main], {
timeout: 5_000,
});
expect(native.stdout.trim().split("\n").slice(0, 3)).toEqual(names);
const result = await analyzeJavaScriptApplication({
input_path: root,
format: "directory",
});
if (!result.ok) throw result.error;
const { graph } = javascriptApplicationAnalysisResultSchema.parse(
result.value.normalized_result,
);
const imports = graph.edges.filter((edge) => edge.relation === "imports");
for (const name of names) {
const matches = imports.filter(
(edge) => edge.properties.specifier === name,
);
expect(matches.length).toBeGreaterThan(0);
for (const edge of matches)
expect(edge.properties.resolved_path).toBe(
`node_modules/${name}/index.js`,
);
}
for (const name of ["fs", "node:test"]) {
const matches = imports.filter(
(edge) => edge.properties.specifier === name,
);
expect(matches.length).toBeGreaterThan(0);
for (const edge of matches)
expect(edge.properties.resolved_path).toBeNull();
}
},
);
});
describe("module specifier punctuation", () => {
it.each(["#", "?"])(
"keeps CommonJS punctuation and strips URL suffixes for ESM (%s)",