154 lines
5.7 KiB
TypeScript
154 lines
5.7 KiB
TypeScript
import fs from "fs/promises"
|
|
import path from "path"
|
|
import { pathToFileURL } from "url"
|
|
import { describe, expect, test } from "bun:test"
|
|
import { Effect, Option } from "effect"
|
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
import { Global } from "@opencode-ai/core/global"
|
|
import { Npm } from "@opencode-ai/core/npm"
|
|
import { which } from "@opencode-ai/core/util/which"
|
|
import { tmpdir } from "./fixture/tmpdir"
|
|
|
|
const win = process.platform === "win32"
|
|
|
|
const writePackage = (dir: string, pkg: Record<string, unknown>) =>
|
|
Bun.write(
|
|
path.join(dir, "package.json"),
|
|
JSON.stringify({
|
|
version: "1.0.0",
|
|
...pkg,
|
|
}),
|
|
)
|
|
|
|
const npmLayer = (cache: string) =>
|
|
AppNodeBuilder.build(Npm.node, [[Global.node, Global.layerWith({ cache, state: path.join(cache, "state") })]])
|
|
|
|
describe("Npm.sanitize", () => {
|
|
test("keeps normal scoped package specs unchanged", () => {
|
|
expect(Npm.sanitize("@opencode/acme")).toBe("@opencode/acme")
|
|
expect(Npm.sanitize("@opencode/acme@1.0.0")).toBe("@opencode/acme@1.0.0")
|
|
expect(Npm.sanitize("prettier")).toBe("prettier")
|
|
})
|
|
|
|
test("handles git https specs", () => {
|
|
const spec = "acme@git+https://github.com/opencode/acme.git"
|
|
const expected = win ? "acme@git+https_//github.com/opencode/acme.git" : spec
|
|
expect(Npm.sanitize(spec)).toBe(expected)
|
|
})
|
|
})
|
|
|
|
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(entry.entrypoint).toBeDefined()
|
|
})
|
|
|
|
// The Desktop sidecar runs the server under Node, where import.meta.resolve cannot take a
|
|
// parent URL. Exercise the real Node branch instead of the Bun one the test runner uses.
|
|
test("resolves an importable file URL under Node", async () => {
|
|
await using tmp = await tmpdir()
|
|
const node = which("node")
|
|
if (!node) throw new Error("Node is required for the Npm Node runtime test")
|
|
|
|
const bundle = await Bun.build({
|
|
entrypoints: [path.join(import.meta.dir, "../src/npm.ts")],
|
|
target: "node",
|
|
format: "esm",
|
|
})
|
|
expect(bundle.success).toBe(true)
|
|
const entry = path.join(tmp.path, "npm.mjs")
|
|
await Bun.write(entry, bundle.outputs[0])
|
|
|
|
const dual = path.join(tmp.path, "dual-provider")
|
|
await writePackage(dual, {
|
|
name: "dual-provider",
|
|
exports: { ".": { import: "./dist/index.mjs", require: "./dist/index.js" } },
|
|
})
|
|
await Bun.write(path.join(dual, "dist", "index.mjs"), "export const createDual = () => 'esm'\n")
|
|
await Bun.write(path.join(dual, "dist", "index.js"), "exports.createDual = () => 'cjs'\n")
|
|
|
|
const scoped = path.join(tmp.path, "scoped-provider")
|
|
await writePackage(scoped, {
|
|
name: "@fixture/scoped-provider",
|
|
type: "module",
|
|
exports: "./dist/index.js",
|
|
})
|
|
await Bun.write(path.join(scoped, "dist", "index.js"), "export const createScoped = () => 'scoped'\n")
|
|
|
|
const proc = Bun.spawn(
|
|
[
|
|
node,
|
|
"--input-type=module",
|
|
"-e",
|
|
`
|
|
import assert from "node:assert/strict"
|
|
import { Npm } from ${JSON.stringify(pathToFileURL(entry).href)}
|
|
assert.equal(typeof Bun, "undefined")
|
|
for (const [spec, name] of [
|
|
[${JSON.stringify(`dual-provider@file:${dual}`)}, "createDual"],
|
|
[${JSON.stringify(`@fixture/scoped-provider@file:${scoped}`)}, "createScoped"],
|
|
]) {
|
|
const result = await Npm.add(spec)
|
|
assert.ok(result.entrypoint?.startsWith("file://"), "entrypoint is a file URL: " + result.entrypoint)
|
|
const mod = await import(result.entrypoint)
|
|
assert.equal(typeof mod[name], "function", "module exports " + name)
|
|
}
|
|
process.exit(0)
|
|
`,
|
|
],
|
|
{
|
|
env: { ...process.env, XDG_CACHE_HOME: path.join(tmp.path, "cache") },
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
},
|
|
)
|
|
const [stdout, stderr, code] = await Promise.all([
|
|
new Response(proc.stdout).text(),
|
|
new Response(proc.stderr).text(),
|
|
proc.exited,
|
|
])
|
|
expect(stderr, stdout).toBe("")
|
|
expect(code).toBe(0)
|
|
}, 30_000)
|
|
})
|
|
|
|
describe("Npm.install", () => {
|
|
test("respects omit from project .npmrc", async () => {
|
|
await using tmp = await tmpdir()
|
|
|
|
await writePackage(tmp.path, {
|
|
name: "fixture",
|
|
dependencies: {
|
|
"prod-pkg": "file:./prod-pkg",
|
|
},
|
|
devDependencies: {
|
|
"dev-pkg": "file:./dev-pkg",
|
|
},
|
|
})
|
|
await Bun.write(path.join(tmp.path, ".npmrc"), "omit=dev\n")
|
|
await fs.mkdir(path.join(tmp.path, "prod-pkg"))
|
|
await fs.mkdir(path.join(tmp.path, "dev-pkg"))
|
|
await writePackage(path.join(tmp.path, "prod-pkg"), { name: "prod-pkg" })
|
|
await writePackage(path.join(tmp.path, "dev-pkg"), { name: "dev-pkg" })
|
|
|
|
await Npm.install(tmp.path)
|
|
|
|
await expect(fs.stat(path.join(tmp.path, "node_modules", "prod-pkg"))).resolves.toBeDefined()
|
|
await expect(fs.stat(path.join(tmp.path, "node_modules", "dev-pkg"))).rejects.toThrow()
|
|
})
|
|
})
|