102 lines
2.7 KiB
TypeScript
102 lines
2.7 KiB
TypeScript
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<string, Entry[]>()
|
|
|
|
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<string, Entry>()
|
|
|
|
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 }
|
|
}
|