import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const packagePath = path.join(root, "web/package.json"); const lockPath = path.join(root, "web/package-lock.json"); const outputPath = path.resolve(process.argv[2] ?? path.join(root, "tests/golden/M13-05C/dependency-inventory.json")); const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8")); const lock = JSON.parse(fs.readFileSync(lockPath, "utf8")); const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); const packageBytes = fs.readFileSync(packagePath); const lockBytes = fs.readFileSync(lockPath); const entries = new Map(Object.entries(lock.packages)); const packageName = (packageKey) => { const marker = packageKey.lastIndexOf("node_modules/"); return marker < 0 ? packageKey : packageKey.slice(marker + "node_modules/".length); }; function resolveDependency(fromKey, dependency) { let base = fromKey; while (true) { const candidate = base ? `${base}/node_modules/${dependency}` : `node_modules/${dependency}`; if (entries.has(candidate)) return candidate; const marker = base.lastIndexOf("/node_modules/"); if (marker < 0) return entries.has(`node_modules/${dependency}`) ? `node_modules/${dependency}` : null; base = base.slice(0, marker); } } function closure(roots) { const visited = new Set(); const queue = roots.map((name) => resolveDependency("", name)).filter(Boolean); while (queue.length) { const key = queue.shift(); if (!key || visited.has(key)) continue; visited.add(key); const entry = entries.get(key); for (const dependency of Object.keys({ ...(entry.dependencies ?? {}), ...(entry.optionalDependencies ?? {}) })) { const next = resolveDependency(key, dependency); if (next) queue.push(next); } } return visited; } const roots = { production: Object.keys(packageJson.dependencies ?? {}), build: ["@eslint/js", "@types/react", "@types/react-dom", "@types/three", "@vitejs/plugin-react", "eslint", "typescript", "typescript-eslint", "vite"], test: ["@axe-core/playwright", "@playwright/test"], }; const categories = Object.fromEntries(Object.entries(roots).map(([category, names]) => [category, closure(names)])); const allKeys = new Set([...categories.production, ...categories.build, ...categories.test]); const packages = [...allKeys].sort().map((key) => { const entry = entries.get(key); const category = Object.entries(categories).filter(([, keys]) => keys.has(key)).map(([name]) => name); return { path: key, name: entry.name ?? packageName(key), version: entry.version, resolved: entry.resolved ?? null, integrity: entry.integrity ?? null, categories: category, dependencies: Object.keys({ ...(entry.dependencies ?? {}), ...(entry.optionalDependencies ?? {}) }).sort(), }; }); const inventory = { schemaVersion: 1, task: "M13-05C", operation: "NPM_DEPENDENCY_INVENTORY", package: { path: "web/package.json", sha256: sha256(packageBytes), name: lock.name, version: lock.version, lockfileVersion: lock.lockfileVersion }, lockfile: { path: "web/package-lock.json", sha256: sha256(lockBytes), packageCount: entries.size - 1 }, roots, categoryCounts: Object.fromEntries(Object.entries(categories).map(([name, keys]) => [name, keys.size])), sharedCount: packages.filter((item) => item.categories.length > 1).length, packages, nextTask: "M13-05D", }; fs.mkdirSync(path.dirname(outputPath), { recursive: true }); fs.writeFileSync(outputPath, `${JSON.stringify(inventory, null, 2)}\n`); process.stdout.write(`dependency-inventory-generated production=${inventory.categoryCounts.production} build=${inventory.categoryCounts.build} test=${inventory.categoryCounts.test} shared=${inventory.sharedCount} next=${inventory.nextTask}\n`);