rea/tests/acceptance/applications/packageMainLookupCli.test.ts

285 lines
10 KiB
TypeScript

import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import {
analyzeJavaScriptDirectory,
runNodeJsonControl,
} from "../../support/cli/javascriptApplication.js";
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
const cases = [
{ name: "null-main", metadata: { main: null } },
{ name: "false-main", metadata: { main: false } },
{ name: "zero-main", metadata: { main: 0 } },
{ name: "true-main", metadata: { main: true } },
{ name: "object-main", metadata: { main: {} } },
{ name: "empty-main", metadata: { main: "" } },
{ name: "missing-main", metadata: {} },
{ name: "string-main", metadata: { main: "./index.js" } },
] as const;
cliTest.for(["CLI", "stdio MCP"] as const)(
"matches Node package main interpretation through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-package-main-");
const files: Record<string, string> = {
"package.json": JSON.stringify({ type: "commonjs" }),
"entry.cjs": cases
.map(({ name }) => `require(${JSON.stringify(name)});`)
.join("\n"),
"entry.mjs": cases
.map(({ name }) => `import ${JSON.stringify(name)};`)
.join("\n"),
"control.cjs": [
`const specifiers = ${JSON.stringify(cases.map(({ name }) => name))};`,
"console.log(JSON.stringify(specifiers.map((specifier) => require(specifier))));",
].join("\n"),
"control.mjs": [
`const specifiers = ${JSON.stringify(cases.map(({ name }) => name))};`,
"console.log(JSON.stringify(await Promise.all(specifiers.map(async (specifier) => (await import(specifier)).default))));",
].join("\n"),
};
for (const { name, metadata } of cases) {
files[`node_modules/${name}/package.json`] = JSON.stringify(metadata);
files[`node_modules/${name}/index.js`] =
`module.exports = ${JSON.stringify(`node_modules/${name}/index.js`)};`;
}
await writeFixtureFiles(root, files);
for (const filename of ["control.cjs", "control.mjs"]) {
expect(await runNodeJsonControl(root, filename)).toEqual(
cases.map(({ name }) => `node_modules/${name}/index.js`),
);
}
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
for (const kind of ["require", "import"])
for (const { name } of cases)
expect(
graph.edges
.filter(({ relation }) => relation === "imports")
.map(({ properties }) => properties),
).toContainEqual(
expect.objectContaining({
module_link_kind: kind,
specifier: name,
resolved_path: `node_modules/${name}/index.js`,
resolution_status: "resolved",
}),
);
},
);
cliTest.for(["CLI", "stdio MCP"] as const)(
"preserves loader-specific package main probes and finalization through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-package-main-path-");
const packages = [
{
name: "dot-slash-main",
main: "./lib/",
files: ["lib.js", "lib/index.js", "index.js"],
require: "lib.js",
import: null,
},
{
name: "slash-main",
main: "lib/",
files: ["lib.js", "lib/index.js", "index.js"],
require: "lib.js",
import: null,
},
{
name: "plain-main",
main: "./lib",
files: ["lib.js", "lib/index.js"],
require: "lib.js",
import: "lib.js",
},
{
name: "directory-main",
main: "./lib/",
files: ["lib/index.js", "index.js"],
require: "lib/index.js",
import: "lib/index.js",
},
{
name: "fallback-main",
main: "./missing/",
files: ["index.js"],
require: "index.js",
import: "index.js",
},
{
name: "exact-main",
main: "./lib.js/",
files: ["lib.js", "index.js"],
require: "lib.js",
import: "lib.js",
},
{
name: "finalized-directory-main",
main: "./lib/",
files: ["lib.js", "lib/.js/child.js"],
require: "lib.js",
import: null,
importError: "ERR_UNSUPPORTED_DIR_IMPORT",
},
{
name: "finalized-main",
main: "./lib/",
files: ["lib.js", "lib/.js", "lib/index.js"],
require: "lib.js",
import: "lib/.js",
},
] as const;
const files: Record<string, string> = {
"package.json": JSON.stringify({ type: "commonjs" }),
"entry.cjs": [
...packages.map(({ name }) => `require(${JSON.stringify(name)});`),
'require("./local/");',
].join("\n"),
"entry.mjs": packages
.map(({ name }) => `import ${JSON.stringify(name)};`)
.join("\n"),
"local.js": 'module.exports = "local.js";',
"local/index.js": 'module.exports = "local/index.js";',
};
for (const item of packages) {
files[`node_modules/${item.name}/package.json`] = JSON.stringify({
main: item.main,
});
for (const path of item.files) {
const target = `node_modules/${item.name}/${path}`;
files[target] = `module.exports = ${JSON.stringify(target)};`;
}
}
for (const kind of ["require", "import"] as const) {
const filename = `control.${kind === "require" ? "cjs" : "mjs"}`;
files[filename] = [
`const specifiers = ${JSON.stringify(packages.map(({ name }) => name))};`,
"const results = [];",
"for (const specifier of specifiers) {",
` try { results.push({ path: ${kind === "require" ? "require(specifier)" : "(await import(specifier)).default"} }); }`,
" catch (error) { results.push({ code: error.code }); }",
"}",
"console.log(JSON.stringify(results));",
].join("\n");
}
files["local-control.cjs"] =
'console.log(JSON.stringify(require("./local/")));';
await writeFixtureFiles(root, files);
for (const kind of ["require", "import"] as const) {
const control = await runNodeJsonControl(
root,
`control.${kind === "require" ? "cjs" : "mjs"}`,
);
expect(control).toEqual(
packages.map((item) =>
item[kind] === null
? {
code:
"importError" in item
? item.importError
: "ERR_MODULE_NOT_FOUND",
}
: { path: `node_modules/${item.name}/${item[kind]}` },
),
);
}
expect(await runNodeJsonControl(root, "local-control.cjs")).toBe(
"local/index.js",
);
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
const relationships = graph.edges
.filter(({ relation }) => relation === "imports")
.map(({ properties }) => properties);
for (const kind of ["require", "import"] as const)
for (const item of packages)
expect(relationships).toContainEqual(
expect.objectContaining({
module_link_kind: kind,
specifier: item.name,
resolved_path:
item[kind] === null
? null
: `node_modules/${item.name}/${item[kind]}`,
resolution_status:
item[kind] === null
? "importError" in item
? "rejected"
: "not-found"
: "resolved",
}),
);
expect(relationships).toContainEqual(
expect.objectContaining({
module_link_kind: "require",
specifier: "./local/",
resolved_path: "local/index.js",
resolution_status: "resolved",
}),
);
},
);
cliTest.for(["CLI", "stdio MCP"] as const)(
"resolves package mains that name the artifact root through %s",
async (surface, { cli }) => {
const mains = ["../..", "../../", "../../."];
const names = mains.map((_, index) => `root-main-${index}`);
const requires = names.map((name) => `require("${name}")`);
for (const rootIndex of ["index.js", "index.json", null]) {
const root = await createTestTempDirectory("rea-package-main-root-");
const importOptions =
rootIndex === "index.json" ? ', { with: { type: "json" } }' : "";
const imports = names.map(
(name) =>
`import("${name}"${importOptions}).then(module => module.default)`,
);
const files: Record<string, string> = {
"package.json": '{"type":"commonjs"}',
"entry.cjs": requires.join(";\n"),
"entry.mjs": names.map((name) => `import "${name}";`).join("\n"),
".js": 'module.exports = ".js decoy";',
".json": '".json decoy"',
"control.cjs": `console.log(JSON.stringify([${requires.join(",")}]));`,
"control.mjs": `console.log(JSON.stringify(await Promise.all([${imports.join(",")}])));`,
};
if (rootIndex !== null)
files[rootIndex] =
rootIndex === "index.js"
? 'module.exports = "index.js";'
: '"index.json"';
for (const [index, main] of mains.entries()) {
const directory = `node_modules/${names[index]}`;
files[`${directory}/package.json`] = JSON.stringify({ main });
files[`${directory}/index.js`] =
`module.exports = "${directory}/index.js";`;
}
await writeFixtureFiles(root, files);
const expected = names.map(
(name) => rootIndex ?? `node_modules/${name}/index.js`,
);
for (const filename of ["control.cjs", "control.mjs"])
expect(await runNodeJsonControl(root, filename)).toEqual(expected);
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
const relationships = graph.edges
.filter(({ relation }) => relation === "imports")
.map(({ properties }) => properties);
for (const kind of ["require", "import"])
for (const [index, name] of names.entries())
expect(relationships).toContainEqual(
expect.objectContaining({
module_link_kind: kind,
specifier: name,
resolved_path: expected[index],
resolution_status: "resolved",
}),
);
}
},
);