summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorShoubhit Dash <[email protected]>2026-04-23 19:54:01 +0530
committerGitHub <[email protected]>2026-04-23 19:54:01 +0530
commit38deb0f3eeedb9da68f80b398a694622602162bb (patch)
treec5d7ea0b3339406d2f692a296ad9306f7880c97d
parent9b6db08d2144c33ec34cd88026774f847ec79761 (diff)
downloadopencode-38deb0f3eeedb9da68f80b398a694622602162bb.tar.gz
opencode-38deb0f3eeedb9da68f80b398a694622602162bb.zip
fix(npm): respect npmrc config (#24001)
-rw-r--r--packages/opencode/src/npm/index.ts38
-rw-r--r--packages/opencode/test/npm.test.ts37
2 files changed, 73 insertions, 2 deletions
diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts
index fc8497d20..d6322d548 100644
--- a/packages/opencode/src/npm/index.ts
+++ b/packages/opencode/src/npm/index.ts
@@ -1,8 +1,11 @@
export * as Npm from "."
import path from "path"
+import { fileURLToPath } from "url"
import npa from "npm-package-arg"
import semver from "semver"
+import Config from "@npmcli/config"
+import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js"
import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect"
import { NodeFileSystem } from "@effect/platform-node"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
@@ -40,12 +43,39 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Npm") {}
const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined
+const npmPath = fileURLToPath(new URL("../..", import.meta.url))
export function sanitize(pkg: string) {
if (!illegal) return pkg
return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("")
}
+const loadOptions = (dir: string) =>
+ Effect.tryPromise({
+ try: async () => {
+ const config = new Config({
+ npmPath,
+ cwd: dir,
+ env: { ...process.env },
+ argv: [process.execPath, process.execPath],
+ execPath: process.execPath,
+ platform: process.platform,
+ definitions,
+ flatten,
+ nerfDarts,
+ shorthands,
+ warn: false,
+ })
+ await config.load()
+ return config.flat
+ },
+ catch: (cause) =>
+ new InstallFailedError({
+ cause,
+ dir,
+ }),
+ })
+
const resolveEntryPoint = (name: string, dir: string): EntryPoint => {
let entrypoint: Option.Option<string>
try {
@@ -81,7 +111,10 @@ export const layer = Layer.effect(
Effect.gen(function* () {
yield* flock.acquire(`npm-install:${input.dir}`)
const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"))
+ const add = input.add ?? []
+ const npmOptions = yield* loadOptions(input.dir)
const arborist = new Arborist({
+ ...npmOptions,
path: input.dir,
binLinks: true,
progress: false,
@@ -91,14 +124,15 @@ export const layer = Layer.effect(
return yield* Effect.tryPromise({
try: () =>
arborist.reify({
- add: input?.add || [],
+ ...npmOptions,
+ add,
save: true,
saveType: "prod",
}),
catch: (cause) =>
new InstallFailedError({
cause,
- add: input?.add,
+ add,
dir: input.dir,
}),
}) as Effect.Effect<ArboristTree, InstallFailedError>
diff --git a/packages/opencode/test/npm.test.ts b/packages/opencode/test/npm.test.ts
index 61e3ca6dd..a8ec92c2a 100644
--- a/packages/opencode/test/npm.test.ts
+++ b/packages/opencode/test/npm.test.ts
@@ -1,7 +1,18 @@
+import fs from "fs/promises"
+import path from "path"
import { describe, expect, test } from "bun:test"
import { Npm } from "../src/npm"
+import { tmpdir } from "./fixture/fixture"
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,
+ }),
+ )
describe("Npm.sanitize", () => {
test("keeps normal scoped package specs unchanged", () => {
@@ -16,3 +27,29 @@ describe("Npm.sanitize", () => {
expect(Npm.sanitize(spec)).toBe(expected)
})
})
+
+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()
+ })
+})