normalize-bun-binaries.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import { lstat, mkdir, readdir, rm, symlink } from "fs/promises"
  2. import { join, relative } from "path"
  3. type PackageManifest = {
  4. name?: string
  5. bin?: string | Record<string, string>
  6. }
  7. const root = process.cwd()
  8. const bunRoot = join(root, "node_modules/.bun")
  9. const bunEntries = (await safeReadDir(bunRoot)).sort()
  10. let rewritten = 0
  11. for (const entry of bunEntries) {
  12. const modulesRoot = join(bunRoot, entry, "node_modules")
  13. if (!(await exists(modulesRoot))) {
  14. continue
  15. }
  16. const binRoot = join(modulesRoot, ".bin")
  17. await rm(binRoot, { recursive: true, force: true })
  18. await mkdir(binRoot, { recursive: true })
  19. const packageDirs = await collectPackages(modulesRoot)
  20. for (const packageDir of packageDirs) {
  21. const manifest = await readManifest(packageDir)
  22. if (!manifest) {
  23. continue
  24. }
  25. const binField = manifest.bin
  26. if (!binField) {
  27. continue
  28. }
  29. const seen = new Set<string>()
  30. if (typeof binField === "string") {
  31. const fallback = manifest.name ?? packageDir.split("/").pop()
  32. if (fallback) {
  33. await linkBinary(binRoot, fallback, packageDir, binField, seen)
  34. }
  35. } else {
  36. const entries = Object.entries(binField).sort((a, b) => a[0].localeCompare(b[0]))
  37. for (const [name, target] of entries) {
  38. await linkBinary(binRoot, name, packageDir, target, seen)
  39. }
  40. }
  41. }
  42. }
  43. console.log(`[normalize-bun-binaries] rewrote ${rewritten} links`)
  44. async function collectPackages(modulesRoot: string) {
  45. const found: string[] = []
  46. const topLevel = (await safeReadDir(modulesRoot)).sort()
  47. for (const name of topLevel) {
  48. if (name === ".bin" || name === ".bun") {
  49. continue
  50. }
  51. const full = join(modulesRoot, name)
  52. if (!(await isDirectory(full))) {
  53. continue
  54. }
  55. if (name.startsWith("@")) {
  56. const scoped = (await safeReadDir(full)).sort()
  57. for (const child of scoped) {
  58. const scopedDir = join(full, child)
  59. if (await isDirectory(scopedDir)) {
  60. found.push(scopedDir)
  61. }
  62. }
  63. continue
  64. }
  65. found.push(full)
  66. }
  67. return found.sort()
  68. }
  69. async function readManifest(dir: string) {
  70. const file = Bun.file(join(dir, "package.json"))
  71. if (!(await file.exists())) {
  72. return null
  73. }
  74. const data = (await file.json()) as PackageManifest
  75. return data
  76. }
  77. async function linkBinary(binRoot: string, name: string, packageDir: string, target: string, seen: Set<string>) {
  78. if (!name || !target) {
  79. return
  80. }
  81. const normalizedName = normalizeBinName(name)
  82. if (seen.has(normalizedName)) {
  83. return
  84. }
  85. const resolved = join(packageDir, target)
  86. const script = Bun.file(resolved)
  87. if (!(await script.exists())) {
  88. return
  89. }
  90. seen.add(normalizedName)
  91. const destination = join(binRoot, normalizedName)
  92. const relativeTarget = relative(binRoot, resolved) || "."
  93. await rm(destination, { force: true })
  94. await symlink(relativeTarget, destination)
  95. rewritten++
  96. }
  97. async function exists(path: string) {
  98. try {
  99. await lstat(path)
  100. return true
  101. } catch {
  102. return false
  103. }
  104. }
  105. async function isDirectory(path: string) {
  106. try {
  107. const info = await lstat(path)
  108. return info.isDirectory()
  109. } catch {
  110. return false
  111. }
  112. }
  113. async function safeReadDir(path: string) {
  114. try {
  115. return await readdir(path)
  116. } catch {
  117. return []
  118. }
  119. }
  120. function normalizeBinName(name: string) {
  121. const slash = name.lastIndexOf("/")
  122. if (slash >= 0) {
  123. return name.slice(slash + 1)
  124. }
  125. return name
  126. }