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) => 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() }) })