Skip to content

Commit 4781564

Browse files
authored
core: fix npm package detection to properly handle cached directories without installed packages (#25354)
1 parent 6252412 commit 4781564

2 files changed

Lines changed: 41 additions & 2 deletions

File tree

packages/core/src/npm.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,17 @@ export const layer = Layer.effect(
120120
}
121121
})()
122122

123-
if (yield* afs.existsSafe(dir)) {
123+
if (yield* afs.existsSafe(path.join(dir, "node_modules", name))) {
124124
return resolveEntryPoint(name, path.join(dir, "node_modules", name))
125125
}
126126

127127
const tree = yield* reify({ dir, add: [pkg] })
128128
const first = tree.edgesOut.values().next().value?.to
129-
if (!first) return yield* new InstallFailedError({ add: [pkg], dir })
129+
if (!first) {
130+
const result = resolveEntryPoint(name, path.join(dir, "node_modules", name))
131+
if (Option.isSome(result.entrypoint)) return result
132+
return yield* new InstallFailedError({ add: [pkg], dir })
133+
}
130134
return resolveEntryPoint(first.name, first.path)
131135
}, Effect.scoped)
132136

packages/core/test/npm.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import fs from "fs/promises"
22
import path from "path"
33
import { describe, expect, test } from "bun:test"
4+
import { NodeFileSystem } from "@effect/platform-node"
5+
import { Effect, Layer, Option } from "effect"
6+
import { AppFileSystem } from "@opencode-ai/core/filesystem"
7+
import { Global } from "@opencode-ai/core/global"
48
import { Npm } from "@opencode-ai/core/npm"
9+
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
510
import { tmpdir } from "./fixture/tmpdir"
611

712
const win = process.platform === "win32"
@@ -15,6 +20,14 @@ const writePackage = (dir: string, pkg: Record<string, unknown>) =>
1520
}),
1621
)
1722

23+
const npmLayer = (cache: string) =>
24+
Npm.layer.pipe(
25+
Layer.provide(EffectFlock.layer),
26+
Layer.provide(AppFileSystem.layer),
27+
Layer.provide(Global.layerWith({ cache, state: path.join(cache, "state") })),
28+
Layer.provide(NodeFileSystem.layer),
29+
)
30+
1831
describe("Npm.sanitize", () => {
1932
test("keeps normal scoped package specs unchanged", () => {
2033
expect(Npm.sanitize("@opencode/acme")).toBe("@opencode/acme")
@@ -29,6 +42,28 @@ describe("Npm.sanitize", () => {
2942
})
3043
})
3144

45+
describe("Npm.add", () => {
46+
test("reifies when package cache directory exists without the package installed", async () => {
47+
await using tmp = await tmpdir()
48+
await fs.mkdir(path.join(tmp.path, "fixture-provider"))
49+
await writePackage(path.join(tmp.path, "fixture-provider"), {
50+
name: "fixture-provider",
51+
main: "index.js",
52+
})
53+
await Bun.write(path.join(tmp.path, "fixture-provider", "index.js"), "export const fixture = true\n")
54+
55+
const spec = `fixture-provider@file:${path.join(tmp.path, "fixture-provider")}`
56+
await fs.mkdir(path.join(tmp.path, "cache", "packages", Npm.sanitize(spec)), { recursive: true })
57+
58+
const entry = await Effect.gen(function* () {
59+
const npm = yield* Npm.Service
60+
return yield* npm.add(spec)
61+
}).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise)
62+
63+
expect(Option.isSome(entry.entrypoint)).toBe(true)
64+
})
65+
})
66+
3267
describe("Npm.install", () => {
3368
test("respects omit from project .npmrc", async () => {
3469
await using tmp = await tmpdir()

0 commit comments

Comments
 (0)