summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-05-01 15:49:14 -0500
committerGitHub <[email protected]>2026-05-01 15:49:14 -0500
commit478156456e92c3db04803953127b4a4af2db064c (patch)
tree29f628ad58e57d5feee65f7f25271e676939c06d /packages
parent6252412d94c91c83bb76f98686f4c987903019e9 (diff)
downloadopencode-478156456e92c3db04803953127b4a4af2db064c.tar.gz
opencode-478156456e92c3db04803953127b4a4af2db064c.zip
core: fix npm package detection to properly handle cached directories without installed packages (#25354)
Diffstat (limited to 'packages')
-rw-r--r--packages/core/src/npm.ts8
-rw-r--r--packages/core/test/npm.test.ts35
2 files changed, 41 insertions, 2 deletions
diff --git a/packages/core/src/npm.ts b/packages/core/src/npm.ts
index 92e404276..8dac8faf0 100644
--- a/packages/core/src/npm.ts
+++ b/packages/core/src/npm.ts
@@ -120,13 +120,17 @@ export const layer = Layer.effect(
}
})()
- if (yield* afs.existsSafe(dir)) {
+ if (yield* afs.existsSafe(path.join(dir, "node_modules", name))) {
return resolveEntryPoint(name, path.join(dir, "node_modules", name))
}
const tree = yield* reify({ dir, add: [pkg] })
const first = tree.edgesOut.values().next().value?.to
- if (!first) return yield* new InstallFailedError({ add: [pkg], dir })
+ if (!first) {
+ const result = resolveEntryPoint(name, path.join(dir, "node_modules", name))
+ if (Option.isSome(result.entrypoint)) return result
+ return yield* new InstallFailedError({ add: [pkg], dir })
+ }
return resolveEntryPoint(first.name, first.path)
}, Effect.scoped)
diff --git a/packages/core/test/npm.test.ts b/packages/core/test/npm.test.ts
index 3e94a0869..3d0767aaf 100644
--- a/packages/core/test/npm.test.ts
+++ b/packages/core/test/npm.test.ts
@@ -1,7 +1,12 @@
import fs from "fs/promises"
import path from "path"
import { describe, expect, test } from "bun:test"
+import { NodeFileSystem } from "@effect/platform-node"
+import { Effect, Layer, Option } from "effect"
+import { AppFileSystem } from "@opencode-ai/core/filesystem"
+import { Global } from "@opencode-ai/core/global"
import { Npm } from "@opencode-ai/core/npm"
+import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { tmpdir } from "./fixture/tmpdir"
const win = process.platform === "win32"
@@ -15,6 +20,14 @@ const writePackage = (dir: string, pkg: Record<string, unknown>) =>
}),
)
+const npmLayer = (cache: string) =>
+ Npm.layer.pipe(
+ Layer.provide(EffectFlock.layer),
+ Layer.provide(AppFileSystem.layer),
+ Layer.provide(Global.layerWith({ cache, state: path.join(cache, "state") })),
+ Layer.provide(NodeFileSystem.layer),
+ )
+
describe("Npm.sanitize", () => {
test("keeps normal scoped package specs unchanged", () => {
expect(Npm.sanitize("@opencode/acme")).toBe("@opencode/acme")
@@ -29,6 +42,28 @@ describe("Npm.sanitize", () => {
})
})
+describe("Npm.add", () => {
+ test("reifies when package cache directory exists without the package installed", async () => {
+ await using tmp = await tmpdir()
+ await fs.mkdir(path.join(tmp.path, "fixture-provider"))
+ await writePackage(path.join(tmp.path, "fixture-provider"), {
+ name: "fixture-provider",
+ main: "index.js",
+ })
+ await Bun.write(path.join(tmp.path, "fixture-provider", "index.js"), "export const fixture = true\n")
+
+ const spec = `fixture-provider@file:${path.join(tmp.path, "fixture-provider")}`
+ await fs.mkdir(path.join(tmp.path, "cache", "packages", Npm.sanitize(spec)), { recursive: true })
+
+ const entry = await Effect.gen(function* () {
+ const npm = yield* Npm.Service
+ return yield* npm.add(spec)
+ }).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise)
+
+ expect(Option.isSome(entry.entrypoint)).toBe(true)
+ })
+})
+
describe("Npm.install", () => {
test("respects omit from project .npmrc", async () => {
await using tmp = await tmpdir()