import fs from "node:fs/promises"; import path from "node:path"; import { createHash } from "node:crypto"; async function packages(directory) { const result = new Map(); for (const entry of await fs.readdir(directory)) { if (entry.startsWith(".")) continue; if (entry.startsWith("@")) { for (const name of await fs.readdir(path.join(directory, entry))) result.set(`${entry}/${name}`, path.join(directory, entry, name)); } else result.set(entry, path.join(directory, entry)); } return result; } async function packageDigest(directory) { const digest = createHash("sha256"); async function visit(relative) { for (const entry of (await fs.readdir(path.join(directory, relative), { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name), )) { if (entry.name === "node_modules") continue; const name = path.join(relative, entry.name), file = path.join(directory, name); if (entry.isDirectory()) await visit(name); else { const bytes = await fs.readFile(file); digest.update(JSON.stringify([name, bytes.length])); digest.update(bytes); } } } await visit(""); return digest.digest("hex"); } /** Merge at package granularity: a scope such as @types is not a reserved package. */ export async function installDependencies({ destination, application, defaults = [], selected = {}, reserved = [] }) { const entries = new Map(); for (const root of defaults) for (const [name, file] of await packages(root)) entries.set(name, file); for (const [name, file] of Object.entries(selected)) entries.set(name, file); if (application) for (const [name, file] of await packages(application)) { if (reserved.includes(name)) { const canonical = entries.get(name); if (!canonical || (await packageDigest(file)) !== (await packageDigest(canonical))) throw Error("Application dependencies cannot substitute selected React/client types or checked SDK: " + name); } else entries.set(name, file); } for (const [name, file] of entries) { await fs.mkdir(path.dirname(path.join(destination, name)), { recursive: true }); await fs.symlink(file, path.join(destination, name)); } }