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:
1 parent
ded199abcd
commit
c923fb0dc9
18 files changed
+503
-100
No files matched your search
@@ -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 = (
|
||||
|
||||
Reference in new issue
Block a user