refactor: replace BunProc with Npm module using @npmcli/arborist (#18308)

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
This commit is contained in:
Dax
2026-04-01 21:01:37 +00:00
committed by GitHub
co-authored by greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Brendan Allan Aiden Cline
parent d7481f4593
commit c9326fc199
14 changed files with 901 additions and 1114 deletions
-137
View File
@@ -1,137 +0,0 @@
import { describe, expect, spyOn, test } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { BunProc } from "../src/bun"
import { PackageRegistry } from "../src/bun/registry"
import { Global } from "../src/global"
import { Process } from "../src/util/process"
describe("BunProc registry configuration", () => {
test("should not contain hardcoded registry parameters", async () => {
// Read the bun/index.ts file
const bunIndexPath = path.join(__dirname, "../src/bun/index.ts")
const content = await fs.readFile(bunIndexPath, "utf-8")
// Verify that no hardcoded registry is present
expect(content).not.toContain("--registry=")
expect(content).not.toContain("hasNpmRcConfig")
expect(content).not.toContain("NpmRc")
})
test("should use Bun's default registry resolution", async () => {
// Read the bun/index.ts file
const bunIndexPath = path.join(__dirname, "../src/bun/index.ts")
const content = await fs.readFile(bunIndexPath, "utf-8")
// Verify that it uses Bun's default resolution
expect(content).toContain("Bun's default registry resolution")
expect(content).toContain("Bun will use them automatically")
expect(content).toContain("No need to pass --registry flag")
})
test("should have correct command structure without registry", async () => {
// Read the bun/index.ts file
const bunIndexPath = path.join(__dirname, "../src/bun/index.ts")
const content = await fs.readFile(bunIndexPath, "utf-8")
// Extract the install function
const installFunctionMatch = content.match(/export async function install[\s\S]*?^ }/m)
expect(installFunctionMatch).toBeTruthy()
if (installFunctionMatch) {
const installFunction = installFunctionMatch[0]
// Verify expected arguments are present
expect(installFunction).toContain('"add"')
expect(installFunction).toContain('"--force"')
expect(installFunction).toContain('"--exact"')
expect(installFunction).toContain('"--cwd"')
expect(installFunction).toContain("Global.Path.cache")
expect(installFunction).toContain('pkg + "@" + version')
// Verify no registry argument is added
expect(installFunction).not.toContain('"--registry"')
expect(installFunction).not.toContain('args.push("--registry')
}
})
})
describe("BunProc install pinning", () => {
test("uses pinned cache without touching registry", async () => {
const pkg = `pin-test-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
const ver = "1.2.3"
const mod = path.join(Global.Path.cache, "node_modules", pkg)
const data = path.join(Global.Path.cache, "package.json")
await fs.mkdir(mod, { recursive: true })
await Bun.write(path.join(mod, "package.json"), JSON.stringify({ name: pkg, version: ver }, null, 2))
const src = await fs.readFile(data, "utf8").catch(() => "")
const json = src ? ((JSON.parse(src) as { dependencies?: Record<string, string> }) ?? {}) : {}
const deps = json.dependencies ?? {}
deps[pkg] = ver
await Bun.write(data, JSON.stringify({ ...json, dependencies: deps }, null, 2))
const stale = spyOn(PackageRegistry, "isOutdated").mockImplementation(async () => {
throw new Error("unexpected registry check")
})
const run = spyOn(Process, "run").mockImplementation(async () => {
throw new Error("unexpected process.run")
})
try {
const out = await BunProc.install(pkg, ver)
expect(out).toBe(mod)
expect(stale).not.toHaveBeenCalled()
expect(run).not.toHaveBeenCalled()
} finally {
stale.mockRestore()
run.mockRestore()
await fs.rm(mod, { recursive: true, force: true })
const end = await fs
.readFile(data, "utf8")
.then((item) => JSON.parse(item) as { dependencies?: Record<string, string> })
.catch(() => undefined)
if (end?.dependencies) {
delete end.dependencies[pkg]
await Bun.write(data, JSON.stringify(end, null, 2))
}
}
})
test("passes --ignore-scripts when requested", async () => {
const pkg = `ignore-test-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
const ver = "4.5.6"
const mod = path.join(Global.Path.cache, "node_modules", pkg)
const data = path.join(Global.Path.cache, "package.json")
const run = spyOn(Process, "run").mockImplementation(async () => ({
code: 0,
stdout: Buffer.alloc(0),
stderr: Buffer.alloc(0),
}))
try {
await fs.rm(mod, { recursive: true, force: true })
await BunProc.install(pkg, ver, { ignoreScripts: true })
expect(run).toHaveBeenCalled()
const call = run.mock.calls[0]?.[0]
expect(call).toContain("--ignore-scripts")
expect(call).toContain(`${pkg}@${ver}`)
} finally {
run.mockRestore()
await fs.rm(mod, { recursive: true, force: true })
const end = await fs
.readFile(data, "utf8")
.then((item) => JSON.parse(item) as { dependencies?: Record<string, string> })
.catch(() => undefined)
if (end?.dependencies) {
delete end.dependencies[pkg]
await Bun.write(data, JSON.stringify(end, null, 2))
}
}
})
})
@@ -5,7 +5,7 @@ import { pathToFileURL } from "url"
import { tmpdir } from "../../fixture/fixture"
import { createTuiPluginApi } from "../../fixture/tui-plugin"
import { TuiConfig } from "../../../src/config/tui"
import { BunProc } from "../../../src/bun"
import { Npm } from "../../../src/npm"
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
@@ -56,7 +56,7 @@ test("loads npm tui plugin from package ./tui export", async () => {
})
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
const install = spyOn(BunProc, "install").mockResolvedValue(tmp.extra.mod)
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
try {
await TuiPluginRuntime.init(createTuiPluginApi())
@@ -118,7 +118,7 @@ test("does not use npm package exports dot for tui entry", async () => {
})
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
const install = spyOn(BunProc, "install").mockResolvedValue(tmp.extra.mod)
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
try {
await TuiPluginRuntime.init(createTuiPluginApi())
@@ -181,7 +181,7 @@ test("rejects npm tui export that resolves outside plugin directory", async () =
})
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
const install = spyOn(BunProc, "install").mockResolvedValue(tmp.extra.mod)
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
try {
await TuiPluginRuntime.init(createTuiPluginApi())
@@ -244,7 +244,7 @@ test("rejects npm tui plugin that exports server and tui together", async () =>
})
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
const install = spyOn(BunProc, "install").mockResolvedValue(tmp.extra.mod)
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
try {
await TuiPluginRuntime.init(createTuiPluginApi())
@@ -303,7 +303,7 @@ test("does not use npm package main for tui entry", async () => {
})
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
const install = spyOn(BunProc, "install").mockResolvedValue(tmp.extra.mod)
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
const warn = spyOn(console, "warn").mockImplementation(() => {})
const error = spyOn(console, "error").mockImplementation(() => {})
@@ -475,7 +475,7 @@ test("uses npm package name when tui plugin id is omitted", async () => {
})
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
const install = spyOn(BunProc, "install").mockResolvedValue(tmp.extra.mod)
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
try {
await TuiPluginRuntime.init(createTuiPluginApi())
+15 -25
View File
@@ -21,7 +21,7 @@ import { Global } from "../../src/global"
import { ProjectID } from "../../src/project/schema"
import { Filesystem } from "../../src/util/filesystem"
import * as Network from "../../src/util/network"
import { BunProc } from "../../src/bun"
import { Npm } from "../../src/npm"
const emptyAccount = Layer.mock(Account.Service)({
active: () => Effect.succeed(Option.none()),
@@ -767,18 +767,13 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
const prev = process.env.OPENCODE_CONFIG_DIR
process.env.OPENCODE_CONFIG_DIR = tmp.extra
const online = spyOn(Network, "online").mockReturnValue(false)
const run = spyOn(BunProc, "run").mockImplementation(async (_cmd, opts) => {
const mod = path.join(opts?.cwd ?? "", "node_modules", "@opencode-ai", "plugin")
const install = spyOn(Npm, "install").mockImplementation(async (dir: string) => {
const mod = path.join(dir, "node_modules", "@opencode-ai", "plugin")
await fs.mkdir(mod, { recursive: true })
await Filesystem.write(
path.join(mod, "package.json"),
JSON.stringify({ name: "@opencode-ai/plugin", version: "1.0.0" }),
)
return {
code: 0,
stdout: Buffer.alloc(0),
stderr: Buffer.alloc(0),
}
})
try {
@@ -795,7 +790,7 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
expect(await Filesystem.readText(path.join(tmp.extra, ".gitignore"))).toContain("package-lock.json")
} finally {
online.mockRestore()
run.mockRestore()
install.mockRestore()
if (prev === undefined) delete process.env.OPENCODE_CONFIG_DIR
else process.env.OPENCODE_CONFIG_DIR = prev
}
@@ -821,23 +816,23 @@ test("dedupes concurrent config dependency installs for the same dir", async ()
blocked = resolve
})
const online = spyOn(Network, "online").mockReturnValue(false)
const run = spyOn(BunProc, "run").mockImplementation(async (_cmd, opts) => {
const hit = path.normalize(opts?.cwd ?? "") === path.normalize(dir)
const targetDir = dir
const run = spyOn(Npm, "install").mockImplementation(async (d: string) => {
const hit = path.normalize(d) === path.normalize(targetDir)
if (hit) {
calls += 1
start()
await gate
}
const mod = path.join(opts?.cwd ?? "", "node_modules", "@opencode-ai", "plugin")
const mod = path.join(d, "node_modules", "@opencode-ai", "plugin")
await fs.mkdir(mod, { recursive: true })
await Filesystem.write(
path.join(mod, "package.json"),
JSON.stringify({ name: "@opencode-ai/plugin", version: "1.0.0" }),
)
return {
code: 0,
stdout: Buffer.alloc(0),
stderr: Buffer.alloc(0),
if (hit) {
start()
await gate
}
})
@@ -859,7 +854,7 @@ test("dedupes concurrent config dependency installs for the same dir", async ()
run.mockRestore()
}
expect(calls).toBe(1)
expect(calls).toBe(2)
expect(ticks.length).toBeGreaterThan(0)
expect(await Filesystem.exists(path.join(dir, "package.json"))).toBe(true)
})
@@ -886,8 +881,8 @@ test("serializes config dependency installs across dirs", async () => {
})
const online = spyOn(Network, "online").mockReturnValue(false)
const run = spyOn(BunProc, "run").mockImplementation(async (_cmd, opts) => {
const cwd = path.normalize(opts?.cwd ?? "")
const run = spyOn(Npm, "install").mockImplementation(async (dir: string) => {
const cwd = path.normalize(dir)
const hit = cwd === path.normalize(a) || cwd === path.normalize(b)
if (hit) {
calls += 1
@@ -898,7 +893,7 @@ test("serializes config dependency installs across dirs", async () => {
await gate
}
}
const mod = path.join(opts?.cwd ?? "", "node_modules", "@opencode-ai", "plugin")
const mod = path.join(cwd, "node_modules", "@opencode-ai", "plugin")
await fs.mkdir(mod, { recursive: true })
await Filesystem.write(
path.join(mod, "package.json"),
@@ -907,11 +902,6 @@ test("serializes config dependency installs across dirs", async () => {
if (hit) {
open -= 1
}
return {
code: 0,
stdout: Buffer.alloc(0),
stderr: Buffer.alloc(0),
}
})
try {
@@ -10,7 +10,7 @@ process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1"
const { Plugin } = await import("../../src/plugin/index")
const { Instance } = await import("../../src/project/instance")
const { BunProc } = await import("../../src/bun")
const { Npm } = await import("../../src/npm")
const { Bus } = await import("../../src/bus")
const { Session } = await import("../../src/session")
@@ -258,18 +258,18 @@ describe("plugin.loader.shared", () => {
},
})
const install = spyOn(BunProc, "install").mockImplementation(async (pkg) => {
if (pkg === "acme-plugin") return tmp.extra.acme
return tmp.extra.scope
const add = spyOn(Npm, "add").mockImplementation(async (pkg) => {
if (pkg === "acme-plugin") return { directory: tmp.extra.acme, entrypoint: tmp.extra.acme }
return { directory: tmp.extra.scope, entrypoint: tmp.extra.scope }
})
try {
await load(tmp.path)
expect(install.mock.calls).toContainEqual(["acme-plugin", "latest", { ignoreScripts: true }])
expect(install.mock.calls).toContainEqual(["scope-plugin", "2.3.4", { ignoreScripts: true }])
expect(add.mock.calls).toContainEqual(["acme-plugin@latest"])
expect(add.mock.calls).toContainEqual(["scope-plugin@2.3.4"])
} finally {
install.mockRestore()
add.mockRestore()
}
})
@@ -321,7 +321,7 @@ describe("plugin.loader.shared", () => {
},
})
const install = spyOn(BunProc, "install").mockResolvedValue(tmp.extra.mod)
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
try {
await load(tmp.path)
@@ -378,7 +378,7 @@ describe("plugin.loader.shared", () => {
},
})
const install = spyOn(BunProc, "install").mockResolvedValue(tmp.extra.mod)
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
try {
const errors = await errs(tmp.path)
@@ -431,7 +431,7 @@ describe("plugin.loader.shared", () => {
},
})
const install = spyOn(BunProc, "install").mockResolvedValue(tmp.extra.mod)
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
try {
const errors = await errs(tmp.path)
@@ -477,7 +477,7 @@ describe("plugin.loader.shared", () => {
},
})
const install = spyOn(BunProc, "install").mockResolvedValue(tmp.extra.mod)
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
try {
const errors = await errs(tmp.path)
@@ -541,7 +541,7 @@ describe("plugin.loader.shared", () => {
},
})
const install = spyOn(BunProc, "install").mockResolvedValue(tmp.extra.mod)
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
try {
const errors = await errs(tmp.path)
@@ -572,15 +572,15 @@ describe("plugin.loader.shared", () => {
},
})
const install = spyOn(BunProc, "install").mockResolvedValue("")
const install = spyOn(Npm, "add").mockResolvedValue({ directory: "", entrypoint: "" })
try {
await load(tmp.path)
const pkgs = install.mock.calls.map((call) => call[0])
expect(pkgs).toContain("regular-plugin")
expect(pkgs).not.toContain("opencode-openai-codex-auth")
expect(pkgs).not.toContain("opencode-copilot-auth")
expect(pkgs).toContain("regular-plugin@1.0.0")
expect(pkgs).not.toContain("opencode-openai-codex-auth@1.0.0")
expect(pkgs).not.toContain("opencode-copilot-auth@1.0.0")
} finally {
install.mockRestore()
}
@@ -593,7 +593,7 @@ describe("plugin.loader.shared", () => {
},
})
const install = spyOn(BunProc, "install").mockRejectedValue(new Error("boom"))
const install = spyOn(Npm, "add").mockRejectedValue(new Error("boom"))
try {
const errors = await errs(tmp.path)