From c3e4c116d598135a2f65fe195f97245223e657bd Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:39:40 +0800 Subject: [PATCH] fix(reference): retain unknown package scope for JavaScript loaders --- docs/cli.md | 4 ++- .../ReferenceSourceImportEntries.ts | 15 +++++------ ...iptStaticAnalysisLoaderInvocations.test.ts | 25 +++++++++++++++++++ src/domain/referenceSourceImportParsing.ts | 3 ++- src/domain/referenceSourceRequireBindings.ts | 14 ++++++++--- .../referenceSourceModuleSemantics.test.ts | 6 +++-- .../filesystem/referenceSourceImport.test.ts | 3 ++- 7 files changed, 53 insertions(+), 17 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 1ffe293390..10451cdef3 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -468,7 +468,9 @@ base are proven: this includes the exact as `require.resolve?.("./dep.cjs")` use the same loader and mutation facts. The nearest inventoried package manifest supplies `.js` module scope; an explicit `type: "module"` supplies no CommonJS wrapper, while an unreadable, excluded or -malformed manifest leaves candidate loader resolution unknown. Other custom bases and search +malformed manifest, or package scope outside the selected inventory, leaves +candidate loader resolution unknown. A nearest manifest without a `type` field +uses the shared parser's JavaScript module-syntax facts. Other custom bases and search options remain partial with unknown resolution and the literal requested specifier. Overwritten resolver methods retain that uncertainty; they are not attributed to Node. `require.main` is a module object, so calling it does not diff --git a/src/application/ReferenceSourceImportEntries.ts b/src/application/ReferenceSourceImportEntries.ts index 576ac8cea4..1426a9f178 100644 --- a/src/application/ReferenceSourceImportEntries.ts +++ b/src/application/ReferenceSourceImportEntries.ts @@ -2,6 +2,7 @@ import { err, ok, type Result } from "../domain/result.js"; import { createHash } from "node:crypto"; import { posix } from "node:path"; +import type { ReferenceSourcePackageType } from "../domain/referenceSourceRequireBindings.js"; import { decodeJavaScriptModuleUrlPath } from "../domain/javascript/javascriptModuleSpecifier.js"; import { logicalPathEscapesRoot } from "../domain/artifactIdentity.js"; import { @@ -102,10 +103,9 @@ type PackageMain = | { readonly state: "none" } | { readonly state: "unknown" }; -type PackageType = "module" | "commonjs" | "unknown" | undefined; interface PackageMetadata { readonly main: PackageMain; - readonly type: PackageType; + readonly type: ReferenceSourcePackageType; } const UNKNOWN_PACKAGE: PackageMetadata = { main: { state: "unknown" }, @@ -158,7 +158,7 @@ const readPackageMetadata = ( ? Reflect.get(manifest, "main") : undefined; packages.set(directory, { - type: type === "module" || type === "commonjs" ? type : undefined, + type: type === "module" || type === "commonjs" ? type : "unspecified", main: typeof main === "string" && main !== "" ? { state: "declared", main } @@ -401,21 +401,18 @@ type ReadFile = Extract; const packageTypeForFile = ( path: string, context: SpecifierContext, -): "module" | "commonjs" | "unknown" | undefined => { +): ReferenceSourcePackageType => { let directory = posix.dirname(path); while (true) { const manifest = packageMainForDirectory(directory, context); if (manifest !== undefined) - return ( - context.packages.get(manifest.directory)?.type ?? - (manifest.main.state === "unknown" ? "unknown" : undefined) - ); + return context.packages.get(manifest.directory)?.type ?? "unknown"; if ( directory === "." || directory === "/" || posix.basename(directory) === "node_modules" ) - return undefined; + return "unknown"; directory = posix.dirname(directory); } }; diff --git a/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts b/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts index e65a02a6a7..95cf53f789 100644 --- a/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts +++ b/src/domain/javascript/javascriptStaticAnalysisLoaderInvocations.test.ts @@ -400,3 +400,28 @@ it.each([ ); }, ); + +it("retains unknown historical .js loader scope until a package manifest establishes it", () => { + const bytes = new TextEncoder().encode('require("./dep.cjs");'); + const relationships = ( + mode?: "module" | "commonjs" | "unspecified" | "unknown", + ) => + parseReferenceSourceImports("main.js", bytes, "JavaScript", mode) + .relationships; + expect(relationships()).toEqual([ + expect.objectContaining({ + to: "./dep.cjs", + resolution: "unknown", + parse_state: "partial", + }), + ]); + expect(relationships("module")).toEqual([]); + for (const mode of ["commonjs", "unspecified"] as const) + expect(relationships(mode)).toEqual([ + expect.objectContaining({ + to: "./dep.cjs", + resolution: "internal", + parse_state: "parsed", + }), + ]); +}); diff --git a/src/domain/referenceSourceImportParsing.ts b/src/domain/referenceSourceImportParsing.ts index 11423984af..3a3ad18413 100644 --- a/src/domain/referenceSourceImportParsing.ts +++ b/src/domain/referenceSourceImportParsing.ts @@ -13,6 +13,7 @@ import { import { referenceSourceRequireCalls, type ReferenceSourceRequireCall, + type ReferenceSourcePackageType, } from "./referenceSourceRequireBindings.js"; import { isRelativeJavaScriptModuleSpecifier } from "./javascript/javascriptModuleSpecifier.js"; import { @@ -261,7 +262,7 @@ export const parseReferenceSourceImports = ( path: string, bytes: Uint8Array, language: string | null, - packageType?: "module" | "commonjs" | "unknown", + packageType?: ReferenceSourcePackageType, ): ReferenceSourceImportParseResult => { if (language !== null && !PARSEABLE_REFERENCE_SOURCE_LANGUAGES.has(language)) return { relationships: [], parse_failures: [], limitations: [] }; diff --git a/src/domain/referenceSourceRequireBindings.ts b/src/domain/referenceSourceRequireBindings.ts index 171a85dcd4..08679f0875 100644 --- a/src/domain/referenceSourceRequireBindings.ts +++ b/src/domain/referenceSourceRequireBindings.ts @@ -23,6 +23,13 @@ import { type ParsedJavaScriptSource, } from "./javascript/javascriptSourceParser.js"; +/** Package scope observed inside the selected inventory; missing scope stays unknown. */ +export type ReferenceSourcePackageType = + | "module" + | "commonjs" + | "unspecified" + | "unknown"; + export type ReferenceSourceRequireResolutionBase = | "source-file" | { readonly kind: "source-relative-file"; readonly path: string } @@ -37,7 +44,7 @@ export interface ReferenceSourceRequireCall { export const referenceSourceRequireCalls = ( file: Pick, path: string, - packageType?: "module" | "commonjs" | "unknown", + packageType: ReferenceSourcePackageType = "unknown", ): ReadonlyMap< t.CallExpression | t.OptionalCallExpression, ReferenceSourceRequireCall @@ -103,7 +110,7 @@ export const referenceSourceRequireCalls = ( path: normalizedSourceRelativePath, } : fact === "commonjs-loader" && callFact.loader === "commonjs-loader" - ? path.endsWith(".js") && packageType === "unknown" + ? path.endsWith(".js") && wrapperMode === "unknown" ? "unknown" : "source-file" : "unknown"; @@ -177,7 +184,7 @@ type CommonJsWrapperMode = "present" | "absent" | "unknown"; const commonJsWrapperMode = ( path: string, program: t.Program, - packageType: "module" | "commonjs" | "unknown" | undefined, + packageType: ReferenceSourcePackageType, ): CommonJsWrapperMode => { if (path.endsWith(".cjs")) return "present"; if (isExplicitModuleSourcePath(path)) return "absent"; @@ -186,6 +193,7 @@ const commonJsWrapperMode = ( if (packageType === "commonjs") return "present"; if (packageType === "unknown") return "unknown"; if (javaScriptRuntimeContext(program) === "module") return "absent"; + return "present"; } return "unknown"; }; diff --git a/tests/boundary/cli/referenceSourceModuleSemantics.test.ts b/tests/boundary/cli/referenceSourceModuleSemantics.test.ts index 643c9bfe68..4970040de9 100644 --- a/tests/boundary/cli/referenceSourceModuleSemantics.test.ts +++ b/tests/boundary/cli/referenceSourceModuleSemantics.test.ts @@ -33,6 +33,8 @@ cliTest( "dep.ts": "export interface Marker { id: string }\n", "nested/package.json": '{"type":"commonjs","main":"main.js"}\n', "nested/main.js": 'require("../dep.cjs");\n', + "untyped/package.json": "{}\n", + "untyped/main.js": 'require("../dep.cjs");\n', "unknown/package.json": "{invalid", "unknown/main.js": 'require("../dep.cjs");\n', "url.mjs": @@ -45,7 +47,7 @@ cliTest( "oracle.mjs": 'import { fileURLToPath } from "node:url"; import { createRequire } from "node:module"; const require = createRequire(import.meta.url); console.log(JSON.stringify([fileURLToPath(import.meta.resolve("./dep%20name.mjs?v=1#first")), fileURLToPath(import.meta.resolve("./dep%2520name.mjs")), require.resolve("./dep%20name.mjs"), require.resolve("./dep?literal.cjs")]));\n', }; - for (const directory of ["nested", "unknown"]) + for (const directory of ["nested", "unknown", "untyped"]) await mkdir(join(root, directory)); await Promise.all( Object.entries(files).map(([path, source]) => @@ -85,7 +87,7 @@ cliTest( }, ]); expect(edges("shadowed.cjs")).toEqual([]); - for (const from of ["optional.cjs", "nested/main.js"]) + for (const from of ["optional.cjs", "nested/main.js", "untyped/main.js"]) expect(edges(from)).toEqual([ { from_path: from, diff --git a/tests/boundary/filesystem/referenceSourceImport.test.ts b/tests/boundary/filesystem/referenceSourceImport.test.ts index d9daae3060..f9f31b3d7b 100644 --- a/tests/boundary/filesystem/referenceSourceImport.test.ts +++ b/tests/boundary/filesystem/referenceSourceImport.test.ts @@ -798,8 +798,9 @@ describe("reference source rooted module specifiers", () => { await Promise.all([ writeFile( join(root, "main.js"), - 'import ".hidden.js"; import "..pkg.js"; import "./.hidden.js"; require(".hidden.js");\n', + 'import ".hidden.js"; import "..pkg.js"; import "./.hidden.js";\n', ), + writeFile(join(root, "common.cjs"), 'require(".hidden.js");\n'), writeFile(join(root, ".hidden.js"), "export {};\n"), writeFile(join(root, "..pkg.js"), "export {};\n"), ]);