// buildIdealTree's port (src/kernel/npm/resolve.ts), checked in Node against the trees // real npm 11.7.0 wrote from the same recorded registries, and against hand cases whose // expected trees came from real npm too. import { readFileSync } from "node:fs"; import { test, expect } from "@playwright/test"; import { resolve, ResolveError } from "../src/kernel/npm/resolve"; import { loadTree, type Tree } from "../src/kernel/npm/tree"; import { fakeRegistry, recordingRegistry, type FakePackages } from "./npm-registry"; type LockEntry = { version?: string; name?: string; dev?: boolean; optional?: boolean; peer?: boolean; devOptional?: boolean }; type Shape = Record; const fixture = (name: string, file: string): string => readFileSync(new URL(`./fixtures/registry/${name}/${file}`, import.meta.url), "utf8"); /** What the oracle compares: each placed node's version, alias name and dependency flags. */ function shapeOf(tree: Tree): Shape { const out: Shape = {}; for (const [location, node] of [...tree.nodes].sort(([a], [b]) => a.localeCompare(b, "en"))) { out[location] = { version: node.version, ...(node.packageName !== node.name && { name: node.packageName }), ...(node.dev && { dev: true }), ...(node.optional && { optional: true }), ...(node.devOptional && { devOptional: true }), ...(node.peer && { peer: true }), }; } return out; } function lockShape(name: string): Shape { const lock = JSON.parse(fixture(name, "package-lock.json")) as { packages: Record }; return packagesShape(lock.packages); } function packagesShape(packages: Record): Shape { const out: Shape = {}; for (const [location, e] of Object.entries(packages).sort(([a], [b]) => a.localeCompare(b, "en"))) { if (location === "") continue; out[location] = { version: e.version, ...(e.name && { name: e.name }), ...(e.dev && { dev: true }), ...(e.optional && { optional: true }), ...(e.devOptional && { devOptional: true }), ...(e.peer && { peer: true }), }; } return out; } for (const name of ["hono", "express", "conflict"]) { test(`${name}: package.json alone resolves to the tree npm locked`, async () => { const registry = recordingRegistry(name); const tree = loadTree({ text: fixture(name, "package.json"), path: "package.json" }); const { warnings } = await resolve(tree, registry); console.log(`${name}: ${tree.nodes.size} nodes, ${registry.calls.length} manifest() calls, ${warnings.length} warnings`); expect(shapeOf(tree)).toEqual(lockShape(name)); expect(warnings).toEqual([]); }); test(`${name}: the lockfile as seed resolves nothing and stands`, async () => { const registry = recordingRegistry(name); const tree = loadTree({ text: fixture(name, "package.json"), path: "package.json" }, { text: fixture(name, "package-lock.json"), path: "package-lock.json" }); await resolve(tree, registry); expect(registry.calls).toEqual([]); expect(shapeOf(tree)).toEqual(lockShape(name)); }); } // Hand cases: tests/fixtures/fake/.json holds a package.json and an in-memory // registry, and under `npm` what real npm 11.7.0 did with exactly those inputs // (`node scripts/npm-oracle.mjs fake ` refreshes it). interface FakeCase { description: string; package: Record; seed?: Record; registry: FakePackages; npm: { version: string; status: number; packages?: Record; seedLock?: unknown; stderr: string }; } const fakeCase = (name: string): FakeCase => JSON.parse(readFileSync(new URL(`./fixtures/fake/${name}.json`, import.meta.url), "utf8")); const source = (json: unknown, path: string) => ({ text: JSON.stringify(json), path }); for (const name of ["replace", "override-ref"]) { test(`${name}: ${fakeCase(name).description}`, async () => { const c = fakeCase(name); const tree = loadTree(source(c.package, "package.json")); const { warnings } = await resolve(tree, fakeRegistry(c.registry)); expect(shapeOf(tree)).toEqual(packagesShape(c.npm.packages!)); expect(warnings).toEqual([]); }); } test(`bump: ${fakeCase("bump").description}`, async () => { const c = fakeCase("bump"); const tree = loadTree(source(c.package, "package.json"), source(c.npm.seedLock, "package-lock.json")); const registry = fakeRegistry(c.registry); const { warnings } = await resolve(tree, registry); expect(registry.calls).toEqual(["a@^1.4.0"]); expect(shapeOf(tree)).toEqual(packagesShape(c.npm.packages!)); expect(warnings).toEqual([]); }); test(`eresolve: ${fakeCase("eresolve").description}`, async () => { const c = fakeCase("eresolve"); const tree = loadTree(source(c.package, "package.json")); const error = await resolve(tree, fakeRegistry(c.registry)).then(() => null, (e: unknown) => e); expect(error).toBeInstanceOf(ResolveError); expect((error as ResolveError).code).toBe("ERESOLVE"); expect(`code ERESOLVE\nERESOLVE ${(error as ResolveError).message}`).toBe(c.npm.stderr); }); test(`peer-warn: ${fakeCase("peer-warn").description}`, async () => { const c = fakeCase("peer-warn"); const tree = loadTree(source(c.package, "package.json")); const { warnings } = await resolve(tree, fakeRegistry(c.registry)); expect(shapeOf(tree)).toEqual(packagesShape(c.npm.packages!)); expect(warnings.join("\n")).toBe(c.npm.stderr); });