fix(npm): respect npmrc config (#24001)

This commit is contained in:
Shoubhit Dash
2026-04-23 19:54:01 +05:30
committed by GitHub
parent 9b6db08d21
commit 38deb0f3ee
2 changed files with 73 additions and 2 deletions

View File

@@ -1,8 +1,11 @@
export * as Npm from "." export * as Npm from "."
import path from "path" import path from "path"
import { fileURLToPath } from "url"
import npa from "npm-package-arg" import npa from "npm-package-arg"
import semver from "semver" 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 { Effect, Schema, Context, Layer, Option, FileSystem } from "effect"
import { NodeFileSystem } from "@effect/platform-node" import { NodeFileSystem } from "@effect/platform-node"
import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { AppFileSystem } from "@opencode-ai/shared/filesystem"
@@ -40,12 +43,39 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Npm") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Npm") {}
const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined
const npmPath = fileURLToPath(new URL("../..", import.meta.url))
export function sanitize(pkg: string) { export function sanitize(pkg: string) {
if (!illegal) return pkg if (!illegal) return pkg
return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("") 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 => { const resolveEntryPoint = (name: string, dir: string): EntryPoint => {
let entrypoint: Option.Option<string> let entrypoint: Option.Option<string>
try { try {
@@ -81,7 +111,10 @@ export const layer = Layer.effect(
Effect.gen(function* () { Effect.gen(function* () {
yield* flock.acquire(`npm-install:${input.dir}`) yield* flock.acquire(`npm-install:${input.dir}`)
const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist")) const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"))
const add = input.add ?? []
const npmOptions = yield* loadOptions(input.dir)
const arborist = new Arborist({ const arborist = new Arborist({
...npmOptions,
path: input.dir, path: input.dir,
binLinks: true, binLinks: true,
progress: false, progress: false,
@@ -91,14 +124,15 @@ export const layer = Layer.effect(
return yield* Effect.tryPromise({ return yield* Effect.tryPromise({
try: () => try: () =>
arborist.reify({ arborist.reify({
add: input?.add || [], ...npmOptions,
add,
save: true, save: true,
saveType: "prod", saveType: "prod",
}), }),
catch: (cause) => catch: (cause) =>
new InstallFailedError({ new InstallFailedError({
cause, cause,
add: input?.add, add,
dir: input.dir, dir: input.dir,
}), }),
}) as Effect.Effect<ArboristTree, InstallFailedError> }) as Effect.Effect<ArboristTree, InstallFailedError>

View File

@@ -1,7 +1,18 @@
import fs from "fs/promises"
import path from "path"
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { Npm } from "../src/npm" import { Npm } from "../src/npm"
import { tmpdir } from "./fixture/fixture"
const win = process.platform === "win32" 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", () => { describe("Npm.sanitize", () => {
test("keeps normal scoped package specs unchanged", () => { test("keeps normal scoped package specs unchanged", () => {
@@ -16,3 +27,29 @@ describe("Npm.sanitize", () => {
expect(Npm.sanitize(spec)).toBe(expected) 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()
})
})