canonicalize-node-modules.ts 3.0 KB

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