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-11 23:52:42 +08:00
import { typescriptEmittedSourceCandidates } from "../TypeScriptEmittedSourceCandidates.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 => {
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 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-11 23:52:42 +08:00
return resolveCandidate (
input ,
confined ,
new Set (),
input . context === "module-specifier" &&
( input . declaredPath . startsWith ( "." ) || input . declaredPath . startsWith ( "/" ))
? typescriptEmittedSourceCandidates ( input . sourcePath , confined )
: [],
);
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-11 18:19:39 +08:00
if ( context === "url-reference" ) {
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." ,
]);
declared = decoded ;
}
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-11 10:17:58 -07:00
if ( packageName === null || isBuiltin ( declared ))
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-11 18:57:58 +08:00
if ( input . moduleKind === "import" && packageName . includes ( "%" ))
return unresolvedCandidate ( "rejected" , [
"Percent signs are not admitted in ES module package names." ,
]);
const self = commonJsSelfCandidate ( input , declared );
if ( self !== null ) return self ;
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 );
2026-10-11 18:57:58 +08:00
const metadata = readPackageMetadata ( input , candidate );
if ( packageExportsEligible ( input ) && metadata . kind !== "absent" )
return resolvePackageSpecifier ( input , candidate , subpath , metadata );
2026-10-10 21:26:02 +08:00
if ( subpath === "." ) {
const direct = resolveFileCandidates ( input , fileCandidates ( candidate ));
if ( direct !== null ) return direct ;
}
if ( hasContainerDirectory ( input . files , candidate , source ? . container_sha256 ))
2026-10-11 18:57:58 +08:00
return resolvePackageSpecifier ( input , candidate , subpath , metadata );
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-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 } .` ,
]);
const selected = selectPackageImportTarget ( lookup . target , input . moduleKind );
if ( selected . status === "rejected" )
return unresolvedCandidate ( "rejected" , [
`Package metadata ${ metadata . path } : ${ selected . limitation } ` ,
]);
if ( selected . status === "unmatched" )
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" } .` ,
]);
if ( selected . status === "blocked" )
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" } .` ,
]);
if ( selected . status === "non-relative" ) {
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 ) } .` ,
]);
}
return resolveExportTarget ( input , metadata . path , selected . value , "imports" );
};
const selectPackageImportTarget = (
value : unknown ,
moduleKind : ResolveArtifactPathInput [ "moduleKind" ],
) :
| { readonly status : "value" ; readonly value : string }
| { readonly status : "non-relative" ; readonly value : string }
| { readonly status : "blocked" ; readonly cause : PackageTargetBlockCause }
| { readonly status : "unmatched" ; readonly declared : readonly string [] }
| { readonly status : "rejected" ; readonly limitation : string } => {
const selected = selectExportTarget (
value ,
packageExportConditions ( moduleKind ),
"imports" ,
);
if ( selected . kind === "invalid" || selected . kind === "invalid-config" )
return { status : "rejected" , limitation : selected.limitation };
if ( selected . kind === "unmatched" )
return {
status : "unmatched" ,
declared : selected.declared ,
};
if ( selected . kind === "blocked" )
return { status : "blocked" , cause : selected.cause };
if ( selected . kind === "non-relative" )
return { status : "non-relative" , value : selected.value };
return { status : "value" , value : selected.value };
};
/** Self references use the nearest package scope before dependency lookup. */
const commonJsSelfCandidate = (
input : ResolveArtifactPathInput ,
declared : string ,
) : CandidateResolution | null => {
if ( input . moduleKind !== "require" || ! input . files . has ( input . sourcePath ))
return null ;
const metadata = enclosingPackageMetadata ( input );
if ( metadata . kind === "none" ) return null ;
if ( metadata . kind === "unavailable" )
return unresolvedCandidate ( "unavailable" , [ metadata . limitation ]);
if (
metadata . name === undefined ||
metadata . exports === undefined ||
( declared !== metadata . name && ! declared . startsWith ( ` ${ metadata . name } /` ))
)
return null ;
const subpath =
declared === metadata . name
? "."
: `. ${ declared . slice ( metadata . name . length ) } ` ;
// 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-10 21:26:02 +08:00
/** Package roots and exact subpaths share metadata and exports selection. */
const resolvePackageSpecifier = (
input : ResolveArtifactPathInput ,
directory : string ,
subpath : string ,
2026-10-11 18:57:58 +08:00
metadata : PackageMetadata ,
2026-10-10 21:26:02 +08:00
) : CandidateResolution => {
2026-10-11 18:57:58 +08:00
const exportsEligible = packageExportsEligible ( input );
if ( exportsEligible && metadata . kind === "unavailable" )
2026-10-10 21:26:02 +08:00
return unresolvedCandidate ( "unavailable" , [ metadata . limitation ]);
const rawExports =
2026-10-11 18:57:58 +08:00
exportsEligible && metadata . kind === "available"
? 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 (
resolveFileCandidates ( input , fileCandidates ( directory )) ??
resolveDirectory ( input , directory , metadata , new Set ())
);
2026-10-11 02:23:05 +04:00
// Preserve the trailing slash for a package directory request.
const candidate = confineCandidate ( ` ${ directory } / ${ subpath . slice ( 2 ) } ` );
2026-10-10 21:26:02 +08:00
return typeof candidate === "string"
? resolveCandidate ( input , candidate , new Set ())
: candidate ;
}
2026-10-11 18:57:58 +08:00
return resolvePackageExports (
input ,
posix . join ( directory , "package.json" ),
subpath ,
rawExports ,
);
};
const resolvePackageExports = (
input : ResolveArtifactPathInput ,
packagePath : string ,
subpath : string ,
rawExports : string | object ,
) : CandidateResolution => {
2026-10-10 21:26:02 +08:00
const exported = selectPackageExport ( rawExports , subpath , input . moduleKind );
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
]);
return resolveExportTarget ( input , packagePath , exported . value );
};
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 ,
target : string ,
2026-10-11 10:17:58 -07:00
mapKind : "exports" | "imports" = "exports" ,
2026-10-10 21:26:02 +08:00
) : 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 ]) ??
2026-10-11 10:17:58 -07:00
exactPackageTargetNotFound ( candidate , mapKind )
2026-10-10 21:26:02 +08:00
);
};
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-11 18:57:58 +08:00
const packageExportsEligible = ( input : ResolveArtifactPathInput ) : boolean => {
if ( input . moduleKind !== "require" ) return true ;
// Node's CommonJS exports matcher excludes percent-containing package names.
// Compare the captured name so a scoped name cannot fall back to its scope.
const match = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/ . exec (
input . declaredPath ,
);
return match ? .[ 1 ] === barePackageName ( input . declaredPath );
};
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 ;
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-11 23:52:42 +08:00
emittedSourceCandidates : readonly string [] = [],
2026-10-10 21:26:02 +08:00
) : CandidateResolution =>
2026-10-11 23:52:42 +08:00
resolveFileCandidates ( input , [
... fileCandidates ( candidate ),
... emittedSourceCandidates ,
]) ??
2026-10-10 21:26:02 +08:00
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 = [
2026-10-11 18:57:58 +08:00
metadata . main ,
...( input . moduleKind === undefined ? [ metadata . module ] : []),
2026-10-10 21:26:02 +08:00
];
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-11 10:17:58 -07:00
type PackageTargetBlockCause = "null" | "empty-array" ;
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-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-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" ,
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-11 10:17:58 -07:00
return { 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 }
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 ,
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 );
return forbidden === undefined
? { kind : "value" , value }
: invalidExportTarget (
value ,
`contains forbidden path segment ${ JSON . stringify ( forbidden ) } before URL normalization` ,
mapKind ,
);
};
const selectArrayPackageTarget = (
value : readonly unknown [],
conditions : ReadonlySet < string >,
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 ) {
const nested = selectExportTarget ( entry , conditions , mapKind );
// 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-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" )
return selectStringPackageTarget ( value , mapKind );
if ( value === null ) return { kind : "blocked" , cause : "null" };
if ( Array . isArray ( value ))
return selectArrayPackageTarget ( value , conditions , 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-11 10:17:58 -07:00
const nested = selectExportTarget ( target , conditions , 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-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-11 10:17:58 -07:00
const exactPackageTargetNotFound = (
path : string ,
mapKind : "exports" | "imports" ,
) : CandidateResolution => ({
2026-10-08 20:49:32 -07:00
resolvedPath : null ,
status : "not-found" ,
limitations : [
2026-10-11 10:17:58 -07:00
`The selected package ${ mapKind === "imports" ? "import" : "exports" } target ${ path } does not exist in the inventoried artifact container; ${ mapKind === "imports" ? "import" : "exports" } targets are exact files, so no extension or directory index was tried.` ,
2026-10-08 20:49:32 -07:00
],
});
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 });