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-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-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-07-17 23:57:23 +08:00
readonly moduleKind ?: "import" | "require" ;
2026-07-17 21:30:24 +08:00
}
const EXTENSIONS = [
".js" ,
".cjs" ,
".mjs" ,
".ts" ,
".tsx" ,
".json" ,
".html" ,
".node" ,
];
2026-08-10 14:57:44 +08:00
type CandidateResolution =
| {
readonly resolvedPath : string ;
readonly status : "resolved" ;
readonly limitations : readonly string [];
}
| {
readonly resolvedPath : null ;
readonly status : UnresolvedArtifactPathStatus ;
readonly limitations : readonly string [];
};
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 );
return resolution . status === "resolved"
? {
declared_path : input.declaredPath ,
resolution_context : input.context ,
resolved_path : resolution.resolvedPath ,
resolution_status : resolution.status ,
limitations : resolution.limitations ,
}
: {
declared_path : input.declaredPath ,
resolution_context : input.context ,
resolved_path : null ,
resolution_status : resolution.status ,
limitations : resolution.limitations ,
};
};
const resolvePath = ( input : ResolveArtifactPathInput ) : CandidateResolution => {
const rejected = rejectDeclaration ( input . declaredPath );
2026-07-17 21:30:24 +08:00
if ( rejected !== null ) return rejected ;
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-08 01:24:24 -04:00
if ( input . context === "html-reference" )
2026-10-10 21:26:02 +08:00
return (
resolveFileCandidates ( input , [ confined ]) ??
unresolvedCandidate ( "not-found" , [
`The exact HTML resource ${ confined } was not found among the selected application files.` ,
])
2026-10-08 01:24:24 -04:00
);
2026-10-10 21:26:02 +08:00
return resolveCandidate ( input , confined , new Set ());
2026-07-17 21:30:24 +08:00
};
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-08 02:00:34 -04:00
let declared =
2026-10-05 07:28:51 -07:00
context === "html-reference" ||
2026-10-07 16:34:15 +05:30
context === "url-reference" ||
2026-10-05 07:28:51 -07:00
( context === "module-specifier" && input . moduleKind !== "require" )
2026-07-17 21:30:24 +08:00
? stripQueryAndFragment ( input . declaredPath )
: input . declaredPath ;
if ( context === "html-reference" ) return htmlCandidate ( input );
if ( context === "module-specifier" ) {
const fileUrl = fileUrlPath ( declared );
if ( fileUrl !== undefined ) return fileUrl ;
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." ,
]);
if ( ! declared . startsWith ( "." ) && ! declared . startsWith ( "/" ))
2026-07-17 23:57:23 +08:00
return bareModuleCandidate ( input , declared );
2026-10-10 21:26:02 +08:00
if ( input . moduleKind !== "require" ) {
const decoded = decodeModuleUrlPath ( declared );
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-10 21:26:02 +08:00
return relativeCandidate ( input . sourcePath , declared );
};
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 ,
declared : string ,
2026-10-10 21:26:02 +08:00
) : CandidateResolution => {
2026-07-17 23:57:23 +08:00
const packageName = barePackageName ( declared );
2026-10-10 00:43:33 +08:00
if ( packageName === null || isBuiltin ( declared ) || declared . startsWith ( "#" ))
2026-10-10 21:26:02 +08:00
return unresolvedCandidate ( "external" , [
2026-07-17 23:57:23 +08:00
"The bare specifier is a Node builtin, package import map, or invalid package name." ,
]);
2026-10-10 21:26:02 +08:00
const subpath =
declared === packageName ? "." : `. ${ declared . slice ( packageName . length ) } ` ;
2026-07-17 23:57:23 +08:00
const source = input . files . get ( input . sourcePath );
let directory = posix . dirname ( input . sourcePath );
while ( true ) {
2026-10-10 21:26:02 +08:00
const candidate = posix . join ( directory , "node_modules" , packageName );
if ( subpath === "." ) {
const direct = resolveFileCandidates ( input , fileCandidates ( candidate ));
if ( direct !== null ) return direct ;
}
if ( hasContainerDirectory ( input . files , candidate , source ? . container_sha256 ))
return resolvePackageSpecifier ( input , candidate , subpath );
2026-07-17 23:57:23 +08:00
if ( directory === "." || directory === "" ) break ;
directory = posix . dirname ( directory );
}
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-10 21:26:02 +08:00
/** Package roots and exact subpaths share metadata and exports selection. */
const resolvePackageSpecifier = (
input : ResolveArtifactPathInput ,
directory : string ,
subpath : string ,
) : CandidateResolution => {
const metadata = readPackageMetadata ( input , directory );
if ( metadata . kind === "unavailable" )
return unresolvedCandidate ( "unavailable" , [ metadata . limitation ]);
const rawExports =
metadata . kind === "available"
? Reflect . get ( metadata . value , "exports" )
: undefined ;
if ( rawExports === undefined || rawExports === null ) {
if ( subpath === "." )
return resolveDirectory ( input , directory , metadata , new Set ());
const candidate = confineCandidate ( posix . join ( directory , subpath . slice ( 2 )));
return typeof candidate === "string"
? resolveCandidate ( input , candidate , new Set ())
: candidate ;
}
const exported = selectPackageExport ( rawExports , subpath , input . moduleKind );
const packagePath = posix . join ( directory , "package.json" );
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" , [
`Directory package metadata ${ packagePath } blocks subpath ${ subpath } with an explicit null exports target.` ,
]);
return resolveExportTarget ( input , packagePath , exported . value );
};
/** An exports target is a URL naming one exact file for both Node loaders. */
const resolveExportTarget = (
input : ResolveArtifactPathInput ,
packagePath : string ,
target : string ,
) : CandidateResolution => {
const rejected = rejectDeclaration ( target );
if ( rejected !== null ) return rejected ;
const decoded = decodeModuleUrlPath ( stripQueryAndFragment ( target ));
if ( typeof decoded !== "string" ) return decoded ;
const candidate = confineCandidate ( relativeCandidate ( packagePath , decoded ));
if ( typeof candidate !== "string" ) return candidate ;
return (
resolveFileCandidates ( input , [ candidate ]) ??
exactExportsTargetNotFound ( candidate )
);
};
2026-07-17 23:57:23 +08:00
const barePackageName = ( specifier : string ) : string | null => {
const segments = specifier . split ( "/" );
if ( specifier . startsWith ( "@" ))
return segments . length >= 2 && segments [ 0 ] !== "" && segments [ 1 ] !== ""
? ` ${ segments [ 0 ] } / ${ segments [ 1 ] } `
: null ;
return segments [ 0 ] === "" ? null : ( segments [ 0 ] ?? null );
};
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-10 21:26:02 +08:00
if ( declaredPath === "" ) return emptyHtmlReference ( decodedBase , basePath );
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-10 03:11:08 +08:00
const emptyHtmlReference = (
base : string ,
basePath : string ,
2026-10-10 21:26:02 +08:00
) : string | CandidateResolution => {
2026-10-10 03:11:08 +08:00
if ( ! htmlBaseIsDirectory ( base )) return basePath ;
2026-10-10 21:26:02 +08:00
const confined = confineCandidate ( basePath );
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-10 03:11:08 +08:00
`The HTML reference resolves to directory ${ confined || "." } , not an exact selected application file.` ,
]);
};
2026-10-05 13:02:56 -04:00
/**
* The directory a relative HTML reference resolves against. A base path
* ending in "/" or in a "." or ".." segment is already a directory, because a
* relative reference resolves against the base URL's directory and those
* segments are dropped rather than stepped through.
*/
2026-10-10 03:11:08 +08:00
const htmlBaseIsDirectory = ( base : string ) : boolean =>
2026-10-05 13:02:56 -04:00
base . endsWith ( "/" ) ||
base . endsWith ( "/." ) ||
base . endsWith ( "/.." ) ||
base === "." ||
2026-10-10 03:11:08 +08:00
base === ".." ;
const htmlBaseDirectory = ( base : string , basePath : string ) : string =>
htmlBaseIsDirectory ( 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 ;
readonly value : object ;
}
| { 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-10 21:26:02 +08:00
return typeof value === "object" && value !== null
? { kind : "available" , path , value }
: {
kind : "unavailable" ,
limitation : `Directory package metadata ${ path } is not valid package JSON.` ,
};
};
/** File and directory lookup never interprets a nested directory's exports. */
const resolveCandidate = (
input : ResolveArtifactPathInput ,
candidate : string ,
packageChain : ReadonlySet < string >,
) : CandidateResolution =>
resolveFileCandidates ( input , fileCandidates ( candidate )) ??
resolveDirectory (
input ,
candidate ,
readPackageMetadata ( input , candidate ),
packageChain ,
);
const resolveDirectory = (
input : ResolveArtifactPathInput ,
directory : string ,
metadata : PackageMetadata ,
packageChain : ReadonlySet < string >,
) : CandidateResolution => {
if ( metadata . kind === "absent" )
return (
resolveFileCandidates ( input , indexCandidates ( directory )) ??
notFoundCandidate ()
);
if ( metadata . kind === "unavailable" )
return unresolvedCandidate ( "unavailable" , [ metadata . limitation ]);
if ( packageChain . has ( metadata . path ))
return unresolvedCandidate ( "unavailable" , [
`Directory package entrypoint cycle includes ${ metadata . path } .` ,
]);
const preferred = [
Reflect . get ( metadata . value , "main" ),
...( input . moduleKind === undefined
? [ Reflect . get ( metadata . value , "module" )]
: []),
];
const entry = preferred . find (( value ) => value !== undefined );
if ( entry === undefined )
2026-10-05 07:34:48 -07:00
return (
2026-10-10 21:26:02 +08:00
resolveFileCandidates ( input , indexCandidates ( directory )) ??
2026-10-05 07:34:48 -07:00
notFoundCandidate ()
);
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 rejected = rejectDeclaration ( entryInput . declaredPath );
if ( rejected !== null ) return rejected ;
const candidate = contextualCandidate ( entryInput );
if ( typeof candidate !== "string" ) return candidate ;
const confined = confineCandidate ( candidate );
if ( typeof confined !== "string" ) return confined ;
const selected =
resolveFileCandidates ( entryInput , [
... fileCandidates ( confined ),
... indexCandidates ( confined ),
]) ?? resolveFileCandidates ( input , indexCandidates ( directory ));
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
};
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 )
)
return { resolvedPath : path , status : "resolved" , limitations : [] };
}
return null ;
2026-07-17 21:30:24 +08:00
};
2026-10-05 07:34:48 -07:00
const fileCandidates = ( candidate : string ) : readonly string [] => [
2026-07-17 21:30:24 +08:00
candidate ,
... EXTENSIONS . map (( extension ) => ` ${ candidate }${ extension } ` ),
];
2026-10-05 07:34:48 -07:00
const indexCandidates = ( candidate : string ) : readonly string [] =>
EXTENSIONS . map (( extension ) => posix . join ( candidate , `index ${ extension } ` ));
2026-10-10 21:26:02 +08:00
type PackageExportOutcome =
| { readonly status : "value" ; readonly value : string }
2026-10-08 02:00:34 -04:00
| { readonly status : "rejected" ; readonly limitation : string }
2026-10-10 21:26:02 +08:00
| { readonly status : "blocked" }
| { 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-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 ),
);
if ( selected . kind === "invalid" || selected . kind === "invalid-config" )
return { status : "rejected" , limitation : selected.limitation };
if ( selected . kind === "unmatched" )
return {
status : "unmatched" ,
declared :
typeof target === "object" && target !== null
? Object . keys ( target )
: declared ,
};
return selected . kind === "blocked"
? { status : "blocked" }
: { status : "value" , value : selected.value };
2026-07-17 23:57:23 +08:00
};
2026-10-10 21:26:02 +08:00
type ExportTargetOutcome =
| { readonly kind : "value" ; readonly value : string }
| { readonly kind : "blocked" }
2026-10-05 14:09:12 -04:00
| { readonly kind : "unmatched" }
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-10 21:26:02 +08:00
) : ExportTargetOutcome => ({
2026-10-08 02:00:34 -04:00
kind : "invalid" ,
limitation : `The package exports target ${ JSON . stringify ( value ) ?? String ( value ) } ${ reason } .` ,
});
// 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-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
* preserves the final invalid-versus-null refusal if no string is selected.
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-10 21:26:02 +08:00
) : ExportTargetOutcome => {
2026-10-08 02:00:34 -04:00
if ( typeof value === "string" ) {
if ( ! value . startsWith ( "./" ))
return invalidExportTarget ( value , 'must start with "./"' );
const forbidden = forbiddenExportSegment ( value );
return forbidden === undefined
2026-10-10 21:26:02 +08:00
? { kind : "value" , value }
2026-10-08 02:00:34 -04:00
: invalidExportTarget (
value ,
`contains forbidden path segment ${ JSON . stringify ( forbidden ) } before URL normalization` ,
);
}
2026-10-10 21:26:02 +08:00
if ( value === null ) return { kind : "blocked" };
2026-10-05 13:03:18 -04:00
if ( Array . isArray ( value )) {
2026-10-08 02:00:34 -04:00
let invalid : string | null = null ;
2026-10-05 13:03:18 -04:00
for ( const entry of value ) {
2026-10-10 21:26:02 +08:00
const nested = selectExportTarget ( entry , conditions );
2026-10-10 00:44:29 +08:00
// Node's array fallback catches invalid targets, not invalid configuration.
if ( nested . kind === "invalid-config" ) return nested ;
2026-10-05 14:09:12 -04:00
if ( nested . kind === "invalid" ) {
2026-10-08 02:00:34 -04:00
invalid = nested . limitation ;
2026-10-05 14:09:12 -04:00
continue ;
}
if ( nested . kind === "unmatched" ) continue ;
2026-10-08 02:00:34 -04:00
invalid = null ;
2026-10-10 21:26:02 +08:00
if ( nested . kind === "value" ) return nested ;
2026-10-05 13:03:18 -04:00
}
2026-10-08 02:00:34 -04:00
return invalid === null
2026-10-10 21:26:02 +08:00
? { kind : "blocked" }
2026-10-08 02:00:34 -04:00
: { kind : "invalid" , limitation : invalid };
2026-10-05 13:03:18 -04:00
}
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-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-05 13:03:18 -04:00
for ( const [ condition , target ] of Object . entries ( value )) {
if ( ! conditions . has ( condition )) continue ;
2026-10-10 21:26:02 +08:00
const nested = selectExportTarget ( target , conditions );
2026-10-05 14:09:12 -04:00
if ( nested . kind !== "unmatched" ) return nested ;
2026-10-05 13:03:18 -04:00
}
2026-10-05 14:09:12 -04:00
return { kind : "unmatched" };
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-07-17 21:30:24 +08:00
const notFoundCandidate = () : CandidateResolution => ({
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-08 20:49:32 -07:00
const exactExportsTargetNotFound = ( path : string ) : CandidateResolution => ({
resolvedPath : null ,
status : "not-found" ,
limitations : [
`The selected package exports target ${ path } does not exist in the inventoried artifact container; exports targets are exact files, so no extension or directory index was tried.` ,
],
});
2026-07-17 21:30:24 +08:00
const fileUrlPath = ( value : string ) : string | undefined => {
if ( ! value . startsWith ( "file://" )) return undefined ;
try {
const url = new URL ( value );
if ( url . protocol !== "file:" || url . hostname !== "" ) return undefined ;
return decodeURIComponent ( url . pathname ). 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 });