From 2b4abbf1ccb6dd6ad1248cf48e381df0842542c1 Mon Sep 17 00:00:00 2001 From: AC Date: Sat, 10 Oct 2026 00:43:33 +0800 Subject: [PATCH] fix(javascript): distinguish prefix-only builtins from packages --- .../JavaScriptArtifactPathResolution.ts | 11 +--- .../JavaScriptModuleSpecifierPaths.test.ts | 66 +++++++++++++++++++ 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/application/javascript/JavaScriptArtifactPathResolution.ts b/src/application/javascript/JavaScriptArtifactPathResolution.ts index bb93419d75..90c79b20a0 100644 --- a/src/application/javascript/JavaScriptArtifactPathResolution.ts +++ b/src/application/javascript/JavaScriptArtifactPathResolution.ts @@ -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.", ]); diff --git a/src/application/javascript/JavaScriptModuleSpecifierPaths.test.ts b/src/application/javascript/JavaScriptModuleSpecifierPaths.test.ts index 9377730001..a31b2a2cae 100644 --- a/src/application/javascript/JavaScriptModuleSpecifierPaths.test.ts +++ b/src/application/javascript/JavaScriptModuleSpecifierPaths.test.ts @@ -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)",