fix(javascript): align module lookup and source evidence
Preserve CommonJS literal paths and confined ESM URL semantics, including package main fallback, directory intent, Unicode identity, and present opaque modules. Parse extraction inputs at their AST/source owner, retain skipped HTML and source-map coverage, and distinguish endpoint candidates from proven receiver facts. Version structural fingerprints with computed literal contents and propagate them through worker contracts. Reviewed fixes include #1810, #1840, #2642, #2700, #2702, #2714, #2803, #2960, #2968, #2986, #3035, and #3036. Validation: 158 resolver tests passed; public CLI/MCP module lookup controls, package-main/Unicode workflows, and six builtin workflows passed in the current compiled lane. Worker schema/fingerprint regressions passed. Large public pdf.js completion and real Electron analysis remain unverified.
This commit is contained in:
1 parent
04672f2b91
commit
b58adafa4f
63 files changed
+4066
-326
No files matched your search
@@ -5,7 +5,10 @@ import {
|
||||
normalizeJoinedLogicalPath,
|
||||
logicalPathEscapesRoot,
|
||||
} from "../../domain/artifactIdentity.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import type {
|
||||
JavaScriptArtifactFile,
|
||||
JavaScriptUnanalyzedArtifactLookup,
|
||||
} from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import {
|
||||
admitsCanonicalPathSyntax,
|
||||
hasScheme,
|
||||
@@ -26,6 +29,7 @@ import {
|
||||
import { isRelativeJavaScriptModuleSpecifier } from "../../domain/javascript/javascriptModuleSpecifier.js";
|
||||
import type { JavaScriptModuleResolutionMode } from "../../domain/javascript/javascriptModuleResolutionMode.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
|
||||
type ArtifactPathResolutionContext =
|
||||
| "package-entrypoint"
|
||||
@@ -75,6 +79,10 @@ export interface ResolveArtifactPathInput {
|
||||
readonly sourcePath: string;
|
||||
readonly context: ArtifactPathResolutionContext;
|
||||
readonly files: ReadonlyMap<string, JavaScriptArtifactFile>;
|
||||
/** Directory identities retained by the complete artifact inventory. */
|
||||
readonly directoryPaths?: ReadonlySet<string> | undefined;
|
||||
/** Inventoried files outside the JavaScript projection: present, never read. */
|
||||
readonly unanalyzedFiles?: JavaScriptUnanalyzedArtifactLookup | undefined;
|
||||
readonly htmlBaseHref?: string | null;
|
||||
/** Import/require semantics; runtime resource paths omit this provenance. */
|
||||
readonly moduleKind?: JavaScriptModuleResolutionMode;
|
||||
@@ -102,16 +110,66 @@ const EXTENSIONS = [
|
||||
// A private URL namespace for logical artifact paths, including root packages.
|
||||
// Encode inventory segments so filename punctuation remains literal in the base.
|
||||
const PACKAGE_ARTIFACT_ROOT_URL = new URL("file:///rea-artifact/");
|
||||
const packageMetadataUrl = (path: string): URL =>
|
||||
const encodePackageMetadataUrl = (path: string): URL =>
|
||||
new URL(
|
||||
path.split("/").map(encodeURIComponent).join("/"),
|
||||
PACKAGE_ARTIFACT_ROOT_URL,
|
||||
);
|
||||
// URL identity cannot represent lone UTF-16 surrogates. Use request-local
|
||||
// opaque segments only for structural admission, never inventory lookup. A
|
||||
// target cannot spell these identities after URL's tab/newline preprocessing.
|
||||
const packageMetadataUrl = (
|
||||
path: string,
|
||||
relativeTarget: string,
|
||||
): Result<
|
||||
URL,
|
||||
{
|
||||
readonly structuralUrl: URL;
|
||||
readonly structuralSegments: ReadonlySet<string>;
|
||||
readonly limitation: string;
|
||||
}
|
||||
> => {
|
||||
try {
|
||||
return ok(encodePackageMetadataUrl(path));
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof URIError)) throw cause;
|
||||
const reserved = new Set(
|
||||
`${path}\n${relativeTarget.replace(/[\t\n\r]/gu, "")}`.match(
|
||||
/rea-opaque-\d+/gu,
|
||||
) ?? [],
|
||||
);
|
||||
const structuralSegments = new Set<string>();
|
||||
let nextIdentity = 0;
|
||||
const structuralPath = path
|
||||
.split("/")
|
||||
.map((segment) => {
|
||||
try {
|
||||
return encodeURIComponent(segment);
|
||||
} catch (segmentCause: unknown) {
|
||||
if (!(segmentCause instanceof URIError)) throw segmentCause;
|
||||
let identity: string;
|
||||
do identity = `rea-opaque-${nextIdentity++}`;
|
||||
while (reserved.has(identity));
|
||||
reserved.add(identity);
|
||||
structuralSegments.add(identity);
|
||||
return identity;
|
||||
}
|
||||
})
|
||||
.join("/");
|
||||
return err({
|
||||
structuralUrl: new URL(structuralPath, PACKAGE_ARTIFACT_ROOT_URL),
|
||||
structuralSegments,
|
||||
limitation: `Package metadata path ${JSON.stringify(path)} contains an unpaired UTF-16 surrogate and cannot be represented as a URL.`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
interface PackageFileTarget {
|
||||
readonly value: string;
|
||||
/** Normalized URL retained until loader-specific validation and file lookup. */
|
||||
readonly url: URL;
|
||||
/** Structural validation remains possible when the inventory base has no URL identity. */
|
||||
readonly lookupLimitation?: string;
|
||||
}
|
||||
|
||||
type CandidateResolution = {
|
||||
@@ -316,7 +374,13 @@ const resolveNodeRuntimeEntrypoint = (
|
||||
if (typeof confined !== "string") return confined;
|
||||
// Node entrypoints use runtime file/directory probes. They do not originate
|
||||
// in emitted TypeScript import syntax and cannot select a source substitute.
|
||||
return resolveCandidate(runtimeInput, confined, new Set());
|
||||
const resolution = resolveCandidate(runtimeInput, confined, new Set());
|
||||
// Runtime entrypoint analysis has no extension or directory fallback, but
|
||||
// retain the normalized loader path when that exact target is absent.
|
||||
return resolution.status === "not-found" &&
|
||||
resolution.candidatePath === undefined
|
||||
? { ...resolution, candidatePath: confined }
|
||||
: resolution;
|
||||
};
|
||||
|
||||
const resolveRuntimeEntrypoint = (
|
||||
@@ -349,19 +413,13 @@ const resolveRuntimeSourceCandidates = (
|
||||
exactPath: string,
|
||||
sourceCandidates: readonly TypeScriptEmittedSourceCandidate[],
|
||||
): CandidateResolution | null => {
|
||||
const source = input.files.get(input.sourcePath);
|
||||
const exactCandidate: TypeScriptEmittedSourceCandidate = {
|
||||
path: exactPath,
|
||||
kind: "implementation",
|
||||
};
|
||||
for (const candidate of [exactCandidate, ...sourceCandidates]) {
|
||||
const target = input.files.get(candidate.path);
|
||||
if (
|
||||
target === undefined ||
|
||||
(source !== undefined &&
|
||||
target.container_sha256 !== source.container_sha256)
|
||||
)
|
||||
continue;
|
||||
const selected = findFileCandidate(input, candidate.path);
|
||||
if (selected === null) continue;
|
||||
return candidate.kind === "declaration"
|
||||
? {
|
||||
resolvedPath: null,
|
||||
@@ -371,7 +429,7 @@ const resolveRuntimeSourceCandidates = (
|
||||
"Only a TypeScript declaration fallback was found; it does not establish the requested runtime module.",
|
||||
],
|
||||
}
|
||||
: { resolvedPath: candidate.path, status: "resolved", limitations: [] };
|
||||
: selected;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -439,7 +497,12 @@ const contextualCandidate = (
|
||||
"File URL authorities or invalid file URL forms cannot be mapped to artifact-local paths.",
|
||||
]);
|
||||
}
|
||||
const rejected = rejectDeclaration(declared, declaresLiteralPercent(input));
|
||||
const hasUrlPathSemantics =
|
||||
context === "module-specifier" && input.moduleKind === "import";
|
||||
const rejected = rejectDeclaration(
|
||||
declared,
|
||||
declaresLiteralPercent(input) || hasUrlPathSemantics,
|
||||
);
|
||||
if (rejected !== null) return rejected;
|
||||
if (context === "module-specifier") {
|
||||
if (hasScheme(declared))
|
||||
@@ -452,6 +515,10 @@ const contextualCandidate = (
|
||||
)
|
||||
return bareModuleCandidate(input);
|
||||
if (input.moduleKind !== "require") {
|
||||
if (/%(?:2f|5c)/iu.test(stripQueryAndFragment(declared)))
|
||||
return unresolvedCandidate("rejected", [
|
||||
"ES module URL paths cannot contain percent-encoded slash or backslash bytes.",
|
||||
]);
|
||||
const decoded = decodeModuleUrlPath(stripQueryAndFragment(declared));
|
||||
if (typeof decoded !== "string") return decoded;
|
||||
declared = decoded;
|
||||
@@ -575,7 +642,13 @@ const bareModuleCandidate = (
|
||||
metadata.exports === undefined)));
|
||||
const packageResolution =
|
||||
metadata.kind !== "absent" ||
|
||||
hasContainerDirectory(input.files, candidate, source?.container_sha256)
|
||||
hasContainerDirectory(
|
||||
input.files,
|
||||
candidate,
|
||||
source?.container_sha256,
|
||||
input.directoryPaths,
|
||||
input.unanalyzedFiles,
|
||||
)
|
||||
? resolvePackageDependency(input, candidate, request, metadata)
|
||||
: request.packageSubpath === "." && input.moduleKind !== "import"
|
||||
? resolveFileCandidates(
|
||||
@@ -674,7 +747,7 @@ const resolvePackageImportAlias = (
|
||||
const selected = selectExportTarget(
|
||||
lookup.target,
|
||||
packageExportConditions(input.moduleKind),
|
||||
packageMetadataUrl(metadata.path),
|
||||
metadata.path,
|
||||
"imports",
|
||||
);
|
||||
if (selected.kind === "invalid" || selected.kind === "invalid-config")
|
||||
@@ -750,17 +823,9 @@ const resolvePackageDependency = (
|
||||
fileCandidates(directory, input.moduleKind),
|
||||
)) ?? resolveDirectory(input, directory, metadata, new Set())
|
||||
);
|
||||
// Resolve legacy ESM subpaths as URLs: suffixes identify the loaded module
|
||||
// but do not belong to its filesystem path. Keep the declared specifier on
|
||||
// the relationship; only derive the inventory lookup path here.
|
||||
const fileSubpath =
|
||||
input.moduleKind === "import"
|
||||
? decodeModuleUrlPath(stripQueryAndFragment(subpath.slice(2)))
|
||||
: subpath.slice(2);
|
||||
if (typeof fileSubpath !== "string") return fileSubpath;
|
||||
const candidate = confineCandidate(`${directory}/${fileSubpath}`);
|
||||
if (input.moduleKind === "import" && typeof candidate === "string")
|
||||
return resolveLegacyEsmSubpath(input, candidate);
|
||||
if (input.moduleKind === "import")
|
||||
return resolveLegacyEsmSubpath(input, directory, subpath);
|
||||
const candidate = confineCandidate(`${directory}/${subpath.slice(2)}`);
|
||||
return typeof candidate === "string"
|
||||
? resolveCandidate(input, candidate, new Set())
|
||||
: candidate;
|
||||
@@ -776,12 +841,58 @@ const resolvePackageDependency = (
|
||||
/** Legacy ESM package subpaths are exact URLs, never extension or directory lookups. */
|
||||
const resolveLegacyEsmSubpath = (
|
||||
input: ResolveArtifactPathInput,
|
||||
candidate: string,
|
||||
directory: string,
|
||||
subpath: string,
|
||||
): CandidateResolution => {
|
||||
const declarationRejection = rejectDeclaration(
|
||||
stripQueryAndFragment(htmlUrlText(subpath)),
|
||||
);
|
||||
if (declarationRejection !== null) return declarationRejection;
|
||||
const metadataUrl = packageMetadataUrl(
|
||||
posix.join(directory, "package.json"),
|
||||
subpath,
|
||||
);
|
||||
const url = new URL(
|
||||
subpath,
|
||||
metadataUrl.ok ? metadataUrl.value : metadataUrl.error.structuralUrl,
|
||||
);
|
||||
if (!url.pathname.startsWith(PACKAGE_ARTIFACT_ROOT_URL.pathname))
|
||||
return unresolvedCandidate("rejected", [
|
||||
"The resolved candidate escapes the canonical artifact root.",
|
||||
]);
|
||||
const encodedPath = url.pathname.slice(
|
||||
PACKAGE_ARTIFACT_ROOT_URL.pathname.length,
|
||||
);
|
||||
const rejected = rejectDeclaration(encodedPath);
|
||||
if (rejected !== null) return rejected;
|
||||
const decoded = decodeModuleUrlPath(encodedPath);
|
||||
if (typeof decoded !== "string") return decoded;
|
||||
const candidate = confineCandidate(decoded);
|
||||
if (typeof candidate !== "string") return candidate;
|
||||
if (urlPathIsDirectory(url.pathname))
|
||||
return unresolvedCandidate("rejected", [
|
||||
`ESM package subpath ${JSON.stringify(input.declaredPath)} has URL directory form; directory imports are unsupported and no exact-file, package main or index lookup was attempted.`,
|
||||
]);
|
||||
const unrepresentableBase = metadataUrl.ok ? undefined : metadataUrl.error;
|
||||
if (
|
||||
unrepresentableBase !== undefined &&
|
||||
url.pathname
|
||||
.split("/")
|
||||
.some((segment) => unrepresentableBase.structuralSegments.has(segment))
|
||||
)
|
||||
return unresolvedCandidate("unavailable", [unrepresentableBase.limitation]);
|
||||
const file = resolveFileCandidates(input, [candidate]);
|
||||
if (file !== null) return file;
|
||||
const source = input.files.get(input.sourcePath);
|
||||
if (hasContainerDirectory(input.files, candidate, source?.container_sha256))
|
||||
if (
|
||||
hasContainerDirectory(
|
||||
input.files,
|
||||
candidate,
|
||||
source?.container_sha256,
|
||||
input.directoryPaths,
|
||||
input.unanalyzedFiles,
|
||||
)
|
||||
)
|
||||
return unresolvedCandidate("rejected", [
|
||||
`ESM package subpath ${candidate} names a directory; directory imports are unsupported and package main or index lookup was not attempted.`,
|
||||
]);
|
||||
@@ -849,6 +960,8 @@ const resolveExportTarget = (
|
||||
if (typeof decoded !== "string") return withTargetSource(decoded);
|
||||
const candidate = confineCandidate(decoded);
|
||||
if (typeof candidate !== "string") return withTargetSource(candidate);
|
||||
if (target.lookupLimitation !== undefined)
|
||||
return unresolvedCandidate("unavailable", [target.lookupLimitation]);
|
||||
return (
|
||||
resolveFileCandidates(input, [candidate]) ??
|
||||
withTargetSource(exactPackageTargetNotFound(candidate, mapKind))
|
||||
@@ -891,7 +1004,10 @@ const hasContainerDirectory = (
|
||||
files: ReadonlyMap<string, JavaScriptArtifactFile>,
|
||||
candidate: string,
|
||||
containerSha256: string | undefined,
|
||||
directoryPaths: ReadonlySet<string> | undefined,
|
||||
unanalyzedFiles?: JavaScriptUnanalyzedArtifactLookup,
|
||||
): boolean => {
|
||||
if (directoryPaths?.has(candidate) === true) return true;
|
||||
if (
|
||||
[...indexCandidates(candidate), posix.join(candidate, "package.json")].some(
|
||||
(path) => {
|
||||
@@ -915,7 +1031,7 @@ const hasContainerDirectory = (
|
||||
file.container_sha256 === containerSha256)
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
return unanalyzedFiles?.hasDirectory(candidate, containerSha256) ?? false;
|
||||
};
|
||||
|
||||
const htmlCandidate = (
|
||||
@@ -1055,6 +1171,7 @@ const readPackageMetadata = (
|
||||
const name = Reflect.get(value, "name");
|
||||
const exports = Reflect.get(value, "exports");
|
||||
const imports = Reflect.get(value, "imports");
|
||||
const main = Reflect.get(value, "main");
|
||||
return {
|
||||
kind: "available",
|
||||
path,
|
||||
@@ -1071,7 +1188,10 @@ const readPackageMetadata = (
|
||||
typeof imports === "object" && imports !== null && !Array.isArray(imports)
|
||||
? imports
|
||||
: undefined,
|
||||
main: Reflect.get(value, "main"),
|
||||
main:
|
||||
input.moduleKind !== undefined && typeof main !== "string"
|
||||
? undefined
|
||||
: main,
|
||||
module: Reflect.get(value, "module"),
|
||||
};
|
||||
};
|
||||
@@ -1149,12 +1269,20 @@ const resolveDirectory = (
|
||||
sourcePath: metadata.path,
|
||||
context: "package-entrypoint",
|
||||
files: input.files,
|
||||
...(input.directoryPaths === undefined
|
||||
? {}
|
||||
: { directoryPaths: input.directoryPaths }),
|
||||
...(input.unanalyzedFiles === undefined
|
||||
? {}
|
||||
: { unanalyzedFiles: input.unanalyzedFiles }),
|
||||
...(input.moduleKind === undefined ? {} : { moduleKind: input.moduleKind }),
|
||||
};
|
||||
const candidate = contextualCandidate(entryInput);
|
||||
if (typeof candidate !== "string") return candidate;
|
||||
const confined = confineCandidate(candidate);
|
||||
if (typeof confined !== "string") return confined;
|
||||
if (input.moduleKind !== undefined)
|
||||
return resolveNodePackageMain(entryInput, directory, confined);
|
||||
const selected =
|
||||
resolveFileCandidates(entryInput, [
|
||||
...fileCandidates(confined, entryInput.moduleKind),
|
||||
@@ -1162,9 +1290,6 @@ const resolveDirectory = (
|
||||
]) ??
|
||||
resolveFileCandidates(input, indexCandidates(directory, input.moduleKind));
|
||||
if (selected !== null) return selected;
|
||||
if (input.moduleKind === "require")
|
||||
return { ...notFoundCandidate(), stopsAncestorLookup: true };
|
||||
if (input.moduleKind !== undefined) return notFoundCandidate();
|
||||
return resolveCandidate(
|
||||
entryInput,
|
||||
confined,
|
||||
@@ -1172,32 +1297,115 @@ const resolveDirectory = (
|
||||
);
|
||||
};
|
||||
|
||||
const resolveNodePackageMain = (
|
||||
input: ResolveArtifactPathInput,
|
||||
directory: string,
|
||||
mainPath: string,
|
||||
): CandidateResolution => {
|
||||
const normalizedMain = mainPath.replace(/\/$/u, "");
|
||||
for (const suffix of NODE_PACKAGE_MAIN_SUFFIXES) {
|
||||
const isDirectoryIndex = suffix.startsWith("/");
|
||||
if (normalizedMain === "" && !isDirectoryIndex) continue;
|
||||
const selected = resolveFileCandidates(input, [
|
||||
isDirectoryIndex
|
||||
? posix.join(normalizedMain, suffix.slice(1))
|
||||
: posix.normalize(`${normalizedMain}${suffix}`),
|
||||
]);
|
||||
if (selected === null) continue;
|
||||
if (input.moduleKind === "require") return selected;
|
||||
// ESM probes the normalized main, then appends the selected suffix to
|
||||
// the original declaration before finalizing the resulting file path.
|
||||
const finalized = confineCandidate(
|
||||
relativeCandidate(input.sourcePath, `${input.declaredPath}${suffix}`),
|
||||
);
|
||||
if (typeof finalized !== "string") return finalized;
|
||||
const finalizedFile = resolveFileCandidates(input, [
|
||||
finalized.replace(/\/$/u, ""),
|
||||
]);
|
||||
if (finalizedFile !== null) return finalizedFile;
|
||||
const source = input.files.get(input.sourcePath);
|
||||
if (
|
||||
hasContainerDirectory(
|
||||
input.files,
|
||||
finalized,
|
||||
source?.container_sha256,
|
||||
input.directoryPaths,
|
||||
input.unanalyzedFiles,
|
||||
)
|
||||
)
|
||||
return {
|
||||
...unresolvedCandidate("rejected", [
|
||||
`The selected legacy ESM package main finalizes to directory ${finalized}; directory imports are unsupported and no later main or index candidate was tried.`,
|
||||
]),
|
||||
candidatePath: finalized,
|
||||
};
|
||||
return unresolvedCandidate("not-found", [
|
||||
`The selected legacy ESM package main finalizes to ${finalized}, which is not an inventoried file; no later main or index candidate was tried.`,
|
||||
]);
|
||||
}
|
||||
const fallback = resolveFileCandidates(
|
||||
input,
|
||||
indexCandidates(directory, input.moduleKind),
|
||||
);
|
||||
if (fallback !== null) return fallback;
|
||||
return input.moduleKind === "require"
|
||||
? { ...notFoundCandidate(), stopsAncestorLookup: true }
|
||||
: notFoundCandidate();
|
||||
};
|
||||
|
||||
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 isRuntimeDeclarationPath(input, path)
|
||||
? {
|
||||
resolvedPath: null,
|
||||
status: "unavailable",
|
||||
candidatePath: path,
|
||||
limitations: [
|
||||
`The TypeScript declaration ${path} is compile-time evidence and cannot identify a runtime module implementation.`,
|
||||
],
|
||||
}
|
||||
: { resolvedPath: path, status: "resolved", limitations: [] };
|
||||
const selected = findFileCandidate(input, path);
|
||||
if (selected === null) continue;
|
||||
return selected.status === "resolved" &&
|
||||
isRuntimeDeclarationPath(input, path)
|
||||
? {
|
||||
resolvedPath: null,
|
||||
status: "unavailable",
|
||||
candidatePath: path,
|
||||
limitations: [
|
||||
`The TypeScript declaration ${path} is compile-time evidence and cannot identify a runtime module implementation.`,
|
||||
],
|
||||
}
|
||||
: selected;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/** Own candidate presence and container identity independently of loader policy. */
|
||||
const findFileCandidate = (
|
||||
input: ResolveArtifactPathInput,
|
||||
path: string,
|
||||
): CandidateResolution | null => {
|
||||
const source = input.files.get(input.sourcePath);
|
||||
const target = input.files.get(path);
|
||||
if (
|
||||
target !== undefined &&
|
||||
(source === undefined ||
|
||||
target.container_sha256 === source.container_sha256)
|
||||
)
|
||||
return { resolvedPath: path, status: "resolved", limitations: [] };
|
||||
const unanalyzed = input.unanalyzedFiles?.get(path);
|
||||
if (
|
||||
unanalyzed !== undefined &&
|
||||
(source === undefined ||
|
||||
unanalyzed.container_sha256 === source.container_sha256)
|
||||
)
|
||||
return {
|
||||
resolvedPath: null,
|
||||
status: "unavailable",
|
||||
candidatePath: path,
|
||||
limitations: [
|
||||
...(unanalyzed.limitations ?? []),
|
||||
`${path} exists in the inventoried artifact container (sha256 ${unanalyzed.sha256 ?? "unknown"}, ${unanalyzed.bytes === null ? "unknown size" : `${String(unanalyzed.bytes)} bytes`}) but is not a JavaScript, JSON, HTML, source map, or native add-on file; its contents were not analyzed.`,
|
||||
],
|
||||
};
|
||||
return null;
|
||||
};
|
||||
|
||||
const isRuntimeDeclarationPath = (
|
||||
input: ResolveArtifactPathInput,
|
||||
path: string,
|
||||
@@ -1205,6 +1413,11 @@ const isRuntimeDeclarationPath = (
|
||||
input.moduleKind !== undefined && /\.d\.(?:ts|mts|cts)$/iu.test(path);
|
||||
|
||||
const NODE_MODULE_EXTENSIONS = [".js", ".json", ".node"] as const;
|
||||
const NODE_PACKAGE_MAIN_SUFFIXES = [
|
||||
"",
|
||||
...NODE_MODULE_EXTENSIONS,
|
||||
...NODE_MODULE_EXTENSIONS.map((extension) => `/index${extension}`),
|
||||
];
|
||||
|
||||
const fileCandidates = (
|
||||
candidate: string,
|
||||
@@ -1264,7 +1477,7 @@ const selectPackageExport = (
|
||||
const selected = selectExportTarget(
|
||||
target,
|
||||
packageExportConditions(moduleKind),
|
||||
packageMetadataUrl(packagePath),
|
||||
packagePath,
|
||||
);
|
||||
if (selected.kind === "invalid" || selected.kind === "invalid-config")
|
||||
return { status: "rejected", limitation: selected.limitation };
|
||||
@@ -1284,6 +1497,11 @@ const selectPackageExport = (
|
||||
status: "value",
|
||||
value: selected.value,
|
||||
url: selected.url,
|
||||
...(selected.lookupLimitation === undefined
|
||||
? {}
|
||||
: {
|
||||
lookupLimitation: selected.lookupLimitation,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1323,7 +1541,7 @@ const forbiddenExportSegment = (value: string): string | undefined =>
|
||||
|
||||
const selectStringPackageTarget = (
|
||||
value: string,
|
||||
packageUrl: URL,
|
||||
packagePath: string,
|
||||
mapKind: "exports" | "imports",
|
||||
): ExportTargetOutcome => {
|
||||
if (mapKind === "imports") {
|
||||
@@ -1344,6 +1562,10 @@ const selectStringPackageTarget = (
|
||||
`contains forbidden path segment ${JSON.stringify(forbidden)} before URL normalization`,
|
||||
mapKind,
|
||||
);
|
||||
const metadataUrl = packageMetadataUrl(packagePath, value);
|
||||
const packageUrl = metadataUrl.ok
|
||||
? metadataUrl.value
|
||||
: metadataUrl.error.structuralUrl;
|
||||
// Node constructs the URL before deciding whether this array entry is an
|
||||
// invalid package target. Tabs/newlines and dot segments normalize here;
|
||||
// percent decoding and existence checks belong after target selection.
|
||||
@@ -1358,13 +1580,18 @@ const selectStringPackageTarget = (
|
||||
kind: "value",
|
||||
value,
|
||||
url,
|
||||
...(metadataUrl.ok
|
||||
? {}
|
||||
: {
|
||||
lookupLimitation: metadataUrl.error.limitation,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const selectArrayPackageTarget = (
|
||||
value: readonly unknown[],
|
||||
conditions: ReadonlySet<string>,
|
||||
packageUrl: URL,
|
||||
packagePath: string,
|
||||
mapKind: "exports" | "imports",
|
||||
): ExportTargetOutcome => {
|
||||
if (value.length === 0) return { kind: "blocked", cause: "empty-array" };
|
||||
@@ -1373,7 +1600,7 @@ const selectArrayPackageTarget = (
|
||||
| undefined;
|
||||
const declared = new Set<string>();
|
||||
for (const entry of value) {
|
||||
const nested = selectExportTarget(entry, conditions, packageUrl, mapKind);
|
||||
const nested = selectExportTarget(entry, conditions, packagePath, mapKind);
|
||||
// Node's array fallback catches invalid targets, not invalid configuration.
|
||||
if (nested.kind === "invalid-config") return nested;
|
||||
if (nested.kind === "unmatched") {
|
||||
@@ -1399,14 +1626,14 @@ const selectArrayPackageTarget = (
|
||||
const selectExportTarget = (
|
||||
value: unknown,
|
||||
conditions: ReadonlySet<string>,
|
||||
packageUrl: URL,
|
||||
packagePath: string,
|
||||
mapKind: "exports" | "imports" = "exports",
|
||||
): ExportTargetOutcome => {
|
||||
if (typeof value === "string")
|
||||
return selectStringPackageTarget(value, packageUrl, mapKind);
|
||||
return selectStringPackageTarget(value, packagePath, mapKind);
|
||||
if (value === null) return { kind: "blocked", cause: "null" };
|
||||
if (Array.isArray(value))
|
||||
return selectArrayPackageTarget(value, conditions, packageUrl, mapKind);
|
||||
return selectArrayPackageTarget(value, conditions, packagePath, mapKind);
|
||||
if (typeof value !== "object")
|
||||
return invalidExportTarget(
|
||||
value,
|
||||
@@ -1425,7 +1652,7 @@ const selectExportTarget = (
|
||||
for (const [condition, target] of Object.entries(value)) {
|
||||
declared.add(condition);
|
||||
if (!conditions.has(condition)) continue;
|
||||
const nested = selectExportTarget(target, conditions, packageUrl, mapKind);
|
||||
const nested = selectExportTarget(target, conditions, packagePath, mapKind);
|
||||
if (nested.kind !== "unmatched") return nested;
|
||||
for (const name of nested.declared) declared.add(name);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user