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-07 17:57:33 +08:00
import type { JavaScriptArtifactFile } 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-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 >;
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/" );
const packageMetadataUrl = ( path : string ) : URL =>
new URL (
path . split ( "/" ). map ( encodeURIComponent ). join ( "/" ),
PACKAGE_ARTIFACT_ROOT_URL ,
);
interface PackageFileTarget {
readonly value : string ;
/** Normalized URL retained until loader-specific validation and file lookup. */
readonly url : URL ;
}
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.
return resolveCandidate ( runtimeInput , confined , new Set ());
};
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 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 ;
};
2026-10-10 21:26:02 +08:00
const rejectDeclaration = ( declared : string ) : CandidateResolution | null => {
2026-07-17 21:30:24 +08:00
if ( declared . length === 0 )
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-05 13:02:56 -04:00
if ( ! 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 ;
};
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-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." ,
]);
}
const rejected = rejectDeclaration ( declared );
if ( rejected !== null ) return rejected ;
if ( context === "html-reference" ) return htmlCandidate ( input );
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-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" ||
hasContainerDirectory ( input . files , candidate , source ? . container_sha256 )
? 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 ),
packageMetadataUrl ( metadata . path ),
"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-12 04:09:17 +08:00
// Resolve legacy ESM subpaths as URLs: suffixes identify the loaded module
// but do not belong to its filesystem path. Keep the declared specifier on
// the relationship; only derive the inventory lookup path here.
const fileSubpath =
input . moduleKind === "import"
? decodeModuleUrlPath ( stripQueryAndFragment ( subpath . slice ( 2 )))
: subpath . slice ( 2 );
if ( typeof fileSubpath !== "string" ) return fileSubpath ;
const candidate = confineCandidate ( ` ${ directory } / ${ fileSubpath } ` );
if ( input . moduleKind === "import" && typeof candidate === "string" )
return resolveLegacyEsmSubpath ( input , candidate );
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 ,
candidate : string ,
) : CandidateResolution => {
const file = resolveFileCandidates ( input , [ candidate ]);
if ( file !== null ) return file ;
const source = input . files . get ( input . sourcePath );
if ( hasContainerDirectory ( input . files , candidate , source ? . container_sha256 ))
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 ,
);
const rejected = rejectDeclaration ( encodedPath );
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-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-10 21:26:02 +08:00
) : boolean => {
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 ;
return false ;
};
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-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 14:18:39 +00:00
// Removing URL tabs and newlines can join an encoded dot or separator that
// the raw declaration split, so admit the parsed URL text again.
if ( ! admitsCanonicalPathSyntax ( declared ))
2026-10-10 21:26:02 +08:00
return unresolvedCandidate ( "rejected" , [
2026-10-09 14:18:39 +00:00
"Encoded dot or separator bytes are rejected before artifact path resolution." ,
]);
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-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-11 18:57:58 +08:00
main : Reflect.get ( value , "main" ),
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-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 ;
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 ;
2026-10-12 11:26:48 +08:00
if ( input . moduleKind === "require" )
return { ... notFoundCandidate (), stopsAncestorLookup : true };
2026-10-12 11:10:32 +08:00
if ( input . moduleKind !== undefined ) return notFoundCandidate ();
2026-10-10 21:26:02 +08:00
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
};
const resolveFileCandidates = (
input : ResolveArtifactPathInput ,
candidates : readonly string [],
) : CandidateResolution | null => {
const source = input . files . get ( input . sourcePath );
for ( const path of candidates ) {
const target = input . files . get ( path );
if (
target !== undefined &&
( source === undefined ||
target . container_sha256 === source . container_sha256 )
)
2026-10-13 15:58:25 +08:00
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 : [] };
2026-10-05 07:34:48 -07:00
}
return null ;
2026-07-17 21:30:24 +08:00
};
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 ;
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-12 01:36:42 +08:00
packageMetadataUrl ( 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-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-12 01:36:42 +08:00
packageUrl : URL ,
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 ,
);
// 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-11 10:17:58 -07:00
};
const selectArrayPackageTarget = (
value : readonly unknown [],
conditions : ReadonlySet < string >,
2026-10-12 01:36:42 +08:00
packageUrl : URL ,
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-12 01:36:42 +08:00
const nested = selectExportTarget ( entry , conditions , packageUrl , 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-12 01:36:42 +08:00
packageUrl : URL ,
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-12 01:36:42 +08:00
return selectStringPackageTarget ( value , packageUrl , mapKind );
2026-10-11 10:17:58 -07:00
if ( value === null ) return { kind : "blocked" , cause : "null" };
if ( Array . isArray ( value ))
2026-10-12 01:36:42 +08:00
return selectArrayPackageTarget ( value , conditions , packageUrl , 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-12 01:36:42 +08:00
const nested = selectExportTarget ( target , conditions , packageUrl , 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 });