2026-10-10 00:43:33 +08:00
import { isBuiltin } from "node:module" ;
2026-07-17 21:30:24 +08:00
import { posix } from "node:path" ;
2026-10-10 17:14:47 +08:00
import {
normalizeJoinedLogicalPath ,
logicalPathEscapesRoot ,
} from "../../domain/artifactIdentity.js" ;
2026-10-14 08:46:50 +08:00
import type {
JavaScriptArtifactFile ,
JavaScriptUnanalyzedArtifactLookup ,
} from "../../domain/javascript/javascriptArtifactFiles.js" ;
2026-10-07 11:28:28 +08:00
import {
admitsCanonicalPathSyntax ,
hasScheme ,
2026-10-09 13:52:55 +00:00
htmlUrlText ,
2026-10-07 11:28:28 +08:00
looksExternal ,
2026-10-09 13:52:55 +00:00
percentDecodeUrlPath ,
2026-10-07 11:28:28 +08:00
stripQueryAndFragment ,
2026-10-07 16:03:37 +08:00
} from "../../domain/artifactPathSyntax.js" ;
2026-10-11 10:17:58 -07:00
import {
classifyPackageImportSpecifier ,
classifyPackageImportTargetString ,
lookupExactPackageImport ,
} from "../../domain/javascript/javascriptPackageImportAliases.js" ;
2026-10-13 15:58:25 +08:00
import {
typescriptEmittedSourceCandidates ,
type TypeScriptEmittedSourceCandidate ,
} from "../TypeScriptEmittedSourceCandidates.js" ;
2026-10-13 14:39:06 +08:00
import { isRelativeJavaScriptModuleSpecifier } from "../../domain/javascript/javascriptModuleSpecifier.js" ;
2026-10-13 15:58:25 +08:00
import type { JavaScriptModuleResolutionMode } from "../../domain/javascript/javascriptModuleResolutionMode.js" ;
import type { JsonValue } from "../../domain/jsonValue.js" ;
2026-10-14 08:46:50 +08:00
import { err , ok , type Result } from "../../domain/result.js" ;
2026-07-17 21:30:24 +08:00
2026-08-10 14:57:44 +08:00
type ArtifactPathResolutionContext =
| "package-entrypoint"
| "filesystem-expression"
| "module-specifier"
2026-10-07 16:34:15 +05:30
| "html-reference"
| "url-reference" ;
2026-08-10 14:57:44 +08:00
type UnresolvedArtifactPathStatus =
| "not-found"
| "unavailable"
| "external"
| "rejected" ;
interface ArtifactPathResolutionBase {
2026-07-17 21:30:24 +08:00
readonly declared_path : string ;
2026-08-10 14:57:44 +08:00
readonly resolution_context : ArtifactPathResolutionContext ;
2026-07-17 21:30:24 +08:00
readonly limitations : readonly string [];
2026-10-13 15:58:25 +08:00
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 [];
2026-07-17 21:30:24 +08:00
}
2026-09-29 15:30:43 +08:00
/** Explicit outcome of artifact-confined path resolution. */
2026-08-10 14:57:44 +08:00
export type ArtifactPathResolution = ArtifactPathResolutionBase &
(
| {
readonly resolved_path : string ;
readonly resolution_status : "resolved" ;
}
| {
readonly resolved_path : null ;
readonly resolution_status : UnresolvedArtifactPathStatus ;
}
);
2026-07-17 21:30:24 +08:00
/** Values needed to resolve a declaration without filesystem access. */
export interface ResolveArtifactPathInput {
readonly declaredPath : string ;
readonly sourcePath : string ;
2026-08-10 14:57:44 +08:00
readonly context : ArtifactPathResolutionContext ;
2026-07-17 21:30:24 +08:00
readonly files : ReadonlyMap < string , JavaScriptArtifactFile >;
2026-10-14 08:46:50 +08:00
/** Directory identities retained by the complete artifact inventory. */
readonly directoryPaths? : ReadonlySet < string > | undefined ;
/** Inventoried files outside the JavaScript projection: present, never read. */
readonly unanalyzedFiles? : JavaScriptUnanalyzedArtifactLookup | undefined ;
2026-07-17 21:30:24 +08:00
readonly htmlBaseHref? : string | null ;
2026-10-12 01:39:25 +08:00
/** Import/require semantics; runtime resource paths omit this provenance. */
2026-10-13 15:58:25 +08:00
readonly moduleKind? : JavaScriptModuleResolutionMode ;
2026-10-14 09:39:17 +11:00
/** Runtime paths retain their loader owner independently of source imports. */
readonly runtimePathKind ?: "exact-file" | "node-entrypoint" | "url-resource" ;
2026-07-17 21:30:24 +08:00
}
2026-10-13 05:04:21 +08:00
/** Preserve an explicitly retained loader mode on graph relationships. */
export const moduleResolutionModeProperties = (
2026-10-13 15:58:25 +08:00
mode : JavaScriptModuleResolutionMode | undefined ,
) : Readonly < Record < string , JavaScriptModuleResolutionMode >> =>
2026-10-13 05:04:21 +08:00
mode === undefined ? {} : { resolution_mode : mode };
2026-10-12 14:08:09 +08:00
2026-07-17 21:30:24 +08:00
const EXTENSIONS = [
".js" ,
".cjs" ,
".mjs" ,
".ts" ,
".tsx" ,
".json" ,
".html" ,
".node" ,
];
2026-10-12 01:36:42 +08:00
// A private URL namespace for logical artifact paths, including root packages.
// Encode inventory segments so filename punctuation remains literal in the base.
const PACKAGE_ARTIFACT_ROOT_URL = new URL ( "file:///rea-artifact/" );
2026-10-14 08:46:50 +08:00
const encodePackageMetadataUrl = ( path : string ) : URL =>
2026-10-12 01:36:42 +08:00
new URL (
path . split ( "/" ). map ( encodeURIComponent ). join ( "/" ),
PACKAGE_ARTIFACT_ROOT_URL ,
);
2026-10-14 08:46:50 +08:00
// URL identity cannot represent lone UTF-16 surrogates. Use request-local
// opaque segments only for structural admission, never inventory lookup. A
// target cannot spell these identities after URL's tab/newline preprocessing.
const packageMetadataUrl = (
path : string ,
relativeTarget : string ,
) : Result <
URL ,
{
readonly structuralUrl : URL ;
readonly structuralSegments : ReadonlySet < string >;
readonly limitation : string ;
}
> => {
try {
return ok ( encodePackageMetadataUrl ( path ));
} catch ( cause : unknown ) {
if ( ! ( cause instanceof URIError )) throw cause ;
const reserved = new Set (
` ${ path } \ n ${ relativeTarget . replace ( /[\t\n\r]/gu, "")}`.match(
/rea-opaque-\d+/gu,
) ?? [],
);
const structuralSegments = new Set < string >();
let nextIdentity = 0 ;
const structuralPath = path
. split ( "/" )
. map (( segment ) => {
try {
return encodeURIComponent ( segment );
} catch (segmentCause: unknown) {
if (!(segmentCause instanceof URIError)) throw segmentCause;
let identity: string;
do identity = ` rea - opaque - $ { nextIdentity ++ } `;
while (reserved.has(identity));
reserved.add(identity);
structuralSegments.add(identity);
return identity;
}
})
.join("/");
return err({
structuralUrl: new URL(structuralPath, PACKAGE_ARTIFACT_ROOT_URL),
structuralSegments,
limitation: ` Package metadata path $ { JSON . stringify ( path )} contains an unpaired UTF - 16 surrogate and cannot be represented as a URL . `,
});
}
};
2026-10-12 01:36:42 +08:00
interface PackageFileTarget {
readonly value: string;
/** Normalized URL retained until loader-specific validation and file lookup. */
readonly url: URL;
2026-10-14 08:46:50 +08:00
/** Structural validation remains possible when the inventory base has no URL identity. */
readonly lookupLimitation?: string;
2026-10-12 01:36:42 +08:00
}
2026-10-13 15:58:25 +08:00
type CandidateResolution = {
readonly limitations: readonly string[];
readonly candidatePath?: string;
readonly conditionalResolutions?: readonly ConditionalModuleResolution[];
} & (
| { readonly resolvedPath: string; readonly status: "resolved" }
2026-08-10 14:57:44 +08:00
| {
readonly resolvedPath: null;
readonly status: UnresolvedArtifactPathStatus;
2026-10-12 11:26:48 +08:00
/** A truthy package main failed, so Node must not try ancestor packages. */
readonly stopsAncestorLookup?: true;
2026-10-13 15:58:25 +08:00
}
);
2026-07-17 21:30:24 +08:00
2026-10-10 21:26:02 +08:00
/** Resolve one declaration while preserving its original input identity. */
2026-07-17 21:30:24 +08:00
export const resolveArtifactPathByContext = (
input: ResolveArtifactPathInput,
): ArtifactPathResolution => {
2026-10-10 21:26:02 +08:00
const resolution = resolvePath(input);
2026-10-13 15:58:25 +08:00
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 }),
};
2026-10-10 21:26:02 +08:00
return resolution.status === "resolved"
? {
2026-10-13 15:58:25 +08:00
...facts,
2026-10-10 21:26:02 +08:00
resolved_path: resolution.resolvedPath,
resolution_status: resolution.status,
}
2026-10-13 15:58:25 +08:00
: { ...facts, resolved_path: null, resolution_status: resolution.status };
2026-10-10 21:26:02 +08:00
};
2026-10-13 15:58:25 +08:00
/** 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 }),
});
2026-10-10 21:26:02 +08:00
const resolvePath = (input: ResolveArtifactPathInput): CandidateResolution => {
2026-10-14 09:39:17 +11:00
if (input.runtimePathKind === "node-entrypoint")
return resolveNodeRuntimeEntrypoint(input);
2026-10-13 15:58:25 +08:00
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),
],
};
}
2026-10-14 09:39:17 +11:00
if (input.runtimePathKind === "url-resource")
return resolveExactUrlResource(input);
if (input.runtimePathKind === "exact-file" && isRuntimePathDeclaration(input))
return resolveRuntimeEntrypoint(input);
2026-10-11 10:17:58 -07:00
// Package import keys are literal lookup identities, not artifact paths.
// The selected target is admitted separately by resolveExportTarget.
if (
input.context === "module-specifier" &&
input.declaredPath.startsWith("#")
)
return resolvePackageImportAlias(input, input.declaredPath);
2026-10-10 21:26:02 +08:00
const candidate = contextualCandidate(input);
2026-07-17 21:30:24 +08:00
if (typeof candidate !== "string") return candidate;
2026-10-10 21:26:02 +08:00
const confined = confineCandidate(candidate);
2026-07-17 21:30:24 +08:00
if (typeof confined !== "string") return confined;
2026-10-13 06:24:45 +08:00
if (input.context === "html-reference" || input.context === "url-reference")
2026-10-10 21:26:02 +08:00
return (
resolveFileCandidates(input, [confined]) ??
unresolvedCandidate("not-found", [
2026-10-13 06:24:45 +08:00
` The exact $ { input . context === "html-reference" ? "HTML" : "URL" } resource $ { confined } was not found among the selected application files . `,
2026-10-10 21:26:02 +08:00
])
2026-10-08 01:24:24 -04:00
);
2026-10-12 11:10:32 +08:00
const isModuleUrlPath =
2026-10-11 23:52:42 +08:00
input.context === "module-specifier" &&
2026-10-13 14:39:06 +08:00
(isRelativeJavaScriptModuleSpecifier(input.declaredPath) ||
2026-10-12 11:10:32 +08:00
input.declaredPath.startsWith("/") ||
fileUrlPath(input.declaredPath) !== undefined);
const emittedSourceCandidates =
isModuleUrlPath && input.moduleKind !== undefined
2026-10-11 23:52:42 +08:00
? typescriptEmittedSourceCandidates(input.sourcePath, confined)
2026-10-12 11:10:32 +08:00
: [];
if (isModuleUrlPath && input.moduleKind === "import")
return (
2026-10-13 15:58:25 +08:00
resolveRuntimeSourceCandidates(
input,
confined,
emittedSourceCandidates,
) ?? notFoundCandidate()
2026-10-12 11:10:32 +08:00
);
return resolveCandidate(input, confined, new Set(), emittedSourceCandidates);
2026-07-17 21:30:24 +08:00
};
2026-10-14 09:39:17 +11:00
const resolveExactUrlResource = (
input: ResolveArtifactPathInput,
): CandidateResolution => {
// Electron loadURL requires a protocol. A scheme-less string is not relative
// to the calling script; filesystem paths belong to loadFile.
if (!hasScheme(input.declaredPath))
return unresolvedCandidate("unavailable", [
"Electron loadURL requires a URL protocol; relative filesystem paths belong to loadFile.",
]);
if (!/^file:/iu.test(input.declaredPath))
return unresolvedCandidate("external", [
"Only file URLs can be mapped to selected local application artifacts.",
]);
const urlContextCandidate = contextualCandidate(input);
if (typeof urlContextCandidate !== "string") return urlContextCandidate;
const confined = confineCandidate(urlContextCandidate);
if (typeof confined !== "string") return confined;
return (
resolveFileCandidates(input, [confined]) ?? {
resolvedPath: null,
status: "not-found",
candidatePath: confined,
limitations: [
` The exact URL resource $ { confined } was not found among the selected application files . `,
],
}
);
};
const isRuntimePathDeclaration = (input: ResolveArtifactPathInput): boolean =>
input.context === "filesystem-expression" ||
(input.context === "module-specifier" &&
(isRelativeJavaScriptModuleSpecifier(input.declaredPath) ||
input.declaredPath.startsWith("/") ||
fileUrlPath(input.declaredPath) !== undefined));
const resolveNodeRuntimeEntrypoint = (
input: ResolveArtifactPathInput,
): CandidateResolution => {
const { runtimePathKind: _runtimePathKind, ...sourceInput } = input;
const runtimeInput = { ...sourceInput, moduleKind: "require" as const };
if (!isRuntimePathDeclaration(runtimeInput)) return resolvePath(runtimeInput);
const candidate = contextualCandidate(runtimeInput);
if (typeof candidate !== "string") return candidate;
const confined = confineCandidate(candidate);
if (typeof confined !== "string") return confined;
// Node entrypoints use runtime file/directory probes. They do not originate
// in emitted TypeScript import syntax and cannot select a source substitute.
2026-10-14 08:46:50 +08:00
const resolution = resolveCandidate(runtimeInput, confined, new Set());
// Runtime entrypoint analysis has no extension or directory fallback, but
// retain the normalized loader path when that exact target is absent.
return resolution.status === "not-found" &&
resolution.candidatePath === undefined
? { ...resolution, candidatePath: confined }
: resolution;
2026-10-14 09:39:17 +11:00
};
const resolveRuntimeEntrypoint = (
input: ResolveArtifactPathInput,
): CandidateResolution => {
const rejected = rejectDeclaration(input.declaredPath);
if (rejected !== null) return rejected;
if (hasScheme(input.declaredPath) || looksExternal(input.declaredPath))
return unresolvedCandidate("external", [
"Electron runtime entrypoints must identify local paths, not external URLs.",
]);
const candidate = confineCandidate(
relativeCandidate(input.sourcePath, input.declaredPath),
);
if (typeof candidate !== "string") return candidate;
return (
resolveFileCandidates(input, [candidate]) ?? {
resolvedPath: null,
status: "not-found",
candidatePath: candidate,
limitations: [
` The declared Electron runtime entrypoint $ { candidate } was not found as an exact file among the selected application files ; extension and directory fallback were not attempted . `,
],
}
);
};
2026-10-13 15:58:25 +08:00
const resolveRuntimeSourceCandidates = (
input: ResolveArtifactPathInput,
exactPath: string,
sourceCandidates: readonly TypeScriptEmittedSourceCandidate[],
): CandidateResolution | null => {
const exactCandidate: TypeScriptEmittedSourceCandidate = {
path: exactPath,
kind: "implementation",
};
for (const candidate of [exactCandidate, ...sourceCandidates]) {
2026-10-14 08:46:50 +08:00
const selected = findFileCandidate(input, candidate.path);
if (selected === null) continue;
2026-10-13 15:58:25 +08:00
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.",
],
}
2026-10-14 08:46:50 +08:00
: selected;
2026-10-13 15:58:25 +08:00
}
return null;
};
2026-10-14 06:41:22 +08:00
const rejectDeclaration = (
declared: string,
2026-10-14 04:20:29 +05:30
literalPercent = false,
2026-10-14 06:41:22 +08:00
allowEmptyPath = false,
): CandidateResolution | null => {
if (declared.length === 0 && !allowEmptyPath)
2026-10-10 21:26:02 +08:00
return unresolvedCandidate("rejected", ["The declared path is empty."]);
2026-07-17 21:30:24 +08:00
if (declared.includes(" \ 0") || declared.includes("\\"))
2026-10-10 21:26:02 +08:00
return unresolvedCandidate("rejected", [
2026-07-17 21:30:24 +08:00
"NUL and backslash path syntax are not admitted for canonical artifact paths.",
]);
2026-10-14 04:20:29 +05:30
if (!literalPercent && !admitsCanonicalPathSyntax(declared))
2026-10-10 21:26:02 +08:00
return unresolvedCandidate("rejected", [
2026-07-17 21:30:24 +08:00
"Encoded dot or separator bytes are rejected before artifact path resolution.",
]);
return null;
};
2026-10-14 04:20:29 +05:30
/**
* Node's CommonJS loader never percent-decodes a request or a legacy package
* main, so ` % 2 e ` and ` % 2 f ` there are literal filename bytes, not encoded dots
* or separators. Root confinement still applies to the joined candidate.
*/
const declaresLiteralPercent = (input: ResolveArtifactPathInput): boolean =>
input.moduleKind === "require" &&
(input.context === "module-specifier" ||
input.context === "package-entrypoint");
2026-07-17 21:30:24 +08:00
const contextualCandidate = (
input: ResolveArtifactPathInput,
2026-10-10 21:26:02 +08:00
): string | CandidateResolution => {
2026-07-17 21:30:24 +08:00
const { context } = input;
2026-10-14 06:41:22 +08:00
if (context === "html-reference") return htmlCandidate(input);
2026-10-13 06:24:45 +08:00
// URL suffixes are evidence, not pathname bytes. Admit only the path while
// retaining the complete declaration on the resolution result.
2026-10-08 02:00:34 -04:00
let declared =
2026-10-13 06:24:45 +08:00
context === "url-reference"
2026-07-17 21:30:24 +08:00
? stripQueryAndFragment(input.declaredPath)
: input.declaredPath;
2026-10-14 09:39:17 +11:00
if (context === "url-reference" && declared.length === 0)
return input.runtimePathKind === "url-resource"
? unresolvedCandidate("unavailable", [
"A suffix-only runtime URL requires the window's current document URL; the calling script does not establish that base.",
])
: unresolvedCandidate("rejected", [
"The URL reference contains no pathname, and its base document is not established by this analysis.",
]);
2026-10-13 07:35:12 +08:00
if (context === "module-specifier" && /^file:/iu.test(declared)) {
2026-07-17 21:30:24 +08:00
const fileUrl = fileUrlPath(declared);
2026-10-13 07:35:12 +08:00
if (fileUrl === null)
return unresolvedCandidate("rejected", [
"The file URL path contains syntax that cannot identify a canonical artifact path.",
]);
2026-10-12 14:08:09 +08:00
if (fileUrl !== undefined)
return input.moduleKind === "require"
? unresolvedCandidate("external", [
"Node CommonJS require specifiers do not resolve file URLs as local artifacts.",
])
: fileUrl;
2026-10-13 07:35:12 +08:00
return unresolvedCandidate("external", [
"File URL authorities or invalid file URL forms cannot be mapped to artifact-local paths.",
]);
}
2026-10-14 08:46:50 +08:00
const hasUrlPathSemantics =
context === "module-specifier" && input.moduleKind === "import";
const rejected = rejectDeclaration(
declared,
declaresLiteralPercent(input) || hasUrlPathSemantics,
);
2026-10-13 07:35:12 +08:00
if (rejected !== null) return rejected;
if (context === "module-specifier") {
2026-07-17 21:30:24 +08:00
if (hasScheme(declared))
2026-10-10 21:26:02 +08:00
return unresolvedCandidate("external", [
2026-07-17 21:30:24 +08:00
"URL and Node builtin schemes are outside static artifact module resolution.",
]);
2026-10-13 14:39:06 +08:00
if (
!isRelativeJavaScriptModuleSpecifier(declared) &&
!declared.startsWith("/")
)
2026-10-12 04:09:17 +08:00
return bareModuleCandidate(input);
2026-10-10 21:26:02 +08:00
if (input.moduleKind !== "require") {
2026-10-14 08:46:50 +08:00
if (/%(?:2f|5c)/iu.test(stripQueryAndFragment(declared)))
return unresolvedCandidate("rejected", [
"ES module URL paths cannot contain percent-encoded slash or backslash bytes.",
]);
2026-10-12 04:09:17 +08:00
const decoded = decodeModuleUrlPath(stripQueryAndFragment(declared));
2026-10-10 21:26:02 +08:00
if (typeof decoded !== "string") return decoded;
declared = decoded;
}
2026-10-07 16:34:15 +05:30
} else if (looksExternal(declared))
2026-10-10 21:26:02 +08:00
return unresolvedCandidate("external", [
2026-10-07 16:34:15 +05:30
"URL schemes and protocol-relative URLs are outside this local artifact path context.",
2026-07-17 21:30:24 +08:00
]);
2026-10-11 18:19:39 +08:00
if (context === "url-reference") {
2026-10-13 06:24:45 +08:00
// A file base gives raw drive-letter segments special meaning, including
// after dot segments reach the root. Parse before percent decoding so an
// encoded drive-like filename stays literal. The synthetic base establishes
// only this ambiguity; it does not establish the source's runtime scheme.
const sourceUrlPath = input.sourcePath
.split("/")
.map(encodeURIComponent)
.join("/");
const fileReference = URL.parse(declared, ` file : ///${sourceUrlPath}`);
if ( fileReference === null )
return unresolvedCandidate ( "rejected" , [
"The URL reference cannot be parsed as a URL." ,
]);
if ( /^\/[A-Za-z]:(?:\/|$)/u.test(fileReference.pathname))
return unresolvedCandidate ( "unavailable" , [
"Drive-letter URL paths cannot be mapped without the source URL scheme and filesystem root." ,
]);
2026-10-11 18:19:39 +08:00
const decoded = percentDecodeUrlPath ( declared );
if ( decoded === null )
return unresolvedCandidate ( "rejected" , [
"The URL reference path percent-decodes to NUL or to bytes that are not UTF-8." ,
]);
2026-10-13 06:24:45 +08:00
// Empty URL segments are significant; canonical filesystem paths cannot
// preserve them, including their interaction with following dot segments.
if ( decoded . includes ( "//" ))
return unresolvedCandidate ( "unavailable" , [
"Repeated URL path separators cannot be mapped to an exact canonical artifact file." ,
]);
return exactUrlPathCandidate (
decoded ,
relativeCandidate ( input . sourcePath , decoded ),
"URL" ,
);
2026-10-11 18:19:39 +08:00
}
2026-10-13 14:39:06 +08:00
const candidate = relativeCandidate ( input . sourcePath , declared );
return context === "module-specifier" && input . moduleKind === "import"
? exactUrlPathCandidate ( declared , candidate , "module URL" )
: candidate ;
2026-10-10 21:26:02 +08:00
};
const decodeModuleUrlPath = (
declared : string ,
) : string | CandidateResolution => {
let decoded : string ;
try {
decoded = decodeURIComponent ( declared );
} catch ( cause : unknown ) {
void cause ;
return unresolvedCandidate ( "rejected" , [
"The module URL path contains malformed percent encoding." ,
]);
2026-10-08 02:00:34 -04:00
}
2026-10-10 21:26:02 +08:00
return decoded . includes ( "\0" )
? unresolvedCandidate ( "rejected" , [
"The decoded module URL path contains NUL." ,
])
: decoded ;
2026-07-17 21:30:24 +08:00
};
2026-10-10 21:26:02 +08:00
const relativeCandidate = ( sourcePath : string , declared : string ) : string =>
declared . startsWith ( "/" )
? declared . slice ( 1 )
: posix . join ( posix . dirname ( sourcePath ), declared );
2026-07-17 23:57:23 +08:00
const bareModuleCandidate = (
input : ResolveArtifactPathInput ,
2026-10-10 21:26:02 +08:00
) : CandidateResolution => {
2026-10-12 04:09:17 +08:00
const request = parseBarePackageRequest ( input . declaredPath , input . moduleKind );
if ( request === null || isBuiltin ( input . declaredPath ))
2026-10-10 21:26:02 +08:00
return unresolvedCandidate ( "external" , [
2026-10-11 10:17:58 -07:00
"The bare specifier is a Node builtin or invalid package name." ,
2026-07-17 23:57:23 +08:00
]);
2026-10-13 14:39:06 +08:00
if (
input . moduleKind === "import" &&
( request . packageName . startsWith ( "." ) || request . packageName . includes ( "%" ))
)
2026-10-11 18:57:58 +08:00
return unresolvedCandidate ( "rejected" , [
2026-10-13 14:39:06 +08:00
"ES module package names cannot start with a dot or contain percent signs." ,
2026-10-11 18:57:58 +08:00
]);
2026-10-12 04:09:17 +08:00
const self = packageSelfCandidate ( input , request );
2026-10-11 18:57:58 +08:00
if ( self !== null ) return self ;
2026-10-12 04:09:17 +08:00
// ESM constructs a package.json URL; its parsed identity is not a literal
// dependency directory. Self resolution above still uses the exact name.
let dependencyDirectory = posix . join ( "node_modules" , request . packageName );
if ( input . moduleKind === "import" ) {
const metadataUrl = new URL (
`./node_modules/ ${ request . packageName } /package.json` ,
PACKAGE_ARTIFACT_ROOT_URL ,
);
if ( ! metadataUrl . pathname . endsWith ( "/package.json" ))
return unresolvedCandidate ( "external" , [
`ESM package name ${ JSON . stringify ( request . packageName ) } produces a dependency URL whose pathname does not name package.json; no literal or suffix-stripped package directory was searched.` ,
]);
const decoded = decodeModuleUrlPath (
metadataUrl . pathname . slice ( PACKAGE_ARTIFACT_ROOT_URL . pathname . length ),
);
if ( typeof decoded !== "string" ) return decoded ;
const confined = confineCandidate ( decoded );
if ( typeof confined !== "string" ) return confined ;
dependencyDirectory = posix . dirname ( confined );
}
2026-07-17 23:57:23 +08:00
const source = input . files . get ( input . sourcePath );
2026-10-12 11:26:48 +08:00
let nearestNotFound : CandidateResolution | null = null ;
2026-10-13 21:01:30 +05:30
for ( const directory of packageLookupDirectories ( input )) {
2026-10-12 04:09:17 +08:00
const candidate = posix . join ( directory , dependencyDirectory );
2026-10-11 18:57:58 +08:00
const metadata = readPackageMetadata ( input , candidate );
2026-10-12 11:26:48 +08:00
const continueCommonJsLookup =
input . moduleKind === "require" &&
( metadata . kind === "absent" ||
( metadata . kind === "available" &&
( ! request . dependencyExportsEligible ||
metadata . exports === undefined )));
const packageResolution =
metadata . kind !== "absent" ||
2026-10-14 08:46:50 +08:00
hasContainerDirectory (
input . files ,
candidate ,
source ? . container_sha256 ,
input . directoryPaths ,
input . unanalyzedFiles ,
)
2026-10-12 11:26:48 +08:00
? resolvePackageDependency ( input , candidate , request , metadata )
: request . packageSubpath === "." && input . moduleKind !== "import"
? resolveFileCandidates (
input ,
fileCandidates ( candidate , input . moduleKind ),
)
: null ;
if ( packageResolution !== null ) {
if (
continueCommonJsLookup &&
packageResolution . status === "not-found" &&
packageResolution . stopsAncestorLookup !== true
) {
nearestNotFound ??= packageResolution ;
} else return packageResolution ;
2026-10-10 21:26:02 +08:00
}
2026-07-17 23:57:23 +08:00
}
2026-10-12 11:26:48 +08:00
if ( nearestNotFound !== null ) return nearestNotFound ;
2026-10-10 21:26:02 +08:00
return unresolvedCandidate ( "external" , [
2026-07-17 23:57:23 +08:00
"No matching bare package was inventoried in an enclosing node_modules directory." ,
]);
};
2026-10-13 21:01:30 +05:30
function * packageLookupDirectories (
input : ResolveArtifactPathInput ,
) : Generator < string > {
let directory = posix . dirname ( input . sourcePath );
while ( true ) {
if (
input . moduleKind !== "require" ||
posix . basename ( directory ) !== "node_modules"
)
yield directory ;
if ( directory === "." || directory === "" ) return ;
directory = posix . dirname ( directory );
}
}
2026-10-11 10:17:58 -07:00
/**
* Nearest same-container package.json, stopping at a `node_modules` directory
* so a nested package cannot inherit an ancestor's map.
*/
const enclosingPackageMetadata = (
2026-10-11 18:57:58 +08:00
input : ResolveArtifactPathInput ,
2026-10-11 10:17:58 -07:00
) :
| Extract < PackageMetadata , { readonly kind : "available" | "unavailable" }>
| { readonly kind : "none" } => {
2026-10-11 18:57:58 +08:00
let directory = posix . dirname ( input . sourcePath );
while ( posix . basename ( directory ) !== "node_modules" ) {
const metadata = readPackageMetadata ( input , directory );
2026-10-11 10:17:58 -07:00
if ( metadata . kind !== "absent" ) return metadata ;
2026-10-11 18:57:58 +08:00
if ( directory === "." || directory === "" ) break ;
directory = posix . dirname ( directory );
}
2026-10-11 10:17:58 -07:00
return { kind : "none" };
};
/**
* Resolve an exact `#alias` against the importer's enclosing `imports` map.
* Selected relative targets are exact files; patterns, recursive aliases, and
* external-package targets stay unresolved without falling through.
*/
const resolvePackageImportAlias = (
input : ResolveArtifactPathInput ,
specifier : string ,
) : CandidateResolution => {
const classified = classifyPackageImportSpecifier ( specifier );
if ( classified . kind === "invalid" )
return unresolvedCandidate ( "rejected" , [
`The package import specifier ${ JSON . stringify ( specifier ) } is invalid; "#" and specifiers that end with "/" are not valid import aliases.` ,
]);
if ( classified . kind === "pattern" )
return unresolvedCandidate ( "external" , [
`Wildcard and directory package import aliases remain unresolved; ${ JSON . stringify ( specifier ) } is not an exact imports key.` ,
]);
const metadata = enclosingPackageMetadata ( input );
if ( metadata . kind === "none" )
return unresolvedCandidate ( "external" , [
`No enclosing package.json defines package import alias ${ specifier } ; package import maps do not cross package scopes.` ,
]);
if ( metadata . kind === "unavailable" )
return unresolvedCandidate ( "unavailable" , [ metadata . limitation ]);
const lookup = lookupExactPackageImport ( metadata . imports , specifier );
if ( lookup . kind === "undefined-map" )
return unresolvedCandidate ( "external" , [
`Package metadata ${ metadata . path } does not define package import alias ${ specifier } .` ,
]);
if ( lookup . kind === "missing-key" )
return unresolvedCandidate ( "external" , [
`Package metadata ${ metadata . path } does not define package import alias ${ specifier } ; it declares ${ lookup . declared . join ( ", " ) || "no imports keys" } .` ,
]);
if ( lookup . kind === "pattern-unresolved" )
return unresolvedCandidate ( "external" , [
`Wildcard package import aliases remain unresolved; package metadata ${ metadata . path } has no exact imports key ${ specifier } .` ,
]);
2026-10-12 04:09:17 +08:00
const selected = selectExportTarget (
2026-10-12 01:36:42 +08:00
lookup . target ,
2026-10-12 04:09:17 +08:00
packageExportConditions ( input . moduleKind ),
2026-10-14 08:46:50 +08:00
metadata . path ,
2026-10-12 04:09:17 +08:00
"imports" ,
2026-10-12 01:36:42 +08:00
);
2026-10-12 04:09:17 +08:00
if ( selected . kind === "invalid" || selected . kind === "invalid-config" )
2026-10-11 10:17:58 -07:00
return unresolvedCandidate ( "rejected" , [
`Package metadata ${ metadata . path } : ${ selected . limitation } ` ,
]);
2026-10-12 04:09:17 +08:00
if ( selected . kind === "unmatched" )
2026-10-11 10:17:58 -07:00
return unresolvedCandidate ( "external" , [
`Package metadata ${ metadata . path } declares no imports target for alias ${ specifier } under the active conditions; it declares ${ selected . declared . join ( ", " ) || "no conditions" } .` ,
]);
2026-10-12 04:09:17 +08:00
if ( selected . kind === "blocked" )
2026-10-11 10:17:58 -07:00
return unresolvedCandidate ( "external" , [
`Package metadata ${ metadata . path } blocks import alias ${ specifier } with ${ selected . cause === "null" ? "an explicit null imports target" : "an empty imports target array" } .` ,
]);
2026-10-12 04:09:17 +08:00
if ( selected . kind === "non-relative" ) {
2026-10-11 10:17:58 -07:00
const target = classifyPackageImportTargetString ( selected . value );
return unresolvedCandidate ( "external" , [
target . kind === "recursive"
? `Recursive package import aliases remain unresolved; package metadata ${ metadata . path } maps ${ specifier } to ${ JSON . stringify ( target . target ) } .`
: `External-package import targets remain unresolved; package metadata ${ metadata . path } maps ${ specifier } to ${ JSON . stringify ( target . target ) } .` ,
]);
}
2026-10-12 01:36:42 +08:00
return resolveExportTarget ( input , metadata . path , selected , "imports" );
2026-10-11 10:17:58 -07:00
};
/** Self references use the nearest package scope before dependency lookup. */
2026-10-12 04:09:17 +08:00
const packageSelfCandidate = (
2026-10-11 10:17:58 -07:00
input : ResolveArtifactPathInput ,
2026-10-12 04:09:17 +08:00
request : BarePackageRequest ,
2026-10-11 10:17:58 -07:00
) : CandidateResolution | null => {
2026-10-12 04:09:17 +08:00
if ( input . moduleKind === undefined || ! input . files . has ( input . sourcePath ))
2026-10-11 10:17:58 -07:00
return null ;
const metadata = enclosingPackageMetadata ( input );
if ( metadata . kind === "none" ) return null ;
if ( metadata . kind === "unavailable" )
return unresolvedCandidate ( "unavailable" , [ metadata . limitation ]);
2026-10-12 04:09:17 +08:00
if ( metadata . name === undefined || metadata . exports === undefined )
2026-10-11 10:17:58 -07:00
return null ;
2026-10-12 04:09:17 +08:00
let subpath : string ;
if ( input . moduleKind === "import" ) {
if ( metadata . name !== request . packageName ) return null ;
subpath = request . packageSubpath ;
} else if ( input . declaredPath === metadata . name ) {
subpath = "." ;
} else if ( input . declaredPath . startsWith ( ` ${ metadata . name } /` )) {
subpath = `. ${ input . declaredPath . slice ( metadata . name . length ) } ` ;
} else return null ;
2026-10-11 10:17:58 -07:00
// Self resolution does not apply the CommonJS dependency-name matcher.
return resolvePackageExports ( input , metadata . path , subpath , metadata . exports );
2026-10-11 18:57:58 +08:00
};
2026-10-12 04:09:17 +08:00
/** Resolve an installed dependency using one parsed request and its metadata. */
const resolvePackageDependency = (
2026-10-10 21:26:02 +08:00
input : ResolveArtifactPathInput ,
directory : string ,
2026-10-12 04:09:17 +08:00
request : BarePackageRequest ,
2026-10-11 18:57:58 +08:00
metadata : PackageMetadata ,
2026-10-10 21:26:02 +08:00
) : CandidateResolution => {
2026-10-12 04:09:17 +08:00
const subpath = request . packageSubpath ;
if ( request . dependencyExportsEligible && metadata . kind === "unavailable" )
2026-10-10 21:26:02 +08:00
return unresolvedCandidate ( "unavailable" , [ metadata . limitation ]);
const rawExports =
2026-10-12 04:09:17 +08:00
request . dependencyExportsEligible && metadata . kind === "available"
2026-10-11 18:57:58 +08:00
? metadata.exports
2026-10-10 21:26:02 +08:00
: undefined ;
2026-10-11 18:57:58 +08:00
if ( rawExports === undefined ) {
2026-10-10 21:26:02 +08:00
if ( subpath === "." )
2026-10-11 18:57:58 +08:00
return (
2026-10-12 11:26:48 +08:00
( input . moduleKind === "import"
? null
: resolveFileCandidates (
input ,
fileCandidates ( directory , input . moduleKind ),
)) ?? resolveDirectory ( input , directory , metadata , new Set ())
2026-10-11 18:57:58 +08:00
);
2026-10-14 08:46:50 +08:00
if ( input . moduleKind === "import" )
return resolveLegacyEsmSubpath ( input , directory , subpath );
const candidate = confineCandidate ( ` ${ directory } / ${ subpath . slice ( 2 ) } ` );
2026-10-10 21:26:02 +08:00
return typeof candidate === "string"
? resolveCandidate ( input , candidate , new Set ())
: candidate ;
}
2026-10-11 18:57:58 +08:00
return resolvePackageExports (
input ,
posix . join ( directory , "package.json" ),
subpath ,
rawExports ,
);
};
2026-10-12 04:09:17 +08:00
/** Legacy ESM package subpaths are exact URLs, never extension or directory lookups. */
const resolveLegacyEsmSubpath = (
input : ResolveArtifactPathInput ,
2026-10-14 08:46:50 +08:00
directory : string ,
subpath : string ,
2026-10-12 04:09:17 +08:00
) : CandidateResolution => {
2026-10-14 08:46:50 +08:00
const declarationRejection = rejectDeclaration (
stripQueryAndFragment ( htmlUrlText ( subpath )),
);
if ( declarationRejection !== null ) return declarationRejection ;
const metadataUrl = packageMetadataUrl (
posix . join ( directory , "package.json" ),
subpath ,
);
const url = new URL (
subpath ,
metadataUrl . ok ? metadataUrl.value : metadataUrl.error.structuralUrl ,
);
if ( ! url . pathname . startsWith ( PACKAGE_ARTIFACT_ROOT_URL . pathname ))
return unresolvedCandidate ( "rejected" , [
"The resolved candidate escapes the canonical artifact root." ,
]);
const encodedPath = url . pathname . slice (
PACKAGE_ARTIFACT_ROOT_URL . pathname . length ,
);
const rejected = rejectDeclaration ( encodedPath );
if ( rejected !== null ) return rejected ;
const decoded = decodeModuleUrlPath ( encodedPath );
if ( typeof decoded !== "string" ) return decoded ;
const candidate = confineCandidate ( decoded );
if ( typeof candidate !== "string" ) return candidate ;
if ( urlPathIsDirectory ( url . pathname ))
return unresolvedCandidate ( "rejected" , [
`ESM package subpath ${ JSON . stringify ( input . declaredPath ) } has URL directory form; directory imports are unsupported and no exact-file, package main or index lookup was attempted.` ,
]);
const unrepresentableBase = metadataUrl . ok ? undefined : metadataUrl . error ;
if (
unrepresentableBase !== undefined &&
url . pathname
. split ( "/" )
. some (( segment ) => unrepresentableBase . structuralSegments . has ( segment ))
)
return unresolvedCandidate ( "unavailable" , [ unrepresentableBase . limitation ]);
2026-10-12 04:09:17 +08:00
const file = resolveFileCandidates ( input , [ candidate ]);
if ( file !== null ) return file ;
const source = input . files . get ( input . sourcePath );
2026-10-14 08:46:50 +08:00
if (
hasContainerDirectory (
input . files ,
candidate ,
source ? . container_sha256 ,
input . directoryPaths ,
input . unanalyzedFiles ,
)
)
2026-10-12 04:09:17 +08:00
return unresolvedCandidate ( "rejected" , [
`ESM package subpath ${ candidate } names a directory; directory imports are unsupported and package main or index lookup was not attempted.` ,
]);
return exactPackageTargetNotFound ( candidate , "ESM subpath" );
};
2026-10-11 18:57:58 +08:00
const resolvePackageExports = (
input : ResolveArtifactPathInput ,
packagePath : string ,
subpath : string ,
rawExports : string | object ,
) : CandidateResolution => {
2026-10-12 01:36:42 +08:00
const exported = selectPackageExport (
rawExports ,
subpath ,
input . moduleKind ,
packagePath ,
);
2026-10-10 21:26:02 +08:00
if ( exported . status === "rejected" )
return unresolvedCandidate ( "rejected" , [
`Package metadata ${ packagePath } : ${ exported . limitation } ` ,
]);
if ( exported . status === "unmatched" )
return unresolvedCandidate ( "external" , [
`Directory package metadata ${ packagePath } declares no exports target ${ subpath === "." ? "for the active conditions" : `for subpath ${ subpath } ` } ; exports encapsulation permits no file or index lookup. It declares ${ exported . declared . join ( ", " ) || "a root target only" } .` ,
]);
if ( exported . status === "blocked" )
return unresolvedCandidate ( "external" , [
2026-10-11 10:17:58 -07:00
`Directory package metadata ${ packagePath } blocks subpath ${ subpath } with ${ exported . cause === "null" ? "an explicit null exports target" : "an empty exports target array" } .` ,
2026-10-10 21:26:02 +08:00
]);
2026-10-12 01:36:42 +08:00
return resolveExportTarget ( input , packagePath , exported );
2026-10-10 21:26:02 +08:00
};
2026-10-11 10:17:58 -07:00
/** An exports or import target is a URL naming one exact file for both Node loaders. */
2026-10-10 21:26:02 +08:00
const resolveExportTarget = (
input : ResolveArtifactPathInput ,
packagePath : string ,
2026-10-12 01:36:42 +08:00
target : PackageFileTarget ,
2026-10-11 10:17:58 -07:00
mapKind : "exports" | "imports" = "exports" ,
2026-10-10 21:26:02 +08:00
) : CandidateResolution => {
2026-10-12 01:36:42 +08:00
const withTargetSource = (
outcome : CandidateResolution ,
) : CandidateResolution => ({
... outcome ,
limitations : outcome.limitations.map (
( reason ) =>
`Package metadata ${ packagePath } , ${ mapKind } target ${ JSON . stringify ( target . value ) } : ${ reason } ` ,
),
});
// Node's CommonJS finalizer checks the serialized URL, including suffixes;
// its ESM finalizer checks only the pathname. Neither error retries arrays.
if ( input . moduleKind === "require" && /%(?:2f|5c)/iu.test(target.url.href))
return withTargetSource (
unresolvedCandidate ( "rejected" , [
"The selected package URL contains encoded separator bytes." ,
]),
);
const encodedPath = target . url . pathname . slice (
PACKAGE_ARTIFACT_ROOT_URL . pathname . length ,
);
2026-10-14 04:20:29 +05:30
// Package targets are URLs for both loaders, so their percent bytes encode.
const rejected = rejectDeclaration ( encodedPath , false );
2026-10-12 01:36:42 +08:00
if ( rejected !== null ) return withTargetSource ( rejected );
const decoded = decodeModuleUrlPath ( encodedPath );
if ( typeof decoded !== "string" ) return withTargetSource ( decoded );
const candidate = confineCandidate ( decoded );
if ( typeof candidate !== "string" ) return withTargetSource ( candidate );
2026-10-14 08:46:50 +08:00
if ( target . lookupLimitation !== undefined )
return unresolvedCandidate ( "unavailable" , [ target . lookupLimitation ]);
2026-10-10 21:26:02 +08:00
return (
resolveFileCandidates ( input , [ candidate ]) ??
2026-10-12 01:36:42 +08:00
withTargetSource ( exactPackageTargetNotFound ( candidate , mapKind ))
2026-10-10 21:26:02 +08:00
);
};
2026-10-12 04:09:17 +08:00
/** Derived package identity and dependency policy; input retains the raw request. */
interface BarePackageRequest {
readonly packageName : string ;
readonly packageSubpath : string ;
readonly dependencyExportsEligible : boolean ;
}
2026-07-17 23:57:23 +08:00
2026-10-12 04:09:17 +08:00
const parseBarePackageRequest = (
specifier : string ,
moduleKind : ResolveArtifactPathInput [ "moduleKind" ],
) : BarePackageRequest | null => {
const segments = specifier . split ( "/" );
let packageName = segments [ 0 ] ?? "" ;
if ( packageName === "" ) return null ;
if ( specifier . startsWith ( "@" )) {
const packageSegment = segments [ 1 ];
if ( packageSegment === undefined || packageSegment === "" ) return null ;
packageName = ` ${ packageName } / ${ packageSegment } ` ;
}
2026-10-11 18:57:58 +08:00
// Node's CommonJS exports matcher excludes percent-containing package names.
// Compare the captured name so a scoped name cannot fall back to its scope.
2026-10-12 04:09:17 +08:00
const dependencyExportsEligible =
moduleKind !== "require" ||
/^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/ . exec ( specifier ) ? .[ 1 ] ===
packageName ;
return {
packageName ,
packageSubpath : `. ${ specifier . slice ( packageName . length ) } ` ,
dependencyExportsEligible ,
};
2026-10-11 18:57:58 +08:00
};
2026-10-10 21:26:02 +08:00
const hasContainerDirectory = (
2026-07-17 23:57:23 +08:00
files : ReadonlyMap < string , JavaScriptArtifactFile >,
candidate : string ,
containerSha256 : string | undefined ,
2026-10-14 08:46:50 +08:00
directoryPaths : ReadonlySet < string > | undefined ,
unanalyzedFiles? : JavaScriptUnanalyzedArtifactLookup ,
2026-10-10 21:26:02 +08:00
) : boolean => {
2026-10-14 08:46:50 +08:00
if ( directoryPaths ? . has ( candidate ) === true ) return true ;
2026-10-10 21:26:02 +08:00
if (
[... indexCandidates ( candidate ), posix . join ( candidate , "package.json" )]. some (
( path ) => {
const file = files . get ( path );
return (
file !== undefined &&
( containerSha256 === undefined ||
file . container_sha256 === containerSha256 )
);
},
)
)
return true ;
// Inventory has files rather than directory entries. Any same-container
// descendant establishes a package directory even without package.json.
const prefix = ` ${ candidate } /` ;
for ( const [ path , file ] of files )
if (
path . startsWith ( prefix ) &&
2026-10-05 07:34:48 -07:00
( containerSha256 === undefined ||
file . container_sha256 === containerSha256 )
2026-10-10 21:26:02 +08:00
)
return true ;
2026-10-14 08:46:50 +08:00
return unanalyzedFiles ? . hasDirectory ( candidate , containerSha256 ) ?? false ;
2026-10-10 21:26:02 +08:00
};
2026-07-17 23:57:23 +08:00
2026-07-17 21:30:24 +08:00
const htmlCandidate = (
input : ResolveArtifactPathInput ,
2026-10-10 21:26:02 +08:00
) : string | CandidateResolution => {
2026-10-09 13:52:55 +00:00
const declared = stripQueryAndFragment ( htmlUrlText ( input . declaredPath ));
2026-10-14 06:41:22 +08:00
// HTML owns URL-text normalization and pathname admission. A nonempty URL
// reference may have no pathname; suffix bytes remain declaration evidence.
2026-10-14 04:20:29 +05:30
// HTML references are URL text, so percent bytes still encode.
const rejected = rejectDeclaration (
declared ,
false ,
input . declaredPath !== "" ,
);
2026-10-14 06:41:22 +08:00
if ( rejected !== null ) return rejected ;
2026-10-07 11:28:28 +08:00
if ( looksExternal ( declared ))
2026-10-10 21:26:02 +08:00
return unresolvedCandidate ( "external" , [
2026-07-17 21:30:24 +08:00
"External HTML references are not mapped to local artifact assets." ,
]);
2026-10-05 13:02:56 -04:00
const rawBase = input . htmlBaseHref ;
const base =
rawBase === undefined || rawBase === null
? rawBase
2026-10-09 13:52:55 +00:00
: stripQueryAndFragment ( htmlUrlText ( rawBase ));
2026-10-07 11:28:28 +08:00
if ( base !== undefined && base !== null && looksExternal ( base ))
2026-10-10 21:26:02 +08:00
return unresolvedCandidate ( "external" , [
2026-10-05 13:02:56 -04:00
"The document base href is external, so its script reference is not a local artifact path." ,
]);
2026-10-09 13:52:55 +00:00
const declaredPath = percentDecodeUrlPath ( declared );
if ( declaredPath === null )
2026-10-10 21:26:02 +08:00
return unresolvedCandidate ( "rejected" , [
2026-10-09 13:52:55 +00:00
"The HTML reference path percent-decodes to NUL or to bytes that are not UTF-8." ,
]);
if ( declaredPath . startsWith ( "/" )) return declaredPath . slice ( 1 );
2026-07-17 21:30:24 +08:00
if ( base === undefined || base === null || base === "" )
2026-10-10 03:11:08 +08:00
return declaredPath === ""
? input.sourcePath
: posix.join ( posix . dirname ( input . sourcePath ), declaredPath );
2026-10-05 13:02:56 -04:00
// A local base href is a second untrusted path input; apply the same
// admission rules a declared path gets so it cannot smuggle traversal or
// separator syntax past canonicalization.
if ( ! admitsCanonicalPathSyntax ( base ))
2026-10-10 21:26:02 +08:00
return unresolvedCandidate ( "rejected" , [
2026-10-05 13:02:56 -04:00
"The document base href uses NUL, backslash, or encoded dot and separator bytes that are not admitted for canonical artifact paths." ,
2026-07-17 21:30:24 +08:00
]);
2026-10-09 13:52:55 +00:00
const decodedBase = percentDecodeUrlPath ( base );
if ( decodedBase === null )
2026-10-10 21:26:02 +08:00
return unresolvedCandidate ( "rejected" , [
2026-10-09 13:52:55 +00:00
"The document base href path percent-decodes to NUL or to bytes that are not UTF-8." ,
]);
const basePath = decodedBase . startsWith ( "/" )
? decodedBase . slice ( 1 )
: posix . join ( posix . dirname ( input . sourcePath ), decodedBase );
2026-10-13 06:24:45 +08:00
if ( declaredPath === "" )
return exactUrlPathCandidate ( decodedBase , basePath , "HTML" );
2026-10-09 13:52:55 +00:00
return posix . join ( htmlBaseDirectory ( decodedBase , basePath ), declaredPath );
2026-07-17 21:30:24 +08:00
};
2026-10-13 06:24:45 +08:00
const exactUrlPathCandidate = (
declared : string ,
candidate : string ,
2026-10-13 14:39:06 +08:00
kind : "HTML" | "URL" | "module URL" ,
2026-10-10 21:26:02 +08:00
) : string | CandidateResolution => {
2026-10-13 06:24:45 +08:00
if ( ! urlPathIsDirectory ( declared )) return candidate ;
const confined = confineCandidate ( candidate );
2026-10-10 03:11:08 +08:00
if ( typeof confined !== "string" ) return confined ;
2026-10-10 21:26:02 +08:00
return unresolvedCandidate ( "not-found" , [
2026-10-13 06:24:45 +08:00
`The ${ kind } reference resolves to directory ${ confined || "." } , not an exact selected application file.` ,
2026-10-10 03:11:08 +08:00
]);
};
2026-10-13 06:24:45 +08:00
/** Whether a URL path retains directory form after dot-segment normalization. */
const urlPathIsDirectory = ( path : string ) : boolean =>
path . endsWith ( "/" ) ||
path . endsWith ( "/." ) ||
path . endsWith ( "/.." ) ||
path === "." ||
path === ".." ;
2026-10-10 03:11:08 +08:00
const htmlBaseDirectory = ( base : string , basePath : string ) : string =>
2026-10-13 06:24:45 +08:00
urlPathIsDirectory ( base ) ? basePath : posix.dirname ( basePath );
2026-10-05 13:02:56 -04:00
2026-10-10 21:26:02 +08:00
const confineCandidate = ( candidate : string ) : string | CandidateResolution => {
2026-10-10 17:14:47 +08:00
const normalized = normalizeJoinedLogicalPath ( candidate );
if ( logicalPathEscapesRoot ( normalized ))
2026-10-10 21:26:02 +08:00
return unresolvedCandidate ( "rejected" , [
2026-07-17 21:30:24 +08:00
"The resolved candidate escapes the canonical artifact root." ,
]);
return normalized === "." ? "" : normalized ;
};
2026-10-10 21:26:02 +08:00
type PackageMetadata =
| {
readonly kind : "available" ;
readonly path : string ;
2026-10-11 18:57:58 +08:00
readonly name : string | undefined ;
readonly exports : string | object | undefined ;
2026-10-11 10:17:58 -07:00
readonly imports : object | undefined ;
2026-10-11 18:57:58 +08:00
readonly main : unknown ;
readonly module : unknown ;
2026-10-10 21:26:02 +08:00
}
| { readonly kind : "absent" }
| { readonly kind : "unavailable" ; readonly limitation : string };
const readPackageMetadata = (
2026-07-17 21:30:24 +08:00
input : ResolveArtifactPathInput ,
2026-10-10 21:26:02 +08:00
directory : string ,
) : PackageMetadata => {
const path = posix . join ( directory , "package.json" );
const file = input . files . get ( path );
2026-07-17 21:30:24 +08:00
const source = input . files . get ( input . sourcePath );
if (
2026-10-10 21:26:02 +08:00
file === undefined ||
( source !== undefined && file . container_sha256 !== source . container_sha256 )
2026-07-17 21:30:24 +08:00
)
2026-10-10 21:26:02 +08:00
return { kind : "absent" };
if ( ! file . text . included )
2026-10-08 02:00:34 -04:00
return {
2026-10-10 21:26:02 +08:00
kind : "unavailable" ,
limitation : `Directory package metadata ${ path } was inventoried but its text is unavailable: ${ file . text . reason } .` ,
2026-10-08 02:00:34 -04:00
};
2026-10-10 21:26:02 +08:00
let value : unknown ;
try {
value = JSON . parse ( file . text . value );
} catch ( cause : unknown ) {
void cause ;
2026-07-17 21:30:24 +08:00
return {
2026-10-10 21:26:02 +08:00
kind : "unavailable" ,
limitation : `Directory package metadata ${ path } is not valid package JSON.` ,
2026-07-17 21:30:24 +08:00
};
2026-10-05 07:34:48 -07:00
}
2026-10-11 18:57:58 +08:00
if ( typeof value !== "object" || value === null )
return {
kind : "unavailable" ,
limitation : `Directory package metadata ${ path } is not valid package JSON.` ,
};
const name = Reflect . get ( value , "name" );
const exports = Reflect . get ( value , "exports" );
2026-10-11 10:17:58 -07:00
const imports = Reflect . get ( value , "imports" );
2026-10-14 08:46:50 +08:00
const main = Reflect . get ( value , "main" );
2026-10-11 18:57:58 +08:00
return {
kind : "available" ,
path ,
name : typeof name === "string" ? name : undefined ,
// Node's package reader ignores scalar root exports other than strings.
// Nested targets retain their raw values for exports validation.
exports :
typeof exports === "string" ||
( typeof exports === "object" && exports !== null )
? exports
: undefined ,
2026-10-11 10:17:58 -07:00
// Import maps must be non-array objects; other shapes are not a map.
imports :
typeof imports === "object" && imports !== null && ! Array . isArray ( imports )
? imports
: undefined ,
2026-10-14 08:46:50 +08:00
main :
input.moduleKind !== undefined && typeof main !== "string"
? undefined
: main ,
2026-10-11 18:57:58 +08:00
module : Reflect.get ( value , "module" ),
};
2026-10-10 21:26:02 +08:00
};
/** File and directory lookup never interprets a nested directory's exports. */
const resolveCandidate = (
input : ResolveArtifactPathInput ,
candidate : string ,
packageChain : ReadonlySet < string >,
2026-10-13 15:58:25 +08:00
emittedSourceCandidates : readonly TypeScriptEmittedSourceCandidate [] = [],
2026-10-13 21:01:30 +05:30
) : CandidateResolution => {
if ( ! commonJsDirectoryRequest ( input )) {
const file =
resolveFileCandidates (
input ,
fileCandidates ( candidate , input . moduleKind ),
) ??
resolveRuntimeSourceCandidates ( input , candidate , emittedSourceCandidates );
if ( file !== null ) return file ;
}
return resolveDirectory (
2026-10-10 21:26:02 +08:00
input ,
candidate ,
readPackageMetadata ( input , candidate ),
packageChain ,
);
2026-10-13 21:01:30 +05:30
};
const commonJsDirectoryRequest = ( input : ResolveArtifactPathInput ) : boolean =>
input . context === "module-specifier" &&
input . moduleKind === "require" &&
( input . declaredPath . endsWith ( "/" ) ||
/(?:^|\/)\.{1,2}$/u.test(input.declaredPath));
2026-10-10 21:26:02 +08:00
const resolveDirectory = (
input : ResolveArtifactPathInput ,
directory : string ,
metadata : PackageMetadata ,
packageChain : ReadonlySet < string >,
) : CandidateResolution => {
if ( metadata . kind === "absent" )
return (
2026-10-12 11:10:32 +08:00
resolveFileCandidates (
input ,
indexCandidates ( directory , input . moduleKind ),
) ?? notFoundCandidate ()
2026-10-10 21:26:02 +08:00
);
if ( metadata . kind === "unavailable" )
return unresolvedCandidate ( "unavailable" , [ metadata . limitation ]);
if ( packageChain . has ( metadata . path ))
return unresolvedCandidate ( "unavailable" , [
`Directory package entrypoint cycle includes ${ metadata . path } .` ,
]);
2026-10-12 11:10:32 +08:00
const entry =
metadata . main === ""
? undefined
: metadata . main !== undefined
? metadata.main
: input.moduleKind === undefined
? metadata.module
: undefined ;
2026-10-10 21:26:02 +08:00
if ( entry === undefined )
2026-10-05 07:34:48 -07:00
return (
2026-10-12 11:10:32 +08:00
resolveFileCandidates (
input ,
indexCandidates ( directory , input . moduleKind ),
) ?? notFoundCandidate ()
2026-10-05 07:34:48 -07:00
);
2026-10-10 21:26:02 +08:00
if ( typeof entry !== "string" || entry . length === 0 )
return unresolvedCandidate ( "unavailable" , [
`Directory package metadata ${ metadata . path } is not valid package JSON.` ,
]);
2026-10-05 07:34:48 -07:00
const entryInput : ResolveArtifactPathInput = {
2026-10-10 21:26:02 +08:00
declaredPath : entry ,
sourcePath : metadata.path ,
2026-10-05 07:34:48 -07:00
context : "package-entrypoint" ,
files : input.files ,
2026-10-14 08:46:50 +08:00
...( input . directoryPaths === undefined
? {}
: { directoryPaths : input.directoryPaths }),
...( input . unanalyzedFiles === undefined
? {}
: { unanalyzedFiles : input.unanalyzedFiles }),
2026-10-10 21:26:02 +08:00
...( input . moduleKind === undefined ? {} : { moduleKind : input.moduleKind }),
2026-10-05 07:34:48 -07:00
};
2026-10-10 21:26:02 +08:00
const candidate = contextualCandidate ( entryInput );
if ( typeof candidate !== "string" ) return candidate ;
const confined = confineCandidate ( candidate );
if ( typeof confined !== "string" ) return confined ;
2026-10-14 08:46:50 +08:00
if ( input . moduleKind !== undefined )
return resolveNodePackageMain ( entryInput , directory , confined );
2026-10-10 21:26:02 +08:00
const selected =
resolveFileCandidates ( entryInput , [
2026-10-12 11:10:32 +08:00
... fileCandidates ( confined , entryInput . moduleKind ),
... indexCandidates ( confined , entryInput . moduleKind ),
]) ??
resolveFileCandidates ( input , indexCandidates ( directory , input . moduleKind ));
2026-10-10 21:26:02 +08:00
if ( selected !== null ) return selected ;
return resolveCandidate (
entryInput ,
confined ,
new Set ([... packageChain , metadata . path ]),
2026-10-08 02:00:34 -04:00
);
2026-10-05 07:34:48 -07:00
};
2026-10-14 08:46:50 +08:00
const resolveNodePackageMain = (
input : ResolveArtifactPathInput ,
directory : string ,
mainPath : string ,
) : CandidateResolution => {
const normalizedMain = mainPath . replace ( /\/$/u, "");
for ( const suffix of NODE_PACKAGE_MAIN_SUFFIXES ) {
const isDirectoryIndex = suffix . startsWith ( "/" );
if ( normalizedMain === "" && ! isDirectoryIndex ) continue ;
const selected = resolveFileCandidates ( input , [
isDirectoryIndex
? posix . join ( normalizedMain , suffix . slice ( 1 ))
: posix . normalize ( ` ${ normalizedMain }${ suffix } ` ),
]);
if ( selected === null ) continue ;
if ( input . moduleKind === "require" ) return selected ;
// ESM probes the normalized main, then appends the selected suffix to
// the original declaration before finalizing the resulting file path.
const finalized = confineCandidate (
relativeCandidate ( input . sourcePath , ` ${ input . declaredPath }${ suffix } ` ),
);
if ( typeof finalized !== "string" ) return finalized ;
const finalizedFile = resolveFileCandidates ( input , [
finalized . replace ( /\/$/u, ""),
]);
if ( finalizedFile !== null ) return finalizedFile ;
const source = input . files . get ( input . sourcePath );
if (
hasContainerDirectory (
input . files ,
finalized ,
source ? . container_sha256 ,
input . directoryPaths ,
input . unanalyzedFiles ,
)
)
return {
... unresolvedCandidate ( "rejected" , [
`The selected legacy ESM package main finalizes to directory ${ finalized } ; directory imports are unsupported and no later main or index candidate was tried.` ,
]),
candidatePath : finalized ,
};
return unresolvedCandidate ( "not-found" , [
`The selected legacy ESM package main finalizes to ${ finalized } , which is not an inventoried file; no later main or index candidate was tried.` ,
]);
}
const fallback = resolveFileCandidates (
input ,
indexCandidates ( directory , input . moduleKind ),
);
if ( fallback !== null ) return fallback ;
return input . moduleKind === "require"
? { ... notFoundCandidate (), stopsAncestorLookup : true }
: notFoundCandidate ();
};
2026-10-05 07:34:48 -07:00
const resolveFileCandidates = (
input : ResolveArtifactPathInput ,
candidates : readonly string [],
) : CandidateResolution | null => {
for ( const path of candidates ) {
2026-10-14 08:46:50 +08:00
const selected = findFileCandidate ( input , path );
if ( selected === null ) continue ;
return selected . status === "resolved" &&
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.` ,
],
}
: selected ;
2026-10-05 07:34:48 -07:00
}
return null ;
2026-07-17 21:30:24 +08:00
};
2026-10-14 08:46:50 +08:00
/** Own candidate presence and container identity independently of loader policy. */
const findFileCandidate = (
input : ResolveArtifactPathInput ,
path : string ,
) : CandidateResolution | null => {
const source = input . files . get ( input . sourcePath );
const target = input . files . get ( path );
if (
target !== undefined &&
( source === undefined ||
target . container_sha256 === source . container_sha256 )
)
return { resolvedPath : path , status : "resolved" , limitations : [] };
const unanalyzed = input . unanalyzedFiles ? . get ( path );
if (
unanalyzed !== undefined &&
( source === undefined ||
unanalyzed . container_sha256 === source . container_sha256 )
)
return {
resolvedPath : null ,
status : "unavailable" ,
candidatePath : path ,
limitations : [
...( unanalyzed . limitations ?? []),
` ${ path } exists in the inventoried artifact container (sha256 ${ unanalyzed . sha256 ?? "unknown" } , ${ unanalyzed . bytes === null ? "unknown size" : ` ${ String ( unanalyzed . bytes ) } bytes` } ) but is not a JavaScript, JSON, HTML, source map, or native add-on file; its contents were not analyzed.` ,
],
};
return null ;
};
2026-10-13 15:58:25 +08:00
const isRuntimeDeclarationPath = (
input : ResolveArtifactPathInput ,
path : string ,
) : boolean =>
input . moduleKind !== undefined && /\.d\.(?:ts|mts|cts)$/iu.test(path);
2026-10-12 11:10:32 +08:00
const NODE_MODULE_EXTENSIONS = [ ".js" , ".json" , ".node" ] as const ;
2026-10-14 08:46:50 +08:00
const NODE_PACKAGE_MAIN_SUFFIXES = [
"" ,
... NODE_MODULE_EXTENSIONS ,
... NODE_MODULE_EXTENSIONS . map (( extension ) => `/index ${ extension } ` ),
];
2026-10-12 11:10:32 +08:00
const fileCandidates = (
candidate : string ,
moduleKind? : ResolveArtifactPathInput [ "moduleKind" ],
) : readonly string [] => [
2026-07-17 21:30:24 +08:00
candidate ,
2026-10-12 11:10:32 +08:00
...( moduleKind === undefined ? EXTENSIONS : NODE_MODULE_EXTENSIONS ). map (
( extension ) => ` ${ candidate }${ extension } ` ,
),
2026-07-17 21:30:24 +08:00
];
2026-10-12 11:10:32 +08:00
const indexCandidates = (
candidate : string ,
moduleKind? : ResolveArtifactPathInput [ "moduleKind" ],
) : readonly string [] =>
( moduleKind === undefined ? EXTENSIONS : NODE_MODULE_EXTENSIONS ). map (
( extension ) => posix . join ( candidate , `index ${ extension } ` ),
);
2026-10-05 07:34:48 -07:00
2026-10-11 10:17:58 -07:00
type PackageTargetBlockCause = "null" | "empty-array" ;
2026-10-10 21:26:02 +08:00
type PackageExportOutcome =
2026-10-12 01:36:42 +08:00
| ({ readonly status : "value" } & PackageFileTarget )
2026-10-08 02:00:34 -04:00
| { readonly status : "rejected" ; readonly limitation : string }
2026-10-11 10:17:58 -07:00
| { readonly status : "blocked" ; readonly cause : PackageTargetBlockCause }
2026-10-10 21:26:02 +08:00
| { readonly status : "unmatched" ; readonly declared : readonly string [] };
2026-07-17 21:30:24 +08:00
2026-10-10 21:26:02 +08:00
/** Only exact non-directory keys participate; wildcard expansion is unsupported. */
const selectPackageExport = (
2026-07-17 23:57:23 +08:00
value : unknown ,
2026-10-10 21:26:02 +08:00
subpath : string ,
2026-07-17 23:57:23 +08:00
moduleKind : ResolveArtifactPathInput [ "moduleKind" ],
2026-10-12 01:36:42 +08:00
packagePath : string ,
2026-10-05 13:03:18 -04:00
) : PackageExportOutcome => {
2026-10-10 21:26:02 +08:00
let target = value ;
let declared : string [] = [];
2026-10-10 00:44:29 +08:00
if ( typeof value === "object" && value !== null && ! Array . isArray ( value )) {
const keys = Object . keys ( value );
2026-10-10 21:26:02 +08:00
const hasSubpaths = keys . some (( key ) => key . startsWith ( "." ));
if ( hasSubpaths && keys . some (( key ) => ! key . startsWith ( "." )))
2026-10-10 00:44:29 +08:00
return {
status : "rejected" ,
limitation :
"The package exports configuration mixes subpath keys and condition keys." ,
};
2026-10-10 21:26:02 +08:00
declared = keys ;
if ( hasSubpaths ) {
if (
subpath . endsWith ( "/" ) ||
subpath . includes ( "*" ) ||
! Object . hasOwn ( value , subpath )
)
return { status : "unmatched" , declared };
target = Reflect . get ( value , subpath );
} else if ( subpath !== "." ) return { status : "unmatched" , declared };
} else if ( subpath !== "." ) return { status : "unmatched" , declared };
const selected = selectExportTarget (
target ,
packageExportConditions ( moduleKind ),
2026-10-14 08:46:50 +08:00
packagePath ,
2026-10-10 21:26:02 +08:00
);
if ( selected . kind === "invalid" || selected . kind === "invalid-config" )
return { status : "rejected" , limitation : selected.limitation };
if ( selected . kind === "unmatched" )
return {
status : "unmatched" ,
2026-10-11 10:17:58 -07:00
declared : selected.declared ,
};
if ( selected . kind === "blocked" )
return { status : "blocked" , cause : selected.cause };
if ( selected . kind === "non-relative" )
return {
status : "rejected" ,
limitation : `The package exports target ${ JSON . stringify ( selected . value ) } must start with "./".` ,
2026-10-10 21:26:02 +08:00
};
2026-10-12 01:36:42 +08:00
return {
status : "value" ,
value : selected.value ,
url : selected.url ,
2026-10-14 08:46:50 +08:00
...( selected . lookupLimitation === undefined
? {}
: {
lookupLimitation : selected.lookupLimitation ,
}),
2026-10-12 01:36:42 +08:00
};
2026-07-17 23:57:23 +08:00
};
2026-10-10 21:26:02 +08:00
type ExportTargetOutcome =
2026-10-12 01:36:42 +08:00
| ({ readonly kind : "value" } & PackageFileTarget )
2026-10-11 10:17:58 -07:00
| { readonly kind : "non-relative" ; readonly value : string }
| { readonly kind : "blocked" ; readonly cause : PackageTargetBlockCause }
| { readonly kind : "unmatched" ; readonly declared : readonly string [] }
2026-10-10 00:44:29 +08:00
| { readonly kind : "invalid" ; readonly limitation : string }
| { readonly kind : "invalid-config" ; readonly limitation : string };
2026-10-08 02:00:34 -04:00
const invalidExportTarget = (
value : unknown ,
reason : string ,
2026-10-11 10:17:58 -07:00
mapKind : "exports" | "imports" = "exports" ,
2026-10-10 21:26:02 +08:00
) : ExportTargetOutcome => ({
2026-10-08 02:00:34 -04:00
kind : "invalid" ,
2026-10-11 10:17:58 -07:00
limitation : `The package ${ mapKind === "imports" ? "import" : "exports" } target ${ JSON . stringify ( value ) ?? String ( value ) } ${ reason } .` ,
2026-10-08 02:00:34 -04:00
});
// Node checks raw target segments before URL normalization. Compare encoded
// segment names here; full URL decoding follows array target selection.
const forbiddenExportSegment = ( value : string ) : string | undefined =>
value
. slice ( 2 )
. split ( /[\\/]/u)
. find (( raw ) => {
const segment = raw . replace ( /%[0-9a-f]{2}/giu, (encoded) =>
String . fromCharCode ( Number . parseInt ( encoded . slice ( 1 ), 16 )),
);
return (
segment === "." ||
segment === ".." ||
segment . toLowerCase () === "node_modules"
);
});
2026-10-05 13:03:18 -04:00
2026-10-11 10:17:58 -07:00
const selectStringPackageTarget = (
value : string ,
2026-10-14 08:46:50 +08:00
packagePath : string ,
2026-10-11 10:17:58 -07:00
mapKind : "exports" | "imports" ,
) : ExportTargetOutcome => {
if ( mapKind === "imports" ) {
const target = classifyPackageImportTargetString ( value );
if ( target . kind === "invalid" )
return invalidExportTarget (
value ,
"cannot be parent-relative, absolute, or a URL" ,
mapKind ,
);
if ( target . kind !== "relative" ) return { kind : "non-relative" , value };
} else if ( ! value . startsWith ( "./" ))
return invalidExportTarget ( value , 'must start with "./"' , mapKind );
const forbidden = forbiddenExportSegment ( value );
2026-10-12 01:36:42 +08:00
if ( forbidden !== undefined )
return invalidExportTarget (
value ,
`contains forbidden path segment ${ JSON . stringify ( forbidden ) } before URL normalization` ,
mapKind ,
);
2026-10-14 08:46:50 +08:00
const metadataUrl = packageMetadataUrl ( packagePath , value );
const packageUrl = metadataUrl . ok
? metadataUrl.value
: metadataUrl.error.structuralUrl ;
2026-10-12 01:36:42 +08:00
// Node constructs the URL before deciding whether this array entry is an
// invalid package target. Tabs/newlines and dot segments normalize here;
// percent decoding and existence checks belong after target selection.
const url = new URL ( value , packageUrl );
if ( ! url . pathname . startsWith ( new URL ( "." , packageUrl ). pathname ))
return invalidExportTarget (
value ,
"escapes the package root after URL normalization" ,
mapKind ,
);
return {
kind : "value" ,
value ,
url ,
2026-10-14 08:46:50 +08:00
...( metadataUrl . ok
? {}
: {
lookupLimitation : metadataUrl.error.limitation ,
}),
2026-10-12 01:36:42 +08:00
};
2026-10-11 10:17:58 -07:00
};
const selectArrayPackageTarget = (
value : readonly unknown [],
conditions : ReadonlySet < string >,
2026-10-14 08:46:50 +08:00
packagePath : string ,
2026-10-11 10:17:58 -07:00
mapKind : "exports" | "imports" ,
) : ExportTargetOutcome => {
if ( value . length === 0 ) return { kind : "blocked" , cause : "empty-array" };
let lastRefusal :
| Extract < ExportTargetOutcome , { readonly kind : "invalid" | "blocked" }>
| undefined ;
const declared = new Set < string >();
for ( const entry of value ) {
2026-10-14 08:46:50 +08:00
const nested = selectExportTarget ( entry , conditions , packagePath , mapKind );
2026-10-11 10:17:58 -07:00
// Node's array fallback catches invalid targets, not invalid configuration.
if ( nested . kind === "invalid-config" ) return nested ;
if ( nested . kind === "unmatched" ) {
for ( const condition of nested . declared ) declared . add ( condition );
continue ;
}
if ( nested . kind === "invalid" || nested . kind === "blocked" ) {
lastRefusal = nested ;
continue ;
}
return nested ;
}
return lastRefusal ?? { kind : "unmatched" , declared : [... declared ] };
};
2026-10-05 13:03:18 -04:00
/**
2026-10-05 14:09:12 -04:00
* Resolve a target in Node's declared order. Explicit null blocks an active
* condition, whereas an unmatched nested object permits the next condition.
* An array skips unmatched and invalid entries, stops at its first string, and
2026-10-11 10:17:58 -07:00
* preserves the last invalid/null outcome if no string is selected. A nonempty
* array of unmatched conditions stays unmatched; an empty array blocks.
2026-10-05 13:03:18 -04:00
*/
2026-10-10 21:26:02 +08:00
const selectExportTarget = (
2026-10-05 13:03:18 -04:00
value : unknown ,
conditions : ReadonlySet < string >,
2026-10-14 08:46:50 +08:00
packagePath : string ,
2026-10-11 10:17:58 -07:00
mapKind : "exports" | "imports" = "exports" ,
2026-10-10 21:26:02 +08:00
) : ExportTargetOutcome => {
2026-10-11 10:17:58 -07:00
if ( typeof value === "string" )
2026-10-14 08:46:50 +08:00
return selectStringPackageTarget ( value , packagePath , mapKind );
2026-10-11 10:17:58 -07:00
if ( value === null ) return { kind : "blocked" , cause : "null" };
if ( Array . isArray ( value ))
2026-10-14 08:46:50 +08:00
return selectArrayPackageTarget ( value , conditions , packagePath , mapKind );
2026-10-08 02:00:34 -04:00
if ( typeof value !== "object" )
return invalidExportTarget (
value ,
"must be a relative string, object, array, or null" ,
2026-10-11 10:17:58 -07:00
mapKind ,
2026-10-08 02:00:34 -04:00
);
2026-10-10 00:44:29 +08:00
for ( const key of Object . keys ( value )) {
const numeric = Number ( key );
if ( String ( numeric ) === key && numeric >= 0 && numeric < 0xffff _ffff )
return {
kind : "invalid-config" ,
limitation : `The package exports configuration contains numeric condition key ${ JSON . stringify ( key ) } .` ,
};
}
2026-10-11 10:17:58 -07:00
const declared = new Set < string >();
2026-10-05 13:03:18 -04:00
for ( const [ condition , target ] of Object . entries ( value )) {
2026-10-11 10:17:58 -07:00
declared . add ( condition );
2026-10-05 13:03:18 -04:00
if ( ! conditions . has ( condition )) continue ;
2026-10-14 08:46:50 +08:00
const nested = selectExportTarget ( target , conditions , packagePath , mapKind );
2026-10-05 14:09:12 -04:00
if ( nested . kind !== "unmatched" ) return nested ;
2026-10-11 10:17:58 -07:00
for ( const name of nested . declared ) declared . add ( name );
2026-10-05 13:03:18 -04:00
}
2026-10-11 10:17:58 -07:00
return { kind : "unmatched" , declared : [... declared ] };
2026-10-05 13:03:18 -04:00
};
/**
* Node selects an exports target by walking the declared keys in order and
* taking the first whose condition is active for the calling resolver.
* "default" is always active, and "node" plus "node-addons" are active for the
2026-10-09 17:49:43 -05:00
* built-in resolver that owns installed-package imports and requires. The
* supported Node runtimes also activate "module-sync" for both loaders.
2026-10-05 13:03:18 -04:00
*/
const packageExportConditions = (
moduleKind : ResolveArtifactPathInput [ "moduleKind" ],
) : ReadonlySet < string > =>
new Set ([
"node" ,
"node-addons" ,
2026-10-09 17:49:43 -05:00
"module-sync" ,
2026-10-05 13:03:18 -04:00
...( moduleKind === undefined ? [] : [ moduleKind ]),
"default" ,
]);
2026-10-12 11:26:48 +08:00
const notFoundCandidate = () : Extract <
CandidateResolution ,
{ readonly resolvedPath : null }
> => ({
2026-07-17 21:30:24 +08:00
resolvedPath : null ,
status : "not-found" ,
limitations : [
2026-09-29 15:30:43 +08:00
"No extension, directory package, or index candidate exists in the inventoried artifact container." ,
2026-07-17 21:30:24 +08:00
],
});
2026-10-11 10:17:58 -07:00
const exactPackageTargetNotFound = (
path : string ,
2026-10-12 04:09:17 +08:00
targetKind : "exports" | "imports" | "ESM subpath" ,
2026-10-11 10:17:58 -07:00
) : CandidateResolution => ({
2026-10-08 20:49:32 -07:00
resolvedPath : null ,
status : "not-found" ,
limitations : [
2026-10-12 04:09:17 +08:00
`The selected package ${ targetKind === "imports" ? "import" : targetKind } target ${ path } does not exist in the inventoried artifact container; ${ targetKind === "imports" ? "import" : targetKind } targets are exact files, so no extension or directory index was tried.` ,
2026-10-08 20:49:32 -07:00
],
});
2026-10-13 07:35:12 +08:00
const fileUrlPath = ( value : string ) : string | null | undefined => {
if ( ! /^file:\/\//iu.test(value)) return undefined;
// URL parsing removes tabs and newlines and normalizes encoded dot segments.
// Check the raw path first, excluding URL identity suffixes, then check the
// parsed pathname so parser normalization cannot introduce a separator.
const rawPath = stripQueryAndFragment ( value );
if (
value . includes ( "\0" ) ||
value . includes ( "\\" ) ||
/[\t\n\r]/u.test(rawPath) ||
! admitsCanonicalPathSyntax ( rawPath )
)
return null ;
2026-07-17 21:30:24 +08:00
try {
const url = new URL ( value );
if ( url . protocol !== "file:" || url . hostname !== "" ) return undefined ;
2026-10-13 07:35:12 +08:00
if ( ! admitsCanonicalPathSyntax ( url . pathname )) return null ;
const decoded = decodeURIComponent ( url . pathname );
if ( decoded . includes ( "\0" )) return null ;
return decoded . replace ( /^\/+/ , "" );
2026-10-06 05:58:46 +08:00
} catch ( cause : unknown ) {
// Unparseable file URLs have no path to resolve.
void cause ;
2026-07-17 21:30:24 +08:00
return undefined ;
}
};
2026-10-10 21:26:02 +08:00
const unresolvedCandidate = (
2026-08-10 14:57:44 +08:00
status : UnresolvedArtifactPathStatus ,
2026-07-17 21:30:24 +08:00
limitations : readonly string [],
2026-10-10 21:26:02 +08:00
) : CandidateResolution => ({ resolvedPath : null , status , limitations });