From 38deb0f3eeedb9da68f80b398a694622602162bb Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 23 Apr 2026 19:54:01 +0530 Subject: [PATCH] fix(npm): respect npmrc config (#24001) --- packages/opencode/src/npm/index.ts | 38 ++++++++++++++++++++++++++++-- packages/opencode/test/npm.test.ts | 37 +++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts index fc8497d20b..d6322d5488 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()("@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 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 diff --git a/packages/opencode/test/npm.test.ts b/packages/opencode/test/npm.test.ts index 61e3ca6ddf..a8ec92c2a7 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) => + 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() + }) +})