fix(javascript): retain compiler-dependent module resolution facts

Compare import and require outcomes for static TypeScript references when compiler options are unknown. Preserve branch facts through worker IR and both graph projections; retain declaration fallbacks as candidates without changing explicitly selected paths.

Validation: 210 focused resolver and semantic tests; 21 production-boundary and source-extension tests. A bounded 2,000-reference probe adds about 16–17 ms wall time for dual resolution; full task token costs and real compiled application behavior remain unmeasured.
This commit is contained in:
morluto
2026-10-13 15:58:25 +08:00
parent ded199abcd
commit c923fb0dc9
18 changed files with 503 additions and 100 deletions
@@ -19,8 +19,13 @@ import {
classifyPackageImportTargetString,
lookupExactPackageImport,
} from "../../domain/javascript/javascriptPackageImportAliases.js";
import { typescriptEmittedSourceCandidates } from "../TypeScriptEmittedSourceCandidates.js";
import {
typescriptEmittedSourceCandidates,
type TypeScriptEmittedSourceCandidate,
} from "../TypeScriptEmittedSourceCandidates.js";
import { isRelativeJavaScriptModuleSpecifier } from "../../domain/javascript/javascriptModuleSpecifier.js";
import type { JavaScriptModuleResolutionMode } from "../../domain/javascript/javascriptModuleResolutionMode.js";
import type { JsonValue } from "../../domain/jsonValue.js";
type ArtifactPathResolutionContext =
| "package-entrypoint"
@@ -39,6 +44,16 @@ interface ArtifactPathResolutionBase {
readonly declared_path: string;
readonly resolution_context: ArtifactPathResolutionContext;
readonly limitations: readonly string[];
readonly conditional_resolutions?: readonly ConditionalModuleResolution[];
readonly candidate_path?: string;
}
export interface ConditionalModuleResolution {
readonly mode: "import" | "require";
readonly resolution_status: UnresolvedArtifactPathStatus | "resolved";
readonly resolved_path: string | null;
readonly candidate_path?: string;
readonly limitations: readonly string[];
}
/** Explicit outcome of artifact-confined path resolution. */
@@ -62,26 +77,13 @@ export interface ResolveArtifactPathInput {
readonly files: ReadonlyMap<string, JavaScriptArtifactFile>;
readonly htmlBaseHref?: string | null;
/** Import/require semantics; runtime resource paths omit this provenance. */
readonly moduleKind?: "import" | "require";
readonly moduleKind?: JavaScriptModuleResolutionMode;
}
/** Loader selected by a source-level JavaScript module reference. */
export const moduleKindForJavaScriptReference = (
sourcePath: string,
referenceKind: "static-import" | "dynamic-import" | "require",
resolutionMode?: "import" | "require",
): "import" | "require" =>
resolutionMode ??
(referenceKind === "require" ||
(referenceKind === "static-import" &&
posix.extname(sourcePath).toLowerCase() === ".cts")
? "require"
: "import");
/** Preserve an explicitly retained loader mode on graph relationships. */
export const moduleResolutionModeProperties = (
mode: "import" | "require" | undefined,
): Readonly<Record<string, "import" | "require">> =>
mode: JavaScriptModuleResolutionMode | undefined,
): Readonly<Record<string, JavaScriptModuleResolutionMode>> =>
mode === undefined ? {} : { resolution_mode: mode };
const EXTENSIONS = [
@@ -110,43 +112,116 @@ interface PackageFileTarget {
readonly url: URL;
}
type CandidateResolution =
| {
readonly resolvedPath: string;
readonly status: "resolved";
readonly limitations: readonly string[];
}
type CandidateResolution = {
readonly limitations: readonly string[];
readonly candidatePath?: string;
readonly conditionalResolutions?: readonly ConditionalModuleResolution[];
} & (
| { readonly resolvedPath: string; readonly status: "resolved" }
| {
readonly resolvedPath: null;
readonly status: UnresolvedArtifactPathStatus;
readonly limitations: readonly string[];
/** A truthy package main failed, so Node must not try ancestor packages. */
readonly stopsAncestorLookup?: true;
};
}
);
/** Resolve one declaration while preserving its original input identity. */
export const resolveArtifactPathByContext = (
input: ResolveArtifactPathInput,
): ArtifactPathResolution => {
const resolution = resolvePath(input);
const facts = {
declared_path: input.declaredPath,
resolution_context: input.context,
limitations: resolution.limitations,
...(resolution.candidatePath === undefined
? {}
: { candidate_path: resolution.candidatePath }),
...(resolution.conditionalResolutions === undefined
? {}
: { conditional_resolutions: resolution.conditionalResolutions }),
};
return resolution.status === "resolved"
? {
declared_path: input.declaredPath,
resolution_context: input.context,
...facts,
resolved_path: resolution.resolvedPath,
resolution_status: resolution.status,
limitations: resolution.limitations,
}
: {
declared_path: input.declaredPath,
resolution_context: input.context,
resolved_path: null,
resolution_status: resolution.status,
limitations: resolution.limitations,
};
: { ...facts, resolved_path: null, resolution_status: resolution.status };
};
/** Project retained conditional identities without flattening them into display text. */
export const artifactPathResolutionProperties = (
resolution: ArtifactPathResolution | undefined,
): Readonly<Record<string, JsonValue>> => ({
...(resolution?.candidate_path === undefined
? {}
: { candidate_path: resolution.candidate_path }),
...(resolution?.conditional_resolutions === undefined
? {}
: {
conditional_resolutions: resolution.conditional_resolutions.map(
(branch) => ({
mode: branch.mode,
resolution_status: branch.resolution_status,
resolved_path: branch.resolved_path,
limitations: [...branch.limitations],
...(branch.candidate_path === undefined
? {}
: { candidate_path: branch.candidate_path }),
}),
),
}),
});
const conditionalResolution = (
mode: "import" | "require",
result: CandidateResolution,
): ConditionalModuleResolution => ({
mode,
resolution_status: result.status,
resolved_path: result.resolvedPath,
limitations: result.limitations,
...(result.candidatePath === undefined
? {}
: { candidate_path: result.candidatePath }),
});
const resolvePath = (input: ResolveArtifactPathInput): CandidateResolution => {
if (input.moduleKind === "compiler-dependent") {
const imported = resolvePath({ ...input, moduleKind: "import" });
const required = resolvePath({ ...input, moduleKind: "require" });
const sameOutcome =
imported.status === required.status &&
imported.resolvedPath === required.resolvedPath &&
imported.candidatePath === required.candidatePath;
const limitation =
"Static TypeScript loader mode is compiler-dependent without retained compiler options. " +
(sameOutcome
? "Node import and require resolution agree on this outcome."
: "Node import and require resolution disagree; neither branch is an established target.");
return sameOutcome
? {
...imported,
limitations: [
...new Set([
...imported.limitations,
...required.limitations,
limitation,
]),
],
}
: {
resolvedPath: null,
status: "unavailable",
limitations: [limitation],
conditionalResolutions: [
conditionalResolution("import", imported),
conditionalResolution("require", required),
],
};
}
// Package import keys are literal lookup identities, not artifact paths.
// The selected target is admitted separately by resolveExportTarget.
if (
@@ -176,12 +251,47 @@ const resolvePath = (input: ResolveArtifactPathInput): CandidateResolution => {
: [];
if (isModuleUrlPath && input.moduleKind === "import")
return (
resolveFileCandidates(input, [confined, ...emittedSourceCandidates]) ??
notFoundCandidate()
resolveRuntimeSourceCandidates(
input,
confined,
emittedSourceCandidates,
) ?? notFoundCandidate()
);
return resolveCandidate(input, confined, new Set(), emittedSourceCandidates);
};
const resolveRuntimeSourceCandidates = (
input: ResolveArtifactPathInput,
exactPath: string,
sourceCandidates: readonly TypeScriptEmittedSourceCandidate[],
): CandidateResolution | null => {
const source = input.files.get(input.sourcePath);
const exactCandidate: TypeScriptEmittedSourceCandidate = {
path: exactPath,
kind: "implementation",
};
for (const candidate of [exactCandidate, ...sourceCandidates]) {
const target = input.files.get(candidate.path);
if (
target === undefined ||
(source !== undefined &&
target.container_sha256 !== source.container_sha256)
)
continue;
return candidate.kind === "declaration"
? {
resolvedPath: null,
status: "unavailable",
candidatePath: candidate.path,
limitations: [
"Only a TypeScript declaration fallback was found; it does not establish the requested runtime module.",
],
}
: { resolvedPath: candidate.path, status: "resolved", limitations: [] };
}
return null;
};
const rejectDeclaration = (declared: string): CandidateResolution | null => {
if (declared.length === 0)
return unresolvedCandidate("rejected", ["The declared path is empty."]);
@@ -849,12 +959,10 @@ const resolveCandidate = (
input: ResolveArtifactPathInput,
candidate: string,
packageChain: ReadonlySet<string>,
emittedSourceCandidates: readonly string[] = [],
emittedSourceCandidates: readonly TypeScriptEmittedSourceCandidate[] = [],
): CandidateResolution =>
resolveFileCandidates(input, [
...fileCandidates(candidate, input.moduleKind),
...emittedSourceCandidates,
]) ??
resolveFileCandidates(input, fileCandidates(candidate, input.moduleKind)) ??
resolveRuntimeSourceCandidates(input, candidate, emittedSourceCandidates) ??
resolveDirectory(
input,
candidate,
@@ -940,11 +1048,26 @@ const resolveFileCandidates = (
(source === undefined ||
target.container_sha256 === source.container_sha256)
)
return { resolvedPath: path, status: "resolved", limitations: [] };
return isRuntimeDeclarationPath(input, path)
? {
resolvedPath: null,
status: "unavailable",
candidatePath: path,
limitations: [
`The TypeScript declaration ${path} is compile-time evidence and cannot identify a runtime module implementation.`,
],
}
: { resolvedPath: path, status: "resolved", limitations: [] };
}
return null;
};
const isRuntimeDeclarationPath = (
input: ResolveArtifactPathInput,
path: string,
): boolean =>
input.moduleKind !== undefined && /\.d\.(?:ts|mts|cts)$/iu.test(path);
const NODE_MODULE_EXTENSIONS = [".js", ".json", ".node"] as const;
const fileCandidates = (