fix(javascript): honor package exports before sibling files

Resolve package exports before sibling-file fallback, preserve explicit export blocking, and retain precise CommonJS package self-reference behavior and caller provenance.

Fixes #1761.

Co-authored-by: Yann Roberto <165009858+shoyann@users.noreply.github.com>
This commit is contained in:
Yann Roberto
2026-10-11 18:57:58 +08:00
committed by GitHub
parent 847a1a3f25
commit 3ad5782dfd
3 changed files with 755 additions and 101 deletions
@@ -206,18 +206,27 @@ const bareModuleCandidate = (
return unresolvedCandidate("external", [
"The bare specifier is a Node builtin, package import map, or invalid package name.",
]);
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;
const subpath =
declared === packageName ? "." : `.${declared.slice(packageName.length)}`;
const source = input.files.get(input.sourcePath);
let directory = posix.dirname(input.sourcePath);
while (true) {
const candidate = posix.join(directory, "node_modules", packageName);
const metadata = readPackageMetadata(input, candidate);
if (packageExportsEligible(input) && metadata.kind !== "absent")
return resolvePackageSpecifier(input, candidate, subpath, metadata);
if (subpath === ".") {
const direct = resolveFileCandidates(input, fileCandidates(candidate));
if (direct !== null) return direct;
}
if (hasContainerDirectory(input.files, candidate, source?.container_sha256))
return resolvePackageSpecifier(input, candidate, subpath);
return resolvePackageSpecifier(input, candidate, subpath, metadata);
if (directory === "." || directory === "") break;
directory = posix.dirname(directory);
}
@@ -226,30 +235,85 @@ const bareModuleCandidate = (
]);
};
/** 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;
let directory = posix.dirname(input.sourcePath);
while (posix.basename(directory) !== "node_modules") {
const metadata = readPackageMetadata(input, directory);
if (metadata.kind === "unavailable")
return unresolvedCandidate("unavailable", [metadata.limitation]);
if (metadata.kind === "available") {
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,
);
}
if (directory === "." || directory === "") break;
directory = posix.dirname(directory);
}
return null;
};
/** Package roots and exact subpaths share metadata and exports selection. */
const resolvePackageSpecifier = (
input: ResolveArtifactPathInput,
directory: string,
subpath: string,
metadata: PackageMetadata,
): CandidateResolution => {
const metadata = readPackageMetadata(input, directory);
if (metadata.kind === "unavailable")
const exportsEligible = packageExportsEligible(input);
if (exportsEligible && metadata.kind === "unavailable")
return unresolvedCandidate("unavailable", [metadata.limitation]);
const rawExports =
metadata.kind === "available"
? Reflect.get(metadata.value, "exports")
exportsEligible && metadata.kind === "available"
? metadata.exports
: undefined;
if (rawExports === undefined || rawExports === null) {
if (rawExports === undefined) {
if (subpath === ".")
return resolveDirectory(input, directory, metadata, new Set());
return (
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)}`);
return typeof candidate === "string"
? resolveCandidate(input, candidate, new Set())
: candidate;
}
return resolvePackageExports(
input,
posix.join(directory, "package.json"),
subpath,
rawExports,
);
};
const resolvePackageExports = (
input: ResolveArtifactPathInput,
packagePath: string,
subpath: string,
rawExports: string | object,
): CandidateResolution => {
const exported = selectPackageExport(rawExports, subpath, input.moduleKind);
const packagePath = posix.join(directory, "package.json");
if (exported.status === "rejected")
return unresolvedCandidate("rejected", [
`Package metadata ${packagePath}: ${exported.limitation}`,
@@ -292,6 +356,16 @@ const barePackageName = (specifier: string): string | null => {
return segments[0] === "" ? null : (segments[0] ?? null);
};
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);
};
const hasContainerDirectory = (
files: ReadonlyMap<string, JavaScriptArtifactFile>,
candidate: string,
@@ -416,7 +490,10 @@ type PackageMetadata =
| {
readonly kind: "available";
readonly path: string;
readonly value: object;
readonly name: string | undefined;
readonly exports: string | object | undefined;
readonly main: unknown;
readonly module: unknown;
}
| { readonly kind: "absent" }
| { readonly kind: "unavailable"; readonly limitation: string };
@@ -448,12 +525,27 @@ const readPackageMetadata = (
limitation: `Directory package metadata ${path} is not valid package JSON.`,
};
}
return typeof value === "object" && value !== null
? { kind: "available", path, value }
: {
kind: "unavailable",
limitation: `Directory package metadata ${path} is not valid package JSON.`,
};
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");
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,
main: Reflect.get(value, "main"),
module: Reflect.get(value, "module"),
};
};
/** File and directory lookup never interprets a nested directory's exports. */
@@ -488,10 +580,8 @@ const resolveDirectory = (
`Directory package entrypoint cycle includes ${metadata.path}.`,
]);
const preferred = [
Reflect.get(metadata.value, "main"),
...(input.moduleKind === undefined
? [Reflect.get(metadata.value, "module")]
: []),
metadata.main,
...(input.moduleKind === undefined ? [metadata.module] : []),
];
const entry = preferred.find((value) => value !== undefined);
if (entry === undefined)
@@ -11,55 +11,88 @@ import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const createPackageSubpathFixture = async () => {
const root = await createTestTempDirectory("rea-package-subpaths-");
const source = [
'const nearest = require("fixture/parser");',
'const nested = require("nested/parser");',
'const parser = require("exported/parser");',
'const directory = require("exported/parser/");',
'const hidden = require("exported/hidden");',
'const missing = require("exported/missing");',
'new BrowserWindow({webPreferences:{preload:"exported/parser"}});',
'const file = require("fixture");',
'const packageDirectory = require("fixture/");',
'const selected = require("@scope/selected");',
'const blocked = require("blocked");',
'const literal = require("fi%xture");',
'const scalar = require("scalar");',
'const self = require("../node_modules/fi%xture/run.cjs");',
].join("\n");
const selfSource = 'const self = require("fi%xture"); module.exports = self;';
const importSource = 'import literal from "fi%xture";';
const files = {
"src/main.cjs": source,
"src/imports.mjs": importSource,
"src/node_modules/fixture/parser.js": "module.exports = 1;",
"src/node_modules/fixture.js": "module.exports = 'file';",
"src/node_modules/fixture/index.js": "module.exports = 'directory';",
"node_modules/fixture/package.json": "{}",
"node_modules/fixture/parser.js": "module.exports = 2;",
"node_modules/nested/package.json": "{}",
"node_modules/nested/parser/package.json": JSON.stringify({
main: "./entry.js",
exports: "./decoy.js",
}),
"node_modules/nested/parser/entry.js": "module.exports = 3;",
"node_modules/nested/parser/index.js": "module.exports = 4;",
"node_modules/nested/parser/decoy.js": "module.exports = 5;",
"node_modules/exported/package.json": JSON.stringify({
exports: {
"./parser": "./parser.cjs",
"./parser/": "./parser.cjs",
"./missing": "./missing.cjs",
},
}),
"node_modules/exported/parser.cjs": "module.exports = 6;",
"node_modules/exported/hidden.js": "module.exports = 7;",
"node_modules/exported/missing/index.js": "module.exports = 8;",
"node_modules/@scope/selected.js": "module.exports = 'sibling';",
"node_modules/@scope/selected/package.json": JSON.stringify({
exports: { require: "./actual.cjs" },
}),
"node_modules/@scope/selected/actual.cjs": "module.exports = 'export';",
"node_modules/blocked.js": "module.exports = 'sibling';",
"node_modules/blocked/package.json": JSON.stringify({
exports: { ".": null },
}),
"node_modules/fi%xture.js": "module.exports = 'literal sibling';",
"node_modules/fi%xture/package.json": JSON.stringify({
name: "fi%xture",
exports: "./actual.cjs",
main: "./legacy.cjs",
}),
"node_modules/fi%xture/actual.cjs": "module.exports = 'export';",
"node_modules/fi%xture/legacy.cjs": "module.exports = 'legacy';",
"node_modules/fi%xture/run.cjs": selfSource,
"node_modules/scalar.js": "module.exports = 'scalar sibling';",
"node_modules/scalar/package.json": JSON.stringify({ exports: false }),
};
await Promise.all(
Object.entries(files).map(async ([path, content]) => {
const absolute = join(root, path);
await mkdir(dirname(absolute), { recursive: true });
await writeFile(absolute, content);
}),
);
return { root, source, selfSource, importSource };
};
cliTest.for(["CLI", "stdio MCP"] as const)(
"resolves package subpaths with source provenance through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-package-subpaths-");
const source = [
'const nearest = require("fixture/parser");',
'const nested = require("nested/parser");',
'const parser = require("exported/parser");',
'const directory = require("exported/parser/");',
'const hidden = require("exported/hidden");',
'const missing = require("exported/missing");',
'new BrowserWindow({webPreferences:{preload:"exported/parser"}});',
'const file = require("fixture");',
'const packageDirectory = require("fixture/");',
].join("\n");
const files = {
"src/main.cjs": source,
"src/node_modules/fixture/parser.js": "module.exports = 1;",
"src/node_modules/fixture.js": "module.exports = 'file';",
"src/node_modules/fixture/index.js": "module.exports = 'directory';",
"node_modules/fixture/package.json": "{}",
"node_modules/fixture/parser.js": "module.exports = 2;",
"node_modules/nested/package.json": "{}",
"node_modules/nested/parser/package.json": JSON.stringify({
main: "./entry.js",
exports: "./decoy.js",
}),
"node_modules/nested/parser/entry.js": "module.exports = 3;",
"node_modules/nested/parser/index.js": "module.exports = 4;",
"node_modules/nested/parser/decoy.js": "module.exports = 5;",
"node_modules/exported/package.json": JSON.stringify({
exports: {
"./parser": "./parser.cjs",
"./parser/": "./parser.cjs",
"./missing": "./missing.cjs",
},
}),
"node_modules/exported/parser.cjs": "module.exports = 6;",
"node_modules/exported/hidden.js": "module.exports = 7;",
"node_modules/exported/missing/index.js": "module.exports = 8;",
};
await Promise.all(
Object.entries(files).map(async ([path, content]) => {
const absolute = join(root, path);
await mkdir(dirname(absolute), { recursive: true });
await writeFile(absolute, content);
}),
);
const { root, source, selfSource, importSource } =
await createPackageSubpathFixture();
const nativeRequire = createRequire(join(root, "src/main.cjs"));
const expectedTargets = {
"fixture/parser": "src/node_modules/fixture/parser.js",
@@ -67,18 +100,29 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
"fixture/": "src/node_modules/fixture/index.js",
"nested/parser": "node_modules/nested/parser/entry.js",
"exported/parser": "node_modules/exported/parser.cjs",
"@scope/selected": "node_modules/@scope/selected/actual.cjs",
"fi%xture": "node_modules/fi%xture.js",
scalar: "node_modules/scalar.js",
"../node_modules/fi%xture/run.cjs": "node_modules/fi%xture/run.cjs",
};
for (const [specifier, target] of Object.entries(expectedTargets))
expect(
relative(root, nativeRequire.resolve(specifier)).replaceAll("\\", "/"),
).toBe(target);
for (const specifier of ["exported/parser/", "exported/hidden"])
for (const specifier of ["exported/parser/", "exported/hidden", "blocked"])
expect(() => nativeRequire.resolve(specifier)).toThrow(
expect.objectContaining({ code: "ERR_PACKAGE_PATH_NOT_EXPORTED" }),
);
expect(() => nativeRequire.resolve("exported/missing")).toThrow(
expect.objectContaining({ code: "MODULE_NOT_FOUND" }),
);
const selfPath = "node_modules/fi%xture/run.cjs";
expect(
relative(
root,
createRequire(join(root, selfPath)).resolve("fi%xture"),
).replaceAll("\\", "/"),
).toBe("node_modules/fi%xture/actual.cjs");
let document: unknown;
if (surface === "CLI") {
@@ -103,34 +147,31 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
const imports = graph.edges.filter(
({ properties }) => properties.module_link_kind === "require",
);
const sourceDigest = createHash("sha256").update(source).digest("hex");
for (const [specifier, target] of Object.entries(expectedTargets))
expect(imports).toContainEqual(
expect.objectContaining({
properties: expect.objectContaining({
specifier,
resolved_path: target,
resolution_status: "resolved",
}),
evidence: expect.objectContaining({
artifact: expect.objectContaining({
available: true,
sha256: sourceDigest,
}),
location: expect.objectContaining({
available: true,
value: expect.objectContaining({
kind: "source-range",
source: "src/main.cjs",
}),
}),
}),
for (const observation of [
{ sourcePath: "src/main.cjs", source, targets: expectedTargets },
{
sourcePath: selfPath,
source: selfSource,
targets: { "fi%xture": "node_modules/fi%xture/actual.cjs" },
},
])
expectResolvedImports(imports, observation);
expect(graph.edges).toContainEqual(
expect.objectContaining({
properties: expect.objectContaining({
module_link_kind: "import",
specifier: "fi%xture",
resolved_path: null,
resolution_status: "rejected",
}),
);
evidence: sourceEvidence("src/imports.mjs", importSource),
}),
);
for (const [specifier, status] of [
["exported/parser/", "external"],
["exported/hidden", "external"],
["exported/missing", "not-found"],
["blocked", "external"],
])
expect(imports).toContainEqual(
expect.objectContaining({
@@ -164,3 +205,41 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
});
},
);
const expectResolvedImports = (
imports: z.infer<
typeof javascriptApplicationAnalysisResultSchema
>["graph"]["edges"],
observation: {
readonly sourcePath: string;
readonly source: string;
readonly targets: Readonly<Record<string, string>>;
},
): void => {
for (const [specifier, target] of Object.entries(observation.targets))
expect.soft(imports).toContainEqual(
expect.objectContaining({
properties: expect.objectContaining({
specifier,
resolved_path: target,
resolution_status: "resolved",
}),
evidence: sourceEvidence(observation.sourcePath, observation.source),
}),
);
};
const sourceEvidence = (sourcePath: string, source: string): unknown =>
expect.objectContaining({
artifact: expect.objectContaining({
available: true,
sha256: createHash("sha256").update(source).digest("hex"),
}),
location: expect.objectContaining({
available: true,
value: expect.objectContaining({
kind: "source-range",
source: sourcePath,
}),
}),
});
@@ -1,6 +1,6 @@
import { execFile } from "node:child_process";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { describe, expect, it } from "vitest";
import { z } from "zod";
@@ -25,11 +25,15 @@ const nodeExportOutcomeSchema = z.strictObject({
});
/** Resolve through the real Node resolver, retaining its refusal reason. */
const nodeTarget = async (cwd: string, moduleKind: "import" | "require") => {
const nodeTarget = async (
cwd: string,
moduleKind: "import" | "require",
specifier: string,
) => {
const expression =
moduleKind === "import"
? '(await import("example")).default'
: 'createRequire(import.meta.url)("example")';
? `(await import(${JSON.stringify(specifier)})).default`
: `createRequire(import.meta.url)(${JSON.stringify(specifier)})`;
const { stdout } = await execute(
process.execPath,
[
@@ -56,24 +60,58 @@ const compareWithNode = async (
exportsMap: unknown,
moduleKind: "import" | "require",
present: readonly string[] = TARGETS,
options: {
readonly siblingFile?: boolean;
readonly packageName?: string;
readonly specifier?: string;
readonly main?: string;
readonly metadataText?: string;
readonly sourceWithinPackage?: string;
readonly packageFiles?: Readonly<Record<string, string>>;
} = {},
): Promise<{
readonly node: string | null;
readonly node_error: string | null;
readonly outcome: ArtifactPathResolution;
readonly files: ReadonlyMap<string, JavaScriptArtifactFile>;
}> => {
const root = await createTestTempDirectory("rea-exports-");
const packageDirectory = join(root, "node_modules", "example");
const packageName = options.packageName ?? "example";
const specifier = options.specifier ?? packageName;
const sourcePath =
options.sourceWithinPackage === undefined
? "main.js"
: `node_modules/${packageName}/${options.sourceWithinPackage}`;
const packageDirectory = join(root, "node_modules", packageName);
await mkdir(packageDirectory, { recursive: true });
const metadata = JSON.stringify({ name: "example", exports: exportsMap });
const metadata =
options.metadataText ??
JSON.stringify({
name: packageName,
exports: exportsMap,
main: options.main,
});
await writeFile(join(packageDirectory, "package.json"), metadata);
const contents = new Map<string, string>([
["main.js", ""],
["node_modules/example/package.json", metadata],
[sourcePath, ""],
[`node_modules/${packageName}/package.json`, metadata],
]);
if (options.siblingFile === true) {
const filename = `${packageName}.js`;
const source = `module.exports = ${JSON.stringify(filename)};`;
await writeFile(join(root, "node_modules", filename), source);
contents.set(`node_modules/${filename}`, source);
}
for (const target of present) {
const source = `module.exports = ${JSON.stringify(target)};`;
await writeFile(join(packageDirectory, target), source);
contents.set(`node_modules/example/${target}`, source);
contents.set(`node_modules/${packageName}/${target}`, source);
}
for (const [path, source] of Object.entries(options.packageFiles ?? {})) {
const absolute = join(packageDirectory, path);
await mkdir(dirname(absolute), { recursive: true });
await writeFile(absolute, source);
contents.set(`node_modules/${packageName}/${path}`, source);
}
const files = new Map<string, JavaScriptArtifactFile>(
[...contents].map(([path, value]) => [
@@ -90,13 +128,18 @@ const compareWithNode = async (
},
]),
);
const node = await nodeTarget(root, moduleKind);
const node = await nodeTarget(
dirname(join(root, sourcePath)),
moduleKind,
specifier,
);
return {
node: node.target,
node_error: node.error_code,
files,
outcome: resolveArtifactPathByContext({
declaredPath: "example",
sourcePath: "main.js",
declaredPath: specifier,
sourcePath,
context: "module-specifier",
moduleKind,
files,
@@ -209,6 +252,448 @@ describe("ordered package exports conditions", () => {
},
);
});
describe("package exports before sibling files", () => {
it.each(["import", "require"] as const)(
"selects the active %s export before a same-named sibling file",
async (moduleKind) => {
const { node, node_error, outcome } = await compareWithNode(
{ import: "./specific.cjs", require: "./require.cjs" },
moduleKind,
TARGETS,
{ siblingFile: true },
);
const target = moduleKind === "import" ? "specific.cjs" : "require.cjs";
expect(node).toBe(target);
expect(node_error).toBeNull();
expect(outcome).toMatchObject({
resolved_path: `node_modules/example/${target}`,
resolution_status: "resolved",
});
},
);
it.each([
[{ ".": null }, "ERR_PACKAGE_PATH_NOT_EXPORTED", "external"],
["./missing.cjs", "MODULE_NOT_FOUND", "not-found"],
["../outside.cjs", "ERR_INVALID_PACKAGE_TARGET", "rejected"],
] as const)(
"preserves the refusal for %j despite a same-named sibling file",
async (exportsMap, error, status) => {
for (const moduleKind of ["import", "require"] as const) {
const { node, node_error, outcome } = await compareWithNode(
exportsMap,
moduleKind,
TARGETS,
{ siblingFile: true },
);
expect(node).toBeNull();
expect(node_error).toBe(
error === "MODULE_NOT_FOUND" && moduleKind === "import"
? "ERR_MODULE_NOT_FOUND"
: error,
);
expect(outcome).toMatchObject({
resolved_path: null,
resolution_status: status,
});
expect(outcome.limitations).not.toHaveLength(0);
}
},
);
it.each([undefined, null])(
"preserves CommonJS sibling-file priority when exports is %s",
async (exportsMap) => {
const { node, node_error, outcome } = await compareWithNode(
exportsMap,
"require",
[...TARGETS, "index.js"],
{ siblingFile: true },
);
expect(node).toBe("example.js");
expect(node_error).toBeNull();
expect(outcome).toMatchObject({
resolved_path: "node_modules/example.js",
resolution_status: "resolved",
});
},
);
it("does not let another container's exports override a local sibling file", async () => {
const { files } = await compareWithNode(
"./default.cjs",
"require",
TARGETS,
{ siblingFile: true },
);
const outcome = resolveArtifactPathByContext({
declaredPath: "example",
sourcePath: "main.js",
context: "module-specifier",
moduleKind: "require",
files: new Map(
[...files].map(([path, file]) => [
path,
path.startsWith("node_modules/example/")
? { ...file, container_sha256: "c".repeat(64) }
: file,
]),
),
});
expect(outcome).toMatchObject({
resolved_path: "node_modules/example.js",
resolution_status: "resolved",
});
});
});
describe("scalar package exports metadata", () => {
it.each([false, true, 0, 42])(
"preserves CommonJS sibling-file priority for root exports %s",
async (exportsMap) => {
const { node, node_error, outcome } = await compareWithNode(
exportsMap,
"require",
TARGETS,
{ siblingFile: true, main: "./default.cjs" },
);
expect(node).toBe("example.js");
expect(node_error).toBeNull();
expect(outcome).toMatchObject({
resolved_path: "node_modules/example.js",
resolution_status: "resolved",
});
},
);
it.each(["import", "require"] as const)(
"uses the legacy main for %s with numeric root exports and no sibling",
async (moduleKind) => {
const { node, node_error, outcome } = await compareWithNode(
42,
moduleKind,
TARGETS,
{ main: "./default.cjs" },
);
expect(node).toBe("default.cjs");
expect(node_error).toBeNull();
expect(outcome).toMatchObject({
resolved_path: "node_modules/example/default.cjs",
resolution_status: "resolved",
});
},
);
it.each([false, 42])(
"still rejects nested scalar target %s without a sibling fallback",
async (target) => {
const { node, node_error, outcome } = await compareWithNode(
{ ".": target },
"require",
TARGETS,
{ siblingFile: true },
);
expect(node).toBeNull();
expect(node_error).toBe("ERR_INVALID_PACKAGE_TARGET");
expect(outcome).toMatchObject({
resolved_path: null,
resolution_status: "rejected",
});
},
);
});
describe("package self-reference", () => {
it.each(["fi%xture", "@scope/fi%xture"])(
"applies self exports before the CommonJS sibling for %s",
async (packageName) => {
const { node, node_error, outcome } = await compareWithNode(
"./specific.cjs",
"require",
TARGETS,
{ packageName, sourceWithinPackage: "main.js", siblingFile: true },
);
expect(node).toBe("specific.cjs");
expect(node_error).toBeNull();
expect(outcome).toMatchObject({
resolved_path: `node_modules/${packageName}/specific.cjs`,
resolution_status: "resolved",
});
},
);
it("resolves an exported self subpath for a percent-named package", async () => {
const { node, node_error, outcome } = await compareWithNode(
{ "./part": "./specific.cjs" },
"require",
TARGETS,
{
packageName: "fi%xture",
specifier: "fi%xture/part",
sourceWithinPackage: "main.js",
},
);
expect(node).toBe("specific.cjs");
expect(node_error).toBeNull();
expect(outcome).toMatchObject({
resolved_path: "node_modules/fi%xture/specific.cjs",
resolution_status: "resolved",
});
});
it("does not fall back to main or a sibling when self exports block the root", async () => {
const { node, node_error, outcome } = await compareWithNode(
{ ".": null },
"require",
TARGETS,
{
packageName: "fi%xture",
sourceWithinPackage: "main.js",
siblingFile: true,
main: "./default.cjs",
},
);
expect(node).toBeNull();
expect(node_error).toBe("ERR_PACKAGE_PATH_NOT_EXPORTED");
expect(outcome).toMatchObject({
resolved_path: null,
resolution_status: "external",
});
});
it("falls through to dependency lookup when the self package has no exports", async () => {
const { node, node_error, outcome } = await compareWithNode(
undefined,
"require",
TARGETS,
{ sourceWithinPackage: "main.js", siblingFile: true },
);
expect(node).toBe("example.js");
expect(node_error).toBeNull();
expect(outcome).toMatchObject({
resolved_path: "node_modules/example.js",
resolution_status: "resolved",
});
});
it("selects self exports before a same-named nested dependency", async () => {
const { node, node_error, outcome } = await compareWithNode(
"./specific.cjs",
"require",
TARGETS,
{
sourceWithinPackage: "main.js",
packageFiles: {
"node_modules/example/index.js": 'module.exports = "dependency.js";',
},
},
);
expect(node).toBe("specific.cjs");
expect(node_error).toBeNull();
expect(outcome).toMatchObject({
resolved_path: "node_modules/example/specific.cjs",
resolution_status: "resolved",
});
});
});
describe("package self-reference scope boundaries", () => {
it.each([{ name: "other", exports: "./missing.cjs" }, { name: "example" }])(
"stops self lookup at the nearest package metadata %j",
async (metadata) => {
const { node, node_error, outcome } = await compareWithNode(
"./specific.cjs",
"require",
TARGETS,
{
sourceWithinPackage: "child/main.js",
packageFiles: {
"child/package.json": JSON.stringify(metadata),
"child/node_modules/example/index.js":
'module.exports = "dependency.js";',
},
},
);
expect(node).toBe("dependency.js");
expect(node_error).toBeNull();
expect(outcome).toMatchObject({
resolved_path:
"node_modules/example/child/node_modules/example/index.js",
resolution_status: "resolved",
});
},
);
it("stops self lookup at node_modules when the nearer package has no metadata", async () => {
const { node, node_error, outcome } = await compareWithNode(
"./specific.cjs",
"require",
TARGETS,
{
sourceWithinPackage: "node_modules/dependency/main.js",
packageFiles: {
"node_modules/dependency/node_modules/example/index.js":
'module.exports = "dependency.js";',
},
},
);
expect(node).toBe("dependency.js");
expect(node_error).toBeNull();
expect(outcome).toMatchObject({
resolved_path:
"node_modules/example/node_modules/dependency/node_modules/example/index.js",
resolution_status: "resolved",
});
});
it("reports malformed nearest metadata without resolving an ancestor self export", async () => {
const { node, node_error, outcome } = await compareWithNode(
"./specific.cjs",
"require",
TARGETS,
{
sourceWithinPackage: "child/main.js",
packageFiles: { "child/package.json": "{" },
},
);
expect(node).toBeNull();
expect(node_error).toBe("ERR_INVALID_PACKAGE_CONFIG");
expect(outcome).toMatchObject({
resolved_path: null,
resolution_status: "unavailable",
});
});
it("does not apply self exports from another container", async () => {
const { files } = await compareWithNode(
"./specific.cjs",
"require",
TARGETS,
{ sourceWithinPackage: "main.js", siblingFile: true },
);
const outcome = resolveArtifactPathByContext({
declaredPath: "example",
sourcePath: "node_modules/example/main.js",
context: "module-specifier",
moduleKind: "require",
files: new Map(
[...files].map(([path, file]) => [
path,
path === "node_modules/example/package.json"
? { ...file, container_sha256: "c".repeat(64) }
: file,
]),
),
});
expect(outcome).toMatchObject({
resolved_path: "node_modules/example.js",
resolution_status: "resolved",
});
});
});
describe("package exports name eligibility", () => {
it.each([
["fi%xture", "./specific.cjs"],
["@scope/fi%xture", { ".": null }],
["@sc%ope/fixture", "./specific.cjs"],
] as const)(
"uses the literal sibling file for %s without applying exports",
async (packageName, exportsMap) => {
const { node, node_error, outcome } = await compareWithNode(
exportsMap,
"require",
TARGETS,
{ packageName, siblingFile: true },
);
expect(node).toBe(`${packageName}.js`);
expect(node_error).toBeNull();
expect(outcome).toMatchObject({
resolved_path: `node_modules/${packageName}.js`,
resolution_status: "resolved",
});
},
);
it.each([
{ packageName: "fi%xture" },
{ packageName: "@scope/fi%xture" },
{ packageName: "@sc%ope/fixture", metadataText: "{" },
])(
"rejects the ESM package name $packageName before metadata or sibling lookup",
async (options) => {
const { node, node_error, outcome } = await compareWithNode(
"./specific.cjs",
"import",
TARGETS,
{ ...options, siblingFile: true },
);
expect(node).toBeNull();
expect(node_error).toBe("ERR_INVALID_MODULE_SPECIFIER");
expect(outcome).toMatchObject({
resolved_path: null,
resolution_status: "rejected",
});
},
);
it("uses the legacy main of a percent-named package without a sibling file", async () => {
const { node, node_error, outcome } = await compareWithNode(
"./specific.cjs",
"require",
TARGETS,
{ packageName: "fi%xture", main: "./default.cjs" },
);
expect(node).toBe("default.cjs");
expect(node_error).toBeNull();
expect(outcome).toMatchObject({
resolved_path: "node_modules/fi%xture/default.cjs",
resolution_status: "resolved",
});
});
it.each([
["require", "example"],
["import", "example"],
["import", "@scope/example"],
] as const)(
"still applies exports to %s percent subpaths of %s",
async (moduleKind, packageName) => {
const { node, node_error, outcome } = await compareWithNode(
{ "./fi%xture": "./specific.cjs" },
moduleKind,
TARGETS,
{ packageName, specifier: `${packageName}/fi%xture` },
);
expect(node).toBe("specific.cjs");
expect(node_error).toBeNull();
expect(outcome).toMatchObject({
resolved_path: `node_modules/${packageName}/specific.cjs`,
resolution_status: "resolved",
});
},
);
it("resolves a percent-package subpath despite malformed parent metadata", async () => {
const { node, node_error, outcome } = await compareWithNode(
undefined,
"require",
TARGETS,
{
packageName: "fi%xture",
specifier: "fi%xture/specific.cjs",
metadataText: "{",
},
);
expect(node).toBe("specific.cjs");
expect(node_error).toBeNull();
expect(outcome).toMatchObject({
resolved_path: "node_modules/fi%xture/specific.cjs",
resolution_status: "resolved",
});
});
});
describe("nested and array package export targets", () => {
it.each([
[