Преглед изворни кода

core: fix npm package detection to properly handle cached directories without installed packages (#25354)

Aiden Cline пре 3 месеци
родитељ
комит
478156456e
2 измењених фајлова са 41 додато и 2 уклоњено
  1. 6 2
      packages/core/src/npm.ts
  2. 35 0
      packages/core/test/npm.test.ts

+ 6 - 2
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)
 

+ 35 - 0
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()