index.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import z from "zod"
  2. import { Global } from "../global"
  3. import { Log } from "../util/log"
  4. import path from "path"
  5. import { NamedError } from "../util/error"
  6. import { readableStreamToText } from "bun"
  7. import { Lock } from "../util/lock"
  8. export namespace BunProc {
  9. const log = Log.create({ service: "bun" })
  10. export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
  11. log.info("running", {
  12. cmd: [which(), ...cmd],
  13. ...options,
  14. })
  15. const result = Bun.spawn([which(), ...cmd], {
  16. ...options,
  17. stdout: "pipe",
  18. stderr: "pipe",
  19. env: {
  20. ...process.env,
  21. ...options?.env,
  22. BUN_BE_BUN: "1",
  23. },
  24. })
  25. const code = await result.exited
  26. const stdout = result.stdout
  27. ? typeof result.stdout === "number"
  28. ? result.stdout
  29. : await readableStreamToText(result.stdout)
  30. : undefined
  31. const stderr = result.stderr
  32. ? typeof result.stderr === "number"
  33. ? result.stderr
  34. : await readableStreamToText(result.stderr)
  35. : undefined
  36. log.info("done", {
  37. code,
  38. stdout,
  39. stderr,
  40. })
  41. if (code !== 0) {
  42. throw new Error(`Command failed with exit code ${result.exitCode}`)
  43. }
  44. return result
  45. }
  46. export function which() {
  47. return process.execPath
  48. }
  49. export const InstallFailedError = NamedError.create(
  50. "BunInstallFailedError",
  51. z.object({
  52. pkg: z.string(),
  53. version: z.string(),
  54. }),
  55. )
  56. export async function install(pkg: string, version = "latest") {
  57. // Use lock to ensure only one install at a time
  58. using _ = await Lock.write("bun-install")
  59. const mod = path.join(Global.Path.cache, "node_modules", pkg)
  60. const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
  61. const parsed = await pkgjson.json().catch(async () => {
  62. const result = { dependencies: {} }
  63. await Bun.write(pkgjson.name!, JSON.stringify(result, null, 2))
  64. return result
  65. })
  66. if (parsed.dependencies[pkg] === version) return mod
  67. // Build command arguments
  68. const args = ["add", "--force", "--exact", "--cwd", Global.Path.cache, pkg + "@" + version]
  69. // Let Bun handle registry resolution:
  70. // - If .npmrc files exist, Bun will use them automatically
  71. // - If no .npmrc files exist, Bun will default to https://registry.npmjs.org
  72. // - No need to pass --registry flag
  73. log.info("installing package using Bun's default registry resolution", {
  74. pkg,
  75. version,
  76. })
  77. const total = 3
  78. const wait = 500
  79. const runInstall = async (count: number = 1): Promise<void> => {
  80. log.info("bun install attempt", {
  81. pkg,
  82. version,
  83. attempt: count,
  84. total,
  85. })
  86. await BunProc.run(args, {
  87. cwd: Global.Path.cache,
  88. }).catch(async (error) => {
  89. log.warn("bun install failed", {
  90. pkg,
  91. version,
  92. attempt: count,
  93. total,
  94. error,
  95. })
  96. if (count >= total) {
  97. throw new InstallFailedError(
  98. { pkg, version },
  99. {
  100. cause: error,
  101. },
  102. )
  103. }
  104. const delay = wait * count
  105. log.info("bun install retrying", {
  106. pkg,
  107. version,
  108. next: count + 1,
  109. delay,
  110. })
  111. await Bun.sleep(delay)
  112. return runInstall(count + 1)
  113. })
  114. }
  115. await runInstall()
  116. parsed.dependencies[pkg] = version
  117. await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
  118. return mod
  119. }
  120. }