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-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
/** Resolve one declaration under its exact syntax/metadata context. */
export const resolveArtifactPathByContext = (
input : ResolveArtifactPathInput ,
2026-09-29 15:30:43 +08:00
) : ArtifactPathResolution => resolvePackagePath ( input , new Set ());
2026-07-17 21:30:24 +08:00
2026-09-29 15:30:43 +08:00
const resolvePackagePath = (
2026-07-17 21:30:24 +08:00
input : ResolveArtifactPathInput ,
2026-09-29 15:30:43 +08:00
packageChain : ReadonlySet < string >,
2026-10-05 07:34:48 -07:00
mode : "full" | "files-and-index" = "full" ,
2026-10-08 02:00:34 -04:00
exportsTargetUrl = false ,
2026-07-17 21:30:24 +08:00
) : ArtifactPathResolution => {
const rejected = rejectDeclaration ( input );
if ( rejected !== null ) return rejected ;
2026-10-08 02:00:34 -04:00
const candidate = contextualCandidate ( input , exportsTargetUrl );
2026-07-17 21:30:24 +08:00
if ( typeof candidate !== "string" ) return candidate ;
const confined = confineCandidate ( input , candidate );
if ( typeof confined !== "string" ) return confined ;
2026-10-08 01:24:24 -04:00
if ( input . context === "html-reference" )
return outcome (
input ,
resolveFileCandidates ( input , [ confined ]) ?? {
resolvedPath : null ,
status : "not-found" ,
limitations : [
`The exact HTML resource ${ confined } was not found among the selected application files.` ,
],
},
);
2026-10-08 20:49:32 -07:00
// An exports target names one exact file; Node tries no extension or index.
const resolved = exportsTargetUrl
? ( resolveFileCandidates ( input , [ confined ]) ??
exactExportsTargetNotFound ( confined ))
: mode === "files-and-index"
2026-10-05 07:34:48 -07:00
? ( resolveFileCandidates ( input , [
... fileCandidates ( confined ),
... indexCandidates ( confined ),
]) ?? notFoundCandidate ())
: resolveCandidate ( input , confined , packageChain );
2026-08-10 14:57:44 +08:00
return outcome ( input , resolved );
2026-07-17 21:30:24 +08:00
};
const rejectDeclaration = (
input : ResolveArtifactPathInput ,
) : ArtifactPathResolution | null => {
const declared = input . declaredPath ;
if ( declared . length === 0 )
2026-08-10 14:57:44 +08:00
return unresolvedOutcome ( input , "rejected" , [
"The declared path is empty." ,
]);
2026-07-17 21:30:24 +08:00
if ( declared . includes ( "\0" ) || declared . includes ( "\\" ))
2026-08-10 14:57:44 +08:00
return unresolvedOutcome ( input , "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-08-10 14:57:44 +08:00
return unresolvedOutcome ( input , "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-08 02:00:34 -04:00
exportsTargetUrl : boolean ,
2026-07-17 21:30:24 +08:00
) : string | ArtifactPathResolution => {
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-08 02:00:34 -04:00
exportsTargetUrl ||
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-08-10 14:57:44 +08:00
return unresolvedOutcome ( input , "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-07 16:34:15 +05:30
} else if ( looksExternal ( declared ))
2026-08-10 14:57:44 +08:00
return unresolvedOutcome ( input , "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-08 02:00:34 -04:00
// Exports targets use URL paths for both loaders; legacy main fields remain literal.
if (
exportsTargetUrl ||
( context === "module-specifier" && input . moduleKind !== "require" )
) {
try {
declared = decodeURIComponent ( declared );
} catch ( cause : unknown ) {
void cause ;
return unresolvedOutcome ( input , "rejected" , [
"The module URL path contains malformed percent encoding." ,
]);
}
if ( declared . includes ( "\0" ))
return unresolvedOutcome ( input , "rejected" , [
"The decoded module URL path contains NUL." ,
]);
}
2026-07-17 21:30:24 +08:00
const relative = declared . startsWith ( "/" ) ? declared . slice ( 1 ) : declared ;
return declared . startsWith ( "/" )
? relative
: posix.join ( posix . dirname ( input . sourcePath ), relative );
};
2026-07-17 23:57:23 +08:00
const bareModuleCandidate = (
input : ResolveArtifactPathInput ,
declared : string ,
) : string | ArtifactPathResolution => {
const packageName = barePackageName ( declared );
2026-10-10 00:43:33 +08:00
if ( packageName === null || isBuiltin ( declared ) || declared . startsWith ( "#" ))
2026-08-10 14:57:44 +08:00
return unresolvedOutcome ( input , "external" , [
2026-07-17 23:57:23 +08:00
"The bare specifier is a Node builtin, package import map, or invalid package name." ,
]);
if ( declared !== packageName )
2026-08-10 14:57:44 +08:00
return unresolvedOutcome ( input , "external" , [
2026-07-17 23:57:23 +08:00
"Bare package subpaths remain unresolved unless an exact package-exports subpath model is available." ,
]);
const source = input . files . get ( input . sourcePath );
let directory = posix . dirname ( input . sourcePath );
while ( true ) {
const candidate = posix . join ( directory , "node_modules" , declared );
if ( hasContainerCandidate ( input . files , candidate , source ? . container_sha256 ))
return candidate ;
if ( directory === "." || directory === "" ) break ;
directory = posix . dirname ( directory );
}
2026-08-10 14:57:44 +08:00
return unresolvedOutcome ( input , "external" , [
2026-07-17 23:57:23 +08:00
"No matching bare package was inventoried in an enclosing node_modules directory." ,
]);
};
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 );
};
const hasContainerCandidate = (
files : ReadonlyMap < string , JavaScriptArtifactFile >,
candidate : string ,
containerSha256 : string | undefined ,
) : boolean =>
2026-10-05 07:34:48 -07:00
[
... fileCandidates ( candidate ),
... indexCandidates ( candidate ),
posix . join ( candidate , "package.json" ),
]. some (( path ) => {
const file = files . get ( path );
return (
file !== undefined &&
( containerSha256 === undefined ||
file . container_sha256 === containerSha256 )
);
});
2026-07-17 23:57:23 +08:00
2026-07-17 21:30:24 +08:00
const htmlCandidate = (
input : ResolveArtifactPathInput ,
) : string | ArtifactPathResolution => {
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-08-10 14:57:44 +08:00
return unresolvedOutcome ( input , "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-05 13:02:56 -04:00
return unresolvedOutcome ( input , "external" , [
"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 ))
return unresolvedOutcome ( input , "rejected" , [
"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 )
return unresolvedOutcome ( input , "rejected" , [
"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 ))
return unresolvedOutcome ( input , "rejected" , [
"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 )
return unresolvedOutcome ( input , "rejected" , [
"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 03:11:08 +08:00
if ( declaredPath === "" )
return emptyHtmlReference ( input , 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 = (
input : ResolveArtifactPathInput ,
base : string ,
basePath : string ,
) : string | ArtifactPathResolution => {
if ( ! htmlBaseIsDirectory ( base )) return basePath ;
const confined = confineCandidate ( input , basePath );
if ( typeof confined !== "string" ) return confined ;
return unresolvedOutcome ( input , "not-found" , [
`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-07-17 21:30:24 +08:00
const confineCandidate = (
input : ResolveArtifactPathInput ,
candidate : string ,
) : string | ArtifactPathResolution => {
const normalized = posix . normalize ( candidate );
if (
normalized === ".." ||
normalized . startsWith ( "../" ) ||
normalized . startsWith ( "/" )
)
2026-08-10 14:57:44 +08:00
return unresolvedOutcome ( input , "rejected" , [
2026-07-17 21:30:24 +08:00
"The resolved candidate escapes the canonical artifact root." ,
]);
return normalized === "." ? "" : normalized ;
};
const resolveCandidate = (
input : ResolveArtifactPathInput ,
candidate : string ,
2026-09-29 15:30:43 +08:00
packageChain : ReadonlySet < string >,
2026-07-17 21:30:24 +08:00
) : CandidateResolution => {
2026-10-05 07:34:48 -07:00
const direct = resolveFileCandidates ( input , fileCandidates ( candidate ));
if ( direct !== null ) return direct ;
2026-07-17 21:30:24 +08:00
const source = input . files . get ( input . sourcePath );
const packagePath = posix . join ( candidate , "package.json" );
const packageFile = input . files . get ( packagePath );
if (
packageFile === undefined ||
( source !== undefined &&
packageFile . container_sha256 !== source . container_sha256 )
)
2026-10-05 07:34:48 -07:00
return (
resolveFileCandidates ( input , indexCandidates ( candidate )) ??
notFoundCandidate ()
);
2026-07-17 21:30:24 +08:00
if ( ! packageFile . text . included )
return {
resolvedPath : null ,
status : "unavailable" ,
limitations : [
`Directory package metadata ${ packagePath } was inventoried but its text is unavailable: ${ packageFile . text . reason } .` ,
],
};
2026-10-05 07:34:48 -07:00
const main = packageEntry (
packageFile . text . value ,
input . moduleKind ,
input . context === "module-specifier" &&
! input . declaredPath . startsWith ( "." ) &&
! input . declaredPath . startsWith ( "/" ) &&
! hasScheme ( input . declaredPath ),
);
if ( main . status === "invalid" )
2026-09-29 15:30:43 +08:00
return {
resolvedPath : null ,
status : "unavailable" ,
limitations : [
2026-10-05 07:34:48 -07:00
`Directory package metadata ${ packagePath } is not valid package JSON.` ,
2026-09-29 15:30:43 +08:00
],
};
2026-10-08 02:00:34 -04:00
if ( main . status === "rejected" )
return {
resolvedPath : null ,
status : "rejected" ,
limitations : [ `Package metadata ${ packagePath } : ${ main . limitation } ` ],
};
2026-10-05 13:03:18 -04:00
if ( main . status === "unmatched" )
return {
resolvedPath : null ,
status : "external" ,
limitations : [
`Directory package metadata ${ packagePath } declares no exports target for the active conditions; it declares ${ main . declared . join ( ", " ) } .` ,
],
};
2026-10-05 07:34:48 -07:00
if ( packageChain . has ( packagePath )) {
2026-07-17 21:30:24 +08:00
return {
resolvedPath : null ,
status : "unavailable" ,
limitations : [
2026-10-05 07:34:48 -07:00
`Directory package entrypoint cycle includes ${ packagePath } .` ,
2026-07-17 21:30:24 +08:00
],
};
2026-10-05 07:34:48 -07:00
}
if ( main . status === "missing" )
return (
resolveFileCandidates ( input , indexCandidates ( candidate )) ??
notFoundCandidate ()
);
const entryInput : ResolveArtifactPathInput = {
declaredPath : main.value ,
sourcePath : packagePath ,
context : "package-entrypoint" ,
files : input.files ,
};
const chain = new Set ([... packageChain , packagePath ]);
if ( main . source === "legacy" ) {
const entry = resolvePackagePath ( entryInput , chain , "files-and-index" );
if ( entry . resolution_status !== "not-found" ) return candidateOutcome ( entry );
const index = resolveFileCandidates ( input , indexCandidates ( candidate ));
if ( index !== null ) return index ;
}
// Preserve recursive artifact package lookup only after legacy file/index fallbacks.
2026-10-08 02:00:34 -04:00
return candidateOutcome (
resolvePackagePath ( entryInput , chain , "full" , main . source === "exports" ),
);
2026-10-05 07:34:48 -07:00
};
const candidateOutcome = (
nested : ArtifactPathResolution ,
) : CandidateResolution =>
nested . resolution_status === "resolved"
2026-08-10 14:57:44 +08:00
? {
resolvedPath : nested.resolved_path ,
status : nested.resolution_status ,
limitations : nested.limitations ,
}
: {
resolvedPath : null ,
status : nested.resolution_status ,
limitations : nested.limitations ,
};
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-07-17 23:57:23 +08:00
const packageEntry = (
2026-07-17 21:30:24 +08:00
text : string ,
2026-07-17 23:57:23 +08:00
moduleKind : ResolveArtifactPathInput [ "moduleKind" ],
2026-10-05 07:34:48 -07:00
useExports : boolean ,
2026-07-17 21:30:24 +08:00
) :
2026-10-05 07:34:48 -07:00
| {
readonly status : "value" ;
readonly value : string ;
readonly source : "legacy" | "exports" ;
}
2026-07-17 21:30:24 +08:00
| { readonly status : "missing" }
2026-10-05 13:03:18 -04:00
| { readonly status : "invalid" }
2026-10-08 02:00:34 -04:00
| { readonly status : "rejected" ; readonly limitation : string }
2026-10-05 13:03:18 -04:00
| { readonly status : "unmatched" ; readonly declared : readonly string [] } => {
2026-07-17 21:30:24 +08:00
try {
const value : unknown = JSON . parse ( text );
if ( typeof value !== "object" || value === null )
return { status : "invalid" };
2026-07-17 23:57:23 +08:00
const rawExports = Reflect . get ( value , "exports" );
2026-10-05 07:34:48 -07:00
if ( useExports && rawExports !== undefined && rawExports !== null ) {
const exported = packageExport ( rawExports , moduleKind );
return exported . status === "value"
? { ... exported , source : "exports" }
: exported ;
}
2026-10-08 08:47:01 -04:00
// Node imports and requires ignore the bundler-only module field. Keep
// that fallback for callers without an explicit module-loading kind.
const preferred = [
Reflect . get ( value , "main" ),
...( moduleKind === undefined ? [ Reflect . get ( value , "module" )] : []),
];
2026-07-17 23:57:23 +08:00
const entry = preferred . find (( candidate ) => candidate !== undefined );
if ( entry === undefined ) return { status : "missing" };
2026-10-05 07:34:48 -07:00
const legacy = packagePathValue ( entry );
return legacy . status === "value" ? { ... legacy , source : "legacy" } : legacy ;
2026-10-06 05:58:46 +08:00
} catch ( cause : unknown ) {
// Reflect-based manifest reads fail closed as invalid.
void cause ;
2026-07-17 21:30:24 +08:00
return { status : "invalid" };
}
};
2026-07-17 23:57:23 +08:00
const packageExport = (
value : unknown ,
moduleKind : ResolveArtifactPathInput [ "moduleKind" ],
2026-10-05 13:03:18 -04:00
) : PackageExportOutcome => {
2026-10-10 00:44:29 +08:00
if ( typeof value === "object" && value !== null && ! Array . isArray ( value )) {
const keys = Object . keys ( value );
if (
keys . some (( key ) => key . startsWith ( "." )) &&
keys . some (( key ) => ! key . startsWith ( "." ))
)
return {
status : "rejected" ,
limitation :
"The package exports configuration mixes subpath keys and condition keys." ,
};
}
2026-10-08 02:00:34 -04:00
const root =
typeof value === "object" && value !== null && Object . hasOwn ( value , "." )
? Reflect . get ( value , "." )
: value ;
2026-10-05 13:03:18 -04:00
const flattened = exportTargets ( root , packageExportConditions ( moduleKind ));
2026-10-10 00:44:29 +08:00
if ( flattened . kind === "invalid" || flattened . kind === "invalid-config" )
2026-10-08 02:00:34 -04:00
return { status : "rejected" , limitation : flattened.limitation };
2026-10-05 14:09:12 -04:00
const first =
flattened . kind === "unmatched" ? undefined : flattened . values [ 0 ];
2026-10-05 13:03:18 -04:00
return first === undefined
2026-10-08 02:00:34 -04:00
? {
status : "unmatched" ,
declared :
typeof root === "object" && root !== null ? Object . keys ( root ) : [],
}
2026-10-05 13:03:18 -04:00
: { status : "value" , value : first };
2026-07-17 23:57:23 +08:00
};
2026-10-05 13:03:18 -04:00
type ExportTargets =
| { readonly kind : "targets" ; readonly values : readonly string [] }
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 ,
) : ExportTargets => ({
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
*/
const exportTargets = (
value : unknown ,
conditions : ReadonlySet < string >,
) : ExportTargets => {
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-05 13:03:18 -04:00
? { kind : "targets" , values : [ value ] }
2026-10-08 02:00:34 -04:00
: invalidExportTarget (
value ,
`contains forbidden path segment ${ JSON . stringify ( forbidden ) } before URL normalization` ,
);
}
2026-10-05 13:03:18 -04:00
if ( value === null ) return { kind : "targets" , values : [] };
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 ) {
const nested = exportTargets ( 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-05 14:09:12 -04:00
if ( nested . values . length > 0 ) return nested ;
2026-10-05 13:03:18 -04:00
}
2026-10-08 02:00:34 -04:00
return invalid === null
? { kind : "targets" , values : [] }
: { 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-05 14:09:12 -04:00
const nested = exportTargets ( target , conditions );
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
* built-in resolver that owns installed-package imports and requires.
*/
const packageExportConditions = (
moduleKind : ResolveArtifactPathInput [ "moduleKind" ],
) : ReadonlySet < string > =>
new Set ([
"node" ,
"node-addons" ,
...( moduleKind === undefined ? [] : [ moduleKind ]),
"default" ,
]);
type PackageExportOutcome =
| { readonly status : "value" ; readonly value : string }
2026-10-08 02:00:34 -04:00
| { readonly status : "rejected" ; readonly limitation : string }
2026-10-05 13:03:18 -04:00
| { readonly status : "unmatched" ; readonly declared : readonly string [] };
2026-09-29 15:30:43 +08:00
const packagePathValue = (
2026-07-17 23:57:23 +08:00
value : unknown ,
) :
| { readonly status : "value" ; readonly value : string }
| { readonly status : "invalid" } =>
2026-09-29 15:30:43 +08:00
typeof value === "string" && value . length > 0
2026-07-17 23:57:23 +08:00
? { status : "value" , value }
: { status : "invalid" };
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 ;
}
};
const outcome = (
input : ResolveArtifactPathInput ,
2026-08-10 14:57:44 +08:00
resolution : CandidateResolution ,
) : ArtifactPathResolution =>
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 unresolvedOutcome = (
input : ResolveArtifactPathInput ,
status : UnresolvedArtifactPathStatus ,
2026-07-17 21:30:24 +08:00
limitations : readonly string [],
2026-08-10 14:57:44 +08:00
) : ArtifactPathResolution =>
outcome ( input , { resolvedPath : null , status , limitations });