canonicalize-node-modules.ts 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import { lstat, mkdir, readdir, rm, symlink } from "fs/promises"
  2. import { join, relative } from "path"
  3. type Entry = {
  4. dir: string
  5. version: string
  6. }
  7. async function isDirectory(path: string) {
  8. try {
  9. const info = await lstat(path)
  10. return info.isDirectory()
  11. } catch {
  12. return false
  13. }
  14. }
  15. const isValidSemver = (v: string) => Bun.semver.satisfies(v, "x.x.x")
  16. const root = process.cwd()
  17. const bunRoot = join(root, "node_modules/.bun")
  18. const linkRoot = join(bunRoot, "node_modules")
  19. const directories = (await readdir(bunRoot)).sort()
  20. const versions = new Map<string, Entry[]>()
  21. for (const entry of directories) {
  22. const full = join(bunRoot, entry)
  23. if (!(await isDirectory(full))) {
  24. continue
  25. }
  26. const parsed = parseEntry(entry)
  27. if (!parsed) {
  28. continue
  29. }
  30. const list = versions.get(parsed.name) ?? []
  31. list.push({ dir: full, version: parsed.version })
  32. versions.set(parsed.name, list)
  33. }
  34. const selections = new Map<string, Entry>()
  35. for (const [slug, list] of versions) {
  36. list.sort((a, b) => {
  37. const aValid = isValidSemver(a.version)
  38. const bValid = isValidSemver(b.version)
  39. if (aValid && bValid) return -Bun.semver.order(a.version, b.version)
  40. if (aValid) return -1
  41. if (bValid) return 1
  42. return b.version.localeCompare(a.version)
  43. })
  44. const first = list[0]
  45. if (first) selections.set(slug, first)
  46. }
  47. await rm(linkRoot, { recursive: true, force: true })
  48. await mkdir(linkRoot, { recursive: true })
  49. const rewrites: string[] = []
  50. for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0].localeCompare(b[0]))) {
  51. const parts = slug.split("/")
  52. const leaf = parts.pop()
  53. if (!leaf) {
  54. continue
  55. }
  56. const parent = join(linkRoot, ...parts)
  57. await mkdir(parent, { recursive: true })
  58. const linkPath = join(parent, leaf)
  59. const desired = join(entry.dir, "node_modules", slug)
  60. if (!(await isDirectory(desired))) {
  61. continue
  62. }
  63. const relativeTarget = relative(parent, desired)
  64. const resolved = relativeTarget.length === 0 ? "." : relativeTarget
  65. await rm(linkPath, { recursive: true, force: true })
  66. await symlink(resolved, linkPath)
  67. rewrites.push(slug + " -> " + resolved)
  68. }
  69. rewrites.sort()
  70. console.log("[canonicalize-node-modules] rebuilt", rewrites.length, "links")
  71. for (const line of rewrites.slice(0, 20)) {
  72. console.log(" ", line)
  73. }
  74. if (rewrites.length > 20) {
  75. console.log(" ...")
  76. }
  77. function parseEntry(label: string) {
  78. const marker = label.startsWith("@") ? label.indexOf("@", 1) : label.indexOf("@")
  79. if (marker <= 0) {
  80. return null
  81. }
  82. const name = label.slice(0, marker).replace(/\+/g, "/")
  83. const version = label.slice(marker + 1)
  84. if (!name || !version) {
  85. return null
  86. }
  87. return { name, version }
  88. }