fix(javascript): preserve package and source-map boundary identity (#1939)
* fix(javascript): preserve bare package specifier suffixes * fix(javascript): resolve ESM package self references * fix(javascript): recover retained prefixed source maps * test(javascript): cover suffixed ESM self references * fix(javascript): retain legacy ESM package suffix behavior * fix(javascript): reject legacy ESM directory and extension fallback * refactor(javascript): consolidate package resolution and workflow coverage * fix(javascript): report exact ESM package subpath misses * fix(javascript): compare parsed names for ESM self references * refactor(javascript): parse bare package requests once * fix(javascript): derive ESM dependency lookup from package URLs --------- Co-authored-by: hhang <169027046+xu91102@users.noreply.github.com>
This commit is contained in:
1 parent
85c1d2587a
commit
14935e4d47
13 files changed
+860
-157
No files matched your search
@@ -176,9 +176,7 @@ const contextualCandidate = (
|
||||
): string | CandidateResolution => {
|
||||
const { context } = input;
|
||||
let declared =
|
||||
context === "html-reference" ||
|
||||
context === "url-reference" ||
|
||||
(context === "module-specifier" && input.moduleKind !== "require")
|
||||
context === "html-reference" || context === "url-reference"
|
||||
? stripQueryAndFragment(input.declaredPath)
|
||||
: input.declaredPath;
|
||||
if (context === "html-reference") return htmlCandidate(input);
|
||||
@@ -190,9 +188,9 @@ const contextualCandidate = (
|
||||
"URL and Node builtin schemes are outside static artifact module resolution.",
|
||||
]);
|
||||
if (!declared.startsWith(".") && !declared.startsWith("/"))
|
||||
return bareModuleCandidate(input, declared);
|
||||
return bareModuleCandidate(input);
|
||||
if (input.moduleKind !== "require") {
|
||||
const decoded = decodeModuleUrlPath(declared);
|
||||
const decoded = decodeModuleUrlPath(stripQueryAndFragment(declared));
|
||||
if (typeof decoded !== "string") return decoded;
|
||||
declared = decoded;
|
||||
}
|
||||
@@ -237,34 +235,51 @@ const relativeCandidate = (sourcePath: string, declared: string): string =>
|
||||
|
||||
const bareModuleCandidate = (
|
||||
input: ResolveArtifactPathInput,
|
||||
declared: string,
|
||||
): CandidateResolution => {
|
||||
const packageName = barePackageName(declared);
|
||||
if (packageName === null || isBuiltin(declared))
|
||||
const request = parseBarePackageRequest(input.declaredPath, input.moduleKind);
|
||||
if (request === null || isBuiltin(input.declaredPath))
|
||||
return unresolvedCandidate("external", [
|
||||
"The bare specifier is a Node builtin or invalid package name.",
|
||||
]);
|
||||
if (input.moduleKind === "import" && packageName.includes("%"))
|
||||
if (input.moduleKind === "import" && request.packageName.includes("%"))
|
||||
return unresolvedCandidate("rejected", [
|
||||
"Percent signs are not admitted in ES module package names.",
|
||||
]);
|
||||
const self = commonJsSelfCandidate(input, declared);
|
||||
const self = packageSelfCandidate(input, request);
|
||||
if (self !== null) return self;
|
||||
const subpath =
|
||||
declared === packageName ? "." : `.${declared.slice(packageName.length)}`;
|
||||
// ESM constructs a package.json URL; its parsed identity is not a literal
|
||||
// dependency directory. Self resolution above still uses the exact name.
|
||||
let dependencyDirectory = posix.join("node_modules", request.packageName);
|
||||
if (input.moduleKind === "import") {
|
||||
const metadataUrl = new URL(
|
||||
`./node_modules/${request.packageName}/package.json`,
|
||||
PACKAGE_ARTIFACT_ROOT_URL,
|
||||
);
|
||||
if (!metadataUrl.pathname.endsWith("/package.json"))
|
||||
return unresolvedCandidate("external", [
|
||||
`ESM package name ${JSON.stringify(request.packageName)} produces a dependency URL whose pathname does not name package.json; no literal or suffix-stripped package directory was searched.`,
|
||||
]);
|
||||
const decoded = decodeModuleUrlPath(
|
||||
metadataUrl.pathname.slice(PACKAGE_ARTIFACT_ROOT_URL.pathname.length),
|
||||
);
|
||||
if (typeof decoded !== "string") return decoded;
|
||||
const confined = confineCandidate(decoded);
|
||||
if (typeof confined !== "string") return confined;
|
||||
dependencyDirectory = posix.dirname(confined);
|
||||
}
|
||||
const source = input.files.get(input.sourcePath);
|
||||
let directory = posix.dirname(input.sourcePath);
|
||||
while (true) {
|
||||
const candidate = posix.join(directory, "node_modules", packageName);
|
||||
const candidate = posix.join(directory, dependencyDirectory);
|
||||
const metadata = readPackageMetadata(input, candidate);
|
||||
if (packageExportsEligible(input) && metadata.kind !== "absent")
|
||||
return resolvePackageSpecifier(input, candidate, subpath, metadata);
|
||||
if (subpath === ".") {
|
||||
if (request.dependencyExportsEligible && metadata.kind !== "absent")
|
||||
return resolvePackageDependency(input, candidate, request, metadata);
|
||||
if (request.packageSubpath === ".") {
|
||||
const direct = resolveFileCandidates(input, fileCandidates(candidate));
|
||||
if (direct !== null) return direct;
|
||||
}
|
||||
if (hasContainerDirectory(input.files, candidate, source?.container_sha256))
|
||||
return resolvePackageSpecifier(input, candidate, subpath, metadata);
|
||||
return resolvePackageDependency(input, candidate, request, metadata);
|
||||
if (directory === "." || directory === "") break;
|
||||
directory = posix.dirname(directory);
|
||||
}
|
||||
@@ -330,24 +345,25 @@ const resolvePackageImportAlias = (
|
||||
return unresolvedCandidate("external", [
|
||||
`Wildcard package import aliases remain unresolved; package metadata ${metadata.path} has no exact imports key ${specifier}.`,
|
||||
]);
|
||||
const selected = selectPackageImportTarget(
|
||||
const selected = selectExportTarget(
|
||||
lookup.target,
|
||||
input.moduleKind,
|
||||
metadata.path,
|
||||
packageExportConditions(input.moduleKind),
|
||||
packageMetadataUrl(metadata.path),
|
||||
"imports",
|
||||
);
|
||||
if (selected.status === "rejected")
|
||||
if (selected.kind === "invalid" || selected.kind === "invalid-config")
|
||||
return unresolvedCandidate("rejected", [
|
||||
`Package metadata ${metadata.path}: ${selected.limitation}`,
|
||||
]);
|
||||
if (selected.status === "unmatched")
|
||||
if (selected.kind === "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")
|
||||
if (selected.kind === "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") {
|
||||
if (selected.kind === "non-relative") {
|
||||
const target = classifyPackageImportTargetString(selected.value);
|
||||
return unresolvedCandidate("external", [
|
||||
target.kind === "recursive"
|
||||
@@ -358,77 +374,44 @@ const resolvePackageImportAlias = (
|
||||
return resolveExportTarget(input, metadata.path, selected, "imports");
|
||||
};
|
||||
|
||||
const selectPackageImportTarget = (
|
||||
value: unknown,
|
||||
moduleKind: ResolveArtifactPathInput["moduleKind"],
|
||||
packagePath: string,
|
||||
):
|
||||
| ({ readonly status: "value" } & PackageFileTarget)
|
||||
| { 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),
|
||||
packageMetadataUrl(packagePath),
|
||||
"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,
|
||||
url: selected.url,
|
||||
};
|
||||
};
|
||||
|
||||
/** Self references use the nearest package scope before dependency lookup. */
|
||||
const commonJsSelfCandidate = (
|
||||
const packageSelfCandidate = (
|
||||
input: ResolveArtifactPathInput,
|
||||
declared: string,
|
||||
request: BarePackageRequest,
|
||||
): CandidateResolution | null => {
|
||||
if (input.moduleKind !== "require" || !input.files.has(input.sourcePath))
|
||||
if (input.moduleKind === undefined || !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}/`))
|
||||
)
|
||||
if (metadata.name === undefined || metadata.exports === undefined)
|
||||
return null;
|
||||
const subpath =
|
||||
declared === metadata.name
|
||||
? "."
|
||||
: `.${declared.slice(metadata.name.length)}`;
|
||||
let subpath: string;
|
||||
if (input.moduleKind === "import") {
|
||||
if (metadata.name !== request.packageName) return null;
|
||||
subpath = request.packageSubpath;
|
||||
} else if (input.declaredPath === metadata.name) {
|
||||
subpath = ".";
|
||||
} else if (input.declaredPath.startsWith(`${metadata.name}/`)) {
|
||||
subpath = `.${input.declaredPath.slice(metadata.name.length)}`;
|
||||
} else return null;
|
||||
// Self resolution does not apply the CommonJS dependency-name matcher.
|
||||
return resolvePackageExports(input, metadata.path, subpath, metadata.exports);
|
||||
};
|
||||
|
||||
/** Package roots and exact subpaths share metadata and exports selection. */
|
||||
const resolvePackageSpecifier = (
|
||||
/** Resolve an installed dependency using one parsed request and its metadata. */
|
||||
const resolvePackageDependency = (
|
||||
input: ResolveArtifactPathInput,
|
||||
directory: string,
|
||||
subpath: string,
|
||||
request: BarePackageRequest,
|
||||
metadata: PackageMetadata,
|
||||
): CandidateResolution => {
|
||||
const exportsEligible = packageExportsEligible(input);
|
||||
if (exportsEligible && metadata.kind === "unavailable")
|
||||
const subpath = request.packageSubpath;
|
||||
if (request.dependencyExportsEligible && metadata.kind === "unavailable")
|
||||
return unresolvedCandidate("unavailable", [metadata.limitation]);
|
||||
const rawExports =
|
||||
exportsEligible && metadata.kind === "available"
|
||||
request.dependencyExportsEligible && metadata.kind === "available"
|
||||
? metadata.exports
|
||||
: undefined;
|
||||
if (rawExports === undefined) {
|
||||
@@ -437,8 +420,17 @@ const resolvePackageSpecifier = (
|
||||
resolveFileCandidates(input, fileCandidates(directory)) ??
|
||||
resolveDirectory(input, directory, metadata, new Set())
|
||||
);
|
||||
// Preserve the trailing slash for a package directory request.
|
||||
const candidate = confineCandidate(`${directory}/${subpath.slice(2)}`);
|
||||
// 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);
|
||||
return typeof candidate === "string"
|
||||
? resolveCandidate(input, candidate, new Set())
|
||||
: candidate;
|
||||
@@ -451,6 +443,21 @@ const resolvePackageSpecifier = (
|
||||
);
|
||||
};
|
||||
|
||||
/** Legacy ESM package subpaths are exact URLs, never extension or directory lookups. */
|
||||
const resolveLegacyEsmSubpath = (
|
||||
input: ResolveArtifactPathInput,
|
||||
candidate: string,
|
||||
): CandidateResolution => {
|
||||
const file = resolveFileCandidates(input, [candidate]);
|
||||
if (file !== null) return file;
|
||||
const source = input.files.get(input.sourcePath);
|
||||
if (hasContainerDirectory(input.files, candidate, source?.container_sha256))
|
||||
return unresolvedCandidate("rejected", [
|
||||
`ESM package subpath ${candidate} names a directory; directory imports are unsupported and package main or index lookup was not attempted.`,
|
||||
]);
|
||||
return exactPackageTargetNotFound(candidate, "ESM subpath");
|
||||
};
|
||||
|
||||
const resolvePackageExports = (
|
||||
input: ResolveArtifactPathInput,
|
||||
packagePath: string,
|
||||
@@ -517,23 +524,36 @@ const resolveExportTarget = (
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
/** Derived package identity and dependency policy; input retains the raw request. */
|
||||
interface BarePackageRequest {
|
||||
readonly packageName: string;
|
||||
readonly packageSubpath: string;
|
||||
readonly dependencyExportsEligible: boolean;
|
||||
}
|
||||
|
||||
const packageExportsEligible = (input: ResolveArtifactPathInput): boolean => {
|
||||
if (input.moduleKind !== "require") return true;
|
||||
const parseBarePackageRequest = (
|
||||
specifier: string,
|
||||
moduleKind: ResolveArtifactPathInput["moduleKind"],
|
||||
): BarePackageRequest | null => {
|
||||
const segments = specifier.split("/");
|
||||
let packageName = segments[0] ?? "";
|
||||
if (packageName === "") return null;
|
||||
if (specifier.startsWith("@")) {
|
||||
const packageSegment = segments[1];
|
||||
if (packageSegment === undefined || packageSegment === "") return null;
|
||||
packageName = `${packageName}/${packageSegment}`;
|
||||
}
|
||||
// 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);
|
||||
const dependencyExportsEligible =
|
||||
moduleKind !== "require" ||
|
||||
/^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/.exec(specifier)?.[1] ===
|
||||
packageName;
|
||||
return {
|
||||
packageName,
|
||||
packageSubpath: `.${specifier.slice(packageName.length)}`,
|
||||
dependencyExportsEligible,
|
||||
};
|
||||
};
|
||||
|
||||
const hasContainerDirectory = (
|
||||
@@ -1061,12 +1081,12 @@ const notFoundCandidate = (): CandidateResolution => ({
|
||||
|
||||
const exactPackageTargetNotFound = (
|
||||
path: string,
|
||||
mapKind: "exports" | "imports",
|
||||
targetKind: "exports" | "imports" | "ESM subpath",
|
||||
): CandidateResolution => ({
|
||||
resolvedPath: null,
|
||||
status: "not-found",
|
||||
limitations: [
|
||||
`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.`,
|
||||
`The selected package ${targetKind === "imports" ? "import" : targetKind} target ${path} does not exist in the inventoried artifact container; ${targetKind === "imports" ? "import" : targetKind} targets are exact files, so no extension or directory index was tried.`,
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user