fix(reference): retain unknown package scope for JavaScript loaders

This commit is contained in:
morluto
2026-10-24 02:39:40 +08:00
parent 97e1b93f13
commit c3e4c116d5
7 changed files with 53 additions and 17 deletions
+3 -1
View File
@@ -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
@@ -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<ReferenceSourceEntry, { status: "read"; kind: "file" }>;
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);
}
};
@@ -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",
}),
]);
});
+2 -1
View File
@@ -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: [] };
+11 -3
View File
@@ -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<ParsedJavaScriptSource, "program">,
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";
};
@@ -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,
@@ -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"),
]);