core: fix npm package detection to properly handle cached directories without installed packages (#25354)
This commit is contained in:
@@ -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))
|
return resolveEntryPoint(name, path.join(dir, "node_modules", name))
|
||||||
}
|
}
|
||||||
|
|
||||||
const tree = yield* reify({ dir, add: [pkg] })
|
const tree = yield* reify({ dir, add: [pkg] })
|
||||||
const first = tree.edgesOut.values().next().value?.to
|
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)
|
return resolveEntryPoint(first.name, first.path)
|
||||||
}, Effect.scoped)
|
}, Effect.scoped)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { describe, expect, test } from "bun:test"
|
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 { Npm } from "@opencode-ai/core/npm"
|
||||||
|
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
|
|
||||||
const win = process.platform === "win32"
|
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", () => {
|
describe("Npm.sanitize", () => {
|
||||||
test("keeps normal scoped package specs unchanged", () => {
|
test("keeps normal scoped package specs unchanged", () => {
|
||||||
expect(Npm.sanitize("@opencode/acme")).toBe("@opencode/acme")
|
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", () => {
|
describe("Npm.install", () => {
|
||||||
test("respects omit from project .npmrc", async () => {
|
test("respects omit from project .npmrc", async () => {
|
||||||
await using tmp = await tmpdir()
|
await using tmp = await tmpdir()
|
||||||
|
|||||||
Reference in New Issue
Block a user