Skip to content

Commit ab15fc1

Browse files
authored
refactor: collapse npm barrel into npm/index.ts (#22911)
1 parent 99d392a commit ab15fc1

2 files changed

Lines changed: 189 additions & 188 deletions

File tree

packages/opencode/src/npm/index.ts

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,189 @@
1-
export * as Npm from "./npm"
1+
import semver from "semver"
2+
import z from "zod"
3+
import { NamedError } from "@opencode-ai/shared/util/error"
4+
import { Global } from "../global"
5+
import { Log } from "../util"
6+
import path from "path"
7+
import { readdir, rm } from "fs/promises"
8+
import { Filesystem } from "@/util"
9+
import { Flock } from "@opencode-ai/shared/util/flock"
10+
11+
const log = Log.create({ service: "npm" })
12+
const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined
13+
14+
export const InstallFailedError = NamedError.create(
15+
"NpmInstallFailedError",
16+
z.object({
17+
pkg: z.string(),
18+
}),
19+
)
20+
21+
export function sanitize(pkg: string) {
22+
if (!illegal) return pkg
23+
return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("")
24+
}
25+
26+
function directory(pkg: string) {
27+
return path.join(Global.Path.cache, "packages", sanitize(pkg))
28+
}
29+
30+
function resolveEntryPoint(name: string, dir: string) {
31+
let entrypoint: string | undefined
32+
try {
33+
entrypoint = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
34+
} catch {}
35+
const result = {
36+
directory: dir,
37+
entrypoint,
38+
}
39+
return result
40+
}
41+
42+
export async function outdated(pkg: string, cachedVersion: string): Promise<boolean> {
43+
const response = await fetch(`https://registry.npmjs.org/${pkg}`)
44+
if (!response.ok) {
45+
log.warn("Failed to resolve latest version, using cached", { pkg, cachedVersion })
46+
return false
47+
}
48+
49+
const data = (await response.json()) as { "dist-tags"?: { latest?: string } }
50+
const latestVersion = data?.["dist-tags"]?.latest
51+
if (!latestVersion) {
52+
log.warn("No latest version found, using cached", { pkg, cachedVersion })
53+
return false
54+
}
55+
56+
const range = /[\s^~*xX<>|=]/.test(cachedVersion)
57+
if (range) return !semver.satisfies(latestVersion, cachedVersion)
58+
59+
return semver.lt(cachedVersion, latestVersion)
60+
}
61+
62+
export async function add(pkg: string) {
63+
const { Arborist } = await import("@npmcli/arborist")
64+
const dir = directory(pkg)
65+
await using _ = await Flock.acquire(`npm-install:${Filesystem.resolve(dir)}`)
66+
log.info("installing package", {
67+
pkg,
68+
})
69+
70+
const arborist = new Arborist({
71+
path: dir,
72+
binLinks: true,
73+
progress: false,
74+
savePrefix: "",
75+
ignoreScripts: true,
76+
})
77+
const tree = await arborist.loadVirtual().catch(() => {})
78+
if (tree) {
79+
const first = tree.edgesOut.values().next().value?.to
80+
if (first) {
81+
return resolveEntryPoint(first.name, first.path)
82+
}
83+
}
84+
85+
const result = await arborist
86+
.reify({
87+
add: [pkg],
88+
save: true,
89+
saveType: "prod",
90+
})
91+
.catch((cause) => {
92+
throw new InstallFailedError(
93+
{ pkg },
94+
{
95+
cause,
96+
},
97+
)
98+
})
99+
100+
const first = result.edgesOut.values().next().value?.to
101+
if (!first) throw new InstallFailedError({ pkg })
102+
return resolveEntryPoint(first.name, first.path)
103+
}
104+
105+
export async function install(dir: string) {
106+
await using _ = await Flock.acquire(`npm-install:${dir}`)
107+
log.info("checking dependencies", { dir })
108+
109+
const reify = async () => {
110+
const { Arborist } = await import("@npmcli/arborist")
111+
const arb = new Arborist({
112+
path: dir,
113+
binLinks: true,
114+
progress: false,
115+
savePrefix: "",
116+
ignoreScripts: true,
117+
})
118+
await arb.reify().catch(() => {})
119+
}
120+
121+
if (!(await Filesystem.exists(path.join(dir, "node_modules")))) {
122+
log.info("node_modules missing, reifying")
123+
await reify()
124+
return
125+
}
126+
127+
const pkg = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({}))
128+
const lock = await Filesystem.readJson(path.join(dir, "package-lock.json")).catch(() => ({}))
129+
130+
const declared = new Set([
131+
...Object.keys(pkg.dependencies || {}),
132+
...Object.keys(pkg.devDependencies || {}),
133+
...Object.keys(pkg.peerDependencies || {}),
134+
...Object.keys(pkg.optionalDependencies || {}),
135+
])
136+
137+
const root = lock.packages?.[""] || {}
138+
const locked = new Set([
139+
...Object.keys(root.dependencies || {}),
140+
...Object.keys(root.devDependencies || {}),
141+
...Object.keys(root.peerDependencies || {}),
142+
...Object.keys(root.optionalDependencies || {}),
143+
])
144+
145+
for (const name of declared) {
146+
if (!locked.has(name)) {
147+
log.info("dependency not in lock file, reifying", { name })
148+
await reify()
149+
return
150+
}
151+
}
152+
153+
log.info("dependencies in sync")
154+
}
155+
156+
export async function which(pkg: string) {
157+
const dir = directory(pkg)
158+
const binDir = path.join(dir, "node_modules", ".bin")
159+
160+
const pick = async () => {
161+
const files = await readdir(binDir).catch(() => [])
162+
if (files.length === 0) return undefined
163+
if (files.length === 1) return files[0]
164+
// Multiple binaries — resolve from package.json bin field like npx does
165+
const pkgJson = await Filesystem.readJson<{ bin?: string | Record<string, string> }>(
166+
path.join(dir, "node_modules", pkg, "package.json"),
167+
).catch(() => undefined)
168+
if (pkgJson?.bin) {
169+
const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg
170+
const bin = pkgJson.bin
171+
if (typeof bin === "string") return unscoped
172+
const keys = Object.keys(bin)
173+
if (keys.length === 1) return keys[0]
174+
return bin[unscoped] ? unscoped : keys[0]
175+
}
176+
return files[0]
177+
}
178+
179+
const bin = await pick()
180+
if (bin) return path.join(binDir, bin)
181+
182+
await rm(path.join(dir, "package-lock.json"), { force: true })
183+
await add(pkg)
184+
const resolved = await pick()
185+
if (!resolved) return
186+
return path.join(binDir, resolved)
187+
}
188+
189+
export * as Npm from "."

packages/opencode/src/npm/npm.ts

Lines changed: 0 additions & 187 deletions
This file was deleted.

0 commit comments

Comments
 (0)