import { lstat, mkdir, readdir, rm, symlink } from "fs/promises" import { join, relative } from "path" type Entry = { dir: string version: string } async function isDirectory(path: string) { try { const info = await lstat(path) return info.isDirectory() } catch { return false } } const isValidSemver = (v: string) => Bun.semver.satisfies(v, "x.x.x") const root = process.cwd() const bunRoot = join(root, "node_modules/.bun") const linkRoot = join(bunRoot, "node_modules") const directories = (await readdir(bunRoot)).sort() const versions = new Map() for (const entry of directories) { const full = join(bunRoot, entry) if (!(await isDirectory(full))) { continue } const parsed = parseEntry(entry) if (!parsed) { continue } const list = versions.get(parsed.name) ?? [] list.push({ dir: full, version: parsed.version }) versions.set(parsed.name, list) } const selections = new Map() for (const [slug, list] of versions) { list.sort((a, b) => { const aValid = isValidSemver(a.version) const bValid = isValidSemver(b.version) if (aValid && bValid) return -Bun.semver.order(a.version, b.version) if (aValid) return -1 if (bValid) return 1 return b.version.localeCompare(a.version) }) const first = list[0] if (first) selections.set(slug, first) } await rm(linkRoot, { recursive: true, force: true }) await mkdir(linkRoot, { recursive: true }) const rewrites: string[] = [] for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0].localeCompare(b[0]))) { const parts = slug.split("/") const leaf = parts.pop() if (!leaf) { continue } const parent = join(linkRoot, ...parts) await mkdir(parent, { recursive: true }) const linkPath = join(parent, leaf) const desired = join(entry.dir, "node_modules", slug) if (!(await isDirectory(desired))) { continue } const relativeTarget = relative(parent, desired) const resolved = relativeTarget.length === 0 ? "." : relativeTarget await rm(linkPath, { recursive: true, force: true }) await symlink(resolved, linkPath) rewrites.push(slug + " -> " + resolved) } rewrites.sort() console.log("[canonicalize-node-modules] rebuilt", rewrites.length, "links") for (const line of rewrites.slice(0, 20)) { console.log(" ", line) } if (rewrites.length > 20) { console.log(" ...") } function parseEntry(label: string) { const marker = label.startsWith("@") ? label.indexOf("@", 1) : label.indexOf("@") if (marker <= 0) { return null } const name = label.slice(0, marker).replace(/\+/g, "/") const version = label.slice(marker + 1) if (!name || !version) { return null } return { name, version } }