Skip to content

Commit 95bd891

Browse files
committed
fix(plugin): load npm config for Arborist installs
1 parent fd4c343 commit 95bd891

2 files changed

Lines changed: 127 additions & 1 deletion

File tree

packages/opencode/src/npm/index.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ import { readdir, rm } from "fs/promises"
77
import { Filesystem } from "@/util/filesystem"
88
import { Flock } from "@/util/flock"
99
import { Arborist } from "@npmcli/arborist"
10+
import Config from "@npmcli/config"
11+
// @ts-ignore documented @npmcli/config integration uses this subpath, but @types/npmcli__config does not declare it
12+
import { definitions, flatten, shorthands } from "@npmcli/config/lib/definitions"
1013

1114
export namespace Npm {
1215
const log = Log.create({ service: "npm" })
1316
const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined
17+
const npmdir = import.meta.dirname
1418

1519
export const InstallFailedError = NamedError.create(
1620
"NpmInstallFailedError",
@@ -40,14 +44,32 @@ export namespace Npm {
4044
return result
4145
}
4246

47+
async function opts(cwd: string, env = process.env) {
48+
const conf = new Config({
49+
cwd,
50+
env,
51+
argv: [],
52+
execPath: process.execPath,
53+
platform: process.platform,
54+
npmPath: npmdir,
55+
definitions,
56+
shorthands,
57+
flatten,
58+
})
59+
await conf.load()
60+
return conf.flat
61+
}
62+
4363
export async function add(pkg: string) {
4464
const dir = directory(pkg)
4565
await using _ = await Flock.acquire(`npm-install:${Filesystem.resolve(dir)}`)
4666
log.info("installing package", {
4767
pkg,
4868
})
4969

70+
const cfg = await opts(Global.Path.cache)
5071
const arborist = new Arborist({
72+
...cfg,
5173
path: dir,
5274
binLinks: true,
5375
progress: false,
@@ -68,7 +90,7 @@ export namespace Npm {
6890
save: true,
6991
saveType: "prod",
7092
})
71-
.catch((cause) => {
93+
.catch((cause: unknown) => {
7294
throw new InstallFailedError(
7395
{ pkg },
7496
{
@@ -87,7 +109,9 @@ export namespace Npm {
87109
log.info("checking dependencies", { dir })
88110

89111
const reify = async () => {
112+
const cfg = await opts(dir)
90113
const arb = new Arborist({
114+
...cfg,
91115
path: dir,
92116
binLinks: true,
93117
progress: false,
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test"
2+
import fs from "fs/promises"
3+
import os from "os"
4+
import path from "path"
5+
import { tmpdir } from "../fixture/fixture"
6+
7+
const base = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-npm-"))
8+
const xdg = path.join(base, "xdg")
9+
const home = path.join(base, "home")
10+
const prev = {
11+
HOME: process.env.HOME,
12+
NPM_CONFIG_USERCONFIG: process.env.NPM_CONFIG_USERCONFIG,
13+
XDG_CACHE_HOME: process.env.XDG_CACHE_HOME,
14+
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
15+
XDG_DATA_HOME: process.env.XDG_DATA_HOME,
16+
XDG_STATE_HOME: process.env.XDG_STATE_HOME,
17+
npm_config_registry: process.env.npm_config_registry,
18+
npm_config_userconfig: process.env.npm_config_userconfig,
19+
}
20+
21+
await fs.mkdir(xdg, { recursive: true })
22+
await fs.mkdir(home, { recursive: true })
23+
24+
process.env.HOME = home
25+
process.env.XDG_CACHE_HOME = path.join(xdg, "cache")
26+
process.env.XDG_CONFIG_HOME = path.join(xdg, "config")
27+
process.env.XDG_DATA_HOME = path.join(xdg, "data")
28+
process.env.XDG_STATE_HOME = path.join(xdg, "state")
29+
30+
const seen: Array<Record<string, unknown>> = []
31+
32+
mock.module("@npmcli/arborist", () => ({
33+
Arborist: class {
34+
constructor(input: Record<string, unknown>) {
35+
seen.push(input)
36+
}
37+
38+
async loadVirtual() {
39+
return undefined
40+
}
41+
42+
async reify() {
43+
return {
44+
edgesOut: new Map([["pkg", { to: { name: "@tngtech/opencode-skainet", path: base } }]]),
45+
}
46+
}
47+
},
48+
}))
49+
50+
const { Global } = await import("../../src/global")
51+
const { Npm } = await import("../../src/npm")
52+
53+
beforeEach(async () => {
54+
seen.length = 0
55+
delete process.env.NPM_CONFIG_USERCONFIG
56+
delete process.env.npm_config_registry
57+
delete process.env.npm_config_userconfig
58+
await fs.rm(home, { recursive: true, force: true })
59+
await fs.mkdir(home, { recursive: true })
60+
await fs.rm(Global.Path.cache, { recursive: true, force: true })
61+
await fs.mkdir(Global.Path.cache, { recursive: true })
62+
await fs.writeFile(path.join(Global.Path.cache, "version"), "21")
63+
})
64+
65+
afterAll(async () => {
66+
for (const [key, value] of Object.entries(prev)) {
67+
if (value === undefined) delete process.env[key]
68+
else process.env[key] = value
69+
}
70+
await fs.rm(base, { recursive: true, force: true })
71+
})
72+
73+
describe("npm", () => {
74+
test("add reads scoped registry from user npmrc", async () => {
75+
await fs.writeFile(path.join(home, ".npmrc"), "@tngtech:registry=https://user.example/\n")
76+
77+
await Npm.add("@tngtech/opencode-skainet@latest")
78+
79+
expect(seen[0]?.["@tngtech:registry"]).toBe("https://user.example/")
80+
expect(seen[0]?.path).toBe(
81+
path.join(Global.Path.cache, "packages", Npm.sanitize("@tngtech/opencode-skainet@latest")),
82+
)
83+
})
84+
85+
test("add keeps cache root npmrc as local config", async () => {
86+
await fs.writeFile(path.join(Global.Path.cache, ".npmrc"), "@tngtech:registry=https://cache.example/\n")
87+
88+
await Npm.add("@tngtech/opencode-skainet@latest")
89+
90+
expect(seen[0]?.["@tngtech:registry"]).toBe("https://cache.example/")
91+
})
92+
93+
test("install reads local npmrc from install dir", async () => {
94+
await using tmp = await tmpdir()
95+
await fs.writeFile(path.join(tmp.path, ".npmrc"), "registry=https://dir.example/\n")
96+
97+
await Npm.install(tmp.path)
98+
99+
expect(seen[0]?.registry).toBe("https://dir.example/")
100+
expect(seen[0]?.path).toBe(tmp.path)
101+
})
102+
})

0 commit comments

Comments
 (0)