Explorar el Código

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

Shoubhit Dash hace 4 meses
padre
commit
38deb0f3ee
Se han modificado 2 ficheros con 73 adiciones y 2 borrados
  1. 36 2
      packages/opencode/src/npm/index.ts
  2. 37 0
      packages/opencode/test/npm.test.ts

+ 36 - 2
packages/opencode/src/npm/index.ts

@@ -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>

+ 37 - 0
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 { 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()
+  })
+})