فهرست منبع

feat: unwrap unpm namespace to flat exports + barrel (#22708)

Kit Langton 4 روز پیش
والد
کامیت
d22b5f026d
2فایلهای تغییر یافته به همراه187 افزوده شده و 188 حذف شده
  1. 1 188
      packages/opencode/src/npm/index.ts
  2. 186 0
      packages/opencode/src/npm/npm.ts

+ 1 - 188
packages/opencode/src/npm/index.ts

@@ -1,188 +1 @@
-import semver from "semver"
-import z from "zod"
-import { NamedError } from "@opencode-ai/shared/util/error"
-import { Global } from "../global"
-import { Log } from "../util/log"
-import path from "path"
-import { readdir, rm } from "fs/promises"
-import { Filesystem } from "@/util/filesystem"
-import { Flock } from "@opencode-ai/shared/util/flock"
-import { Arborist } from "@npmcli/arborist"
-
-export namespace Npm {
-  const log = Log.create({ service: "npm" })
-  const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined
-
-  export const InstallFailedError = NamedError.create(
-    "NpmInstallFailedError",
-    z.object({
-      pkg: z.string(),
-    }),
-  )
-
-  export function sanitize(pkg: string) {
-    if (!illegal) return pkg
-    return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("")
-  }
-
-  function directory(pkg: string) {
-    return path.join(Global.Path.cache, "packages", sanitize(pkg))
-  }
-
-  function resolveEntryPoint(name: string, dir: string) {
-    let entrypoint: string | undefined
-    try {
-      entrypoint = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
-    } catch {}
-    const result = {
-      directory: dir,
-      entrypoint,
-    }
-    return result
-  }
-
-  export async function outdated(pkg: string, cachedVersion: string): Promise<boolean> {
-    const response = await fetch(`https://registry.npmjs.org/${pkg}`)
-    if (!response.ok) {
-      log.warn("Failed to resolve latest version, using cached", { pkg, cachedVersion })
-      return false
-    }
-
-    const data = (await response.json()) as { "dist-tags"?: { latest?: string } }
-    const latestVersion = data?.["dist-tags"]?.latest
-    if (!latestVersion) {
-      log.warn("No latest version found, using cached", { pkg, cachedVersion })
-      return false
-    }
-
-    const range = /[\s^~*xX<>|=]/.test(cachedVersion)
-    if (range) return !semver.satisfies(latestVersion, cachedVersion)
-
-    return semver.lt(cachedVersion, latestVersion)
-  }
-
-  export async function add(pkg: string) {
-    const dir = directory(pkg)
-    await using _ = await Flock.acquire(`npm-install:${Filesystem.resolve(dir)}`)
-    log.info("installing package", {
-      pkg,
-    })
-
-    const arborist = new Arborist({
-      path: dir,
-      binLinks: true,
-      progress: false,
-      savePrefix: "",
-      ignoreScripts: true,
-    })
-    const tree = await arborist.loadVirtual().catch(() => {})
-    if (tree) {
-      const first = tree.edgesOut.values().next().value?.to
-      if (first) {
-        return resolveEntryPoint(first.name, first.path)
-      }
-    }
-
-    const result = await arborist
-      .reify({
-        add: [pkg],
-        save: true,
-        saveType: "prod",
-      })
-      .catch((cause) => {
-        throw new InstallFailedError(
-          { pkg },
-          {
-            cause,
-          },
-        )
-      })
-
-    const first = result.edgesOut.values().next().value?.to
-    if (!first) throw new InstallFailedError({ pkg })
-    return resolveEntryPoint(first.name, first.path)
-  }
-
-  export async function install(dir: string) {
-    await using _ = await Flock.acquire(`npm-install:${dir}`)
-    log.info("checking dependencies", { dir })
-
-    const reify = async () => {
-      const arb = new Arborist({
-        path: dir,
-        binLinks: true,
-        progress: false,
-        savePrefix: "",
-        ignoreScripts: true,
-      })
-      await arb.reify().catch(() => {})
-    }
-
-    if (!(await Filesystem.exists(path.join(dir, "node_modules")))) {
-      log.info("node_modules missing, reifying")
-      await reify()
-      return
-    }
-
-    const pkg = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({}))
-    const lock = await Filesystem.readJson(path.join(dir, "package-lock.json")).catch(() => ({}))
-
-    const declared = new Set([
-      ...Object.keys(pkg.dependencies || {}),
-      ...Object.keys(pkg.devDependencies || {}),
-      ...Object.keys(pkg.peerDependencies || {}),
-      ...Object.keys(pkg.optionalDependencies || {}),
-    ])
-
-    const root = lock.packages?.[""] || {}
-    const locked = new Set([
-      ...Object.keys(root.dependencies || {}),
-      ...Object.keys(root.devDependencies || {}),
-      ...Object.keys(root.peerDependencies || {}),
-      ...Object.keys(root.optionalDependencies || {}),
-    ])
-
-    for (const name of declared) {
-      if (!locked.has(name)) {
-        log.info("dependency not in lock file, reifying", { name })
-        await reify()
-        return
-      }
-    }
-
-    log.info("dependencies in sync")
-  }
-
-  export async function which(pkg: string) {
-    const dir = directory(pkg)
-    const binDir = path.join(dir, "node_modules", ".bin")
-
-    const pick = async () => {
-      const files = await readdir(binDir).catch(() => [])
-      if (files.length === 0) return undefined
-      if (files.length === 1) return files[0]
-      // Multiple binaries — resolve from package.json bin field like npx does
-      const pkgJson = await Filesystem.readJson<{ bin?: string | Record<string, string> }>(
-        path.join(dir, "node_modules", pkg, "package.json"),
-      ).catch(() => undefined)
-      if (pkgJson?.bin) {
-        const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg
-        const bin = pkgJson.bin
-        if (typeof bin === "string") return unscoped
-        const keys = Object.keys(bin)
-        if (keys.length === 1) return keys[0]
-        return bin[unscoped] ? unscoped : keys[0]
-      }
-      return files[0]
-    }
-
-    const bin = await pick()
-    if (bin) return path.join(binDir, bin)
-
-    await rm(path.join(dir, "package-lock.json"), { force: true })
-    await add(pkg)
-    const resolved = await pick()
-    if (!resolved) return
-    return path.join(binDir, resolved)
-  }
-}
+export * as Npm from "./npm"

+ 186 - 0
packages/opencode/src/npm/npm.ts

@@ -0,0 +1,186 @@
+import semver from "semver"
+import z from "zod"
+import { NamedError } from "@opencode-ai/shared/util/error"
+import { Global } from "../global"
+import { Log } from "../util/log"
+import path from "path"
+import { readdir, rm } from "fs/promises"
+import { Filesystem } from "@/util/filesystem"
+import { Flock } from "@opencode-ai/shared/util/flock"
+import { Arborist } from "@npmcli/arborist"
+
+const log = Log.create({ service: "npm" })
+const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined
+
+export const InstallFailedError = NamedError.create(
+  "NpmInstallFailedError",
+  z.object({
+    pkg: z.string(),
+  }),
+)
+
+export function sanitize(pkg: string) {
+  if (!illegal) return pkg
+  return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("")
+}
+
+function directory(pkg: string) {
+  return path.join(Global.Path.cache, "packages", sanitize(pkg))
+}
+
+function resolveEntryPoint(name: string, dir: string) {
+  let entrypoint: string | undefined
+  try {
+    entrypoint = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
+  } catch {}
+  const result = {
+    directory: dir,
+    entrypoint,
+  }
+  return result
+}
+
+export async function outdated(pkg: string, cachedVersion: string): Promise<boolean> {
+  const response = await fetch(`https://registry.npmjs.org/${pkg}`)
+  if (!response.ok) {
+    log.warn("Failed to resolve latest version, using cached", { pkg, cachedVersion })
+    return false
+  }
+
+  const data = (await response.json()) as { "dist-tags"?: { latest?: string } }
+  const latestVersion = data?.["dist-tags"]?.latest
+  if (!latestVersion) {
+    log.warn("No latest version found, using cached", { pkg, cachedVersion })
+    return false
+  }
+
+  const range = /[\s^~*xX<>|=]/.test(cachedVersion)
+  if (range) return !semver.satisfies(latestVersion, cachedVersion)
+
+  return semver.lt(cachedVersion, latestVersion)
+}
+
+export async function add(pkg: string) {
+  const dir = directory(pkg)
+  await using _ = await Flock.acquire(`npm-install:${Filesystem.resolve(dir)}`)
+  log.info("installing package", {
+    pkg,
+  })
+
+  const arborist = new Arborist({
+    path: dir,
+    binLinks: true,
+    progress: false,
+    savePrefix: "",
+    ignoreScripts: true,
+  })
+  const tree = await arborist.loadVirtual().catch(() => {})
+  if (tree) {
+    const first = tree.edgesOut.values().next().value?.to
+    if (first) {
+      return resolveEntryPoint(first.name, first.path)
+    }
+  }
+
+  const result = await arborist
+    .reify({
+      add: [pkg],
+      save: true,
+      saveType: "prod",
+    })
+    .catch((cause) => {
+      throw new InstallFailedError(
+        { pkg },
+        {
+          cause,
+        },
+      )
+    })
+
+  const first = result.edgesOut.values().next().value?.to
+  if (!first) throw new InstallFailedError({ pkg })
+  return resolveEntryPoint(first.name, first.path)
+}
+
+export async function install(dir: string) {
+  await using _ = await Flock.acquire(`npm-install:${dir}`)
+  log.info("checking dependencies", { dir })
+
+  const reify = async () => {
+    const arb = new Arborist({
+      path: dir,
+      binLinks: true,
+      progress: false,
+      savePrefix: "",
+      ignoreScripts: true,
+    })
+    await arb.reify().catch(() => {})
+  }
+
+  if (!(await Filesystem.exists(path.join(dir, "node_modules")))) {
+    log.info("node_modules missing, reifying")
+    await reify()
+    return
+  }
+
+  const pkg = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({}))
+  const lock = await Filesystem.readJson(path.join(dir, "package-lock.json")).catch(() => ({}))
+
+  const declared = new Set([
+    ...Object.keys(pkg.dependencies || {}),
+    ...Object.keys(pkg.devDependencies || {}),
+    ...Object.keys(pkg.peerDependencies || {}),
+    ...Object.keys(pkg.optionalDependencies || {}),
+  ])
+
+  const root = lock.packages?.[""] || {}
+  const locked = new Set([
+    ...Object.keys(root.dependencies || {}),
+    ...Object.keys(root.devDependencies || {}),
+    ...Object.keys(root.peerDependencies || {}),
+    ...Object.keys(root.optionalDependencies || {}),
+  ])
+
+  for (const name of declared) {
+    if (!locked.has(name)) {
+      log.info("dependency not in lock file, reifying", { name })
+      await reify()
+      return
+    }
+  }
+
+  log.info("dependencies in sync")
+}
+
+export async function which(pkg: string) {
+  const dir = directory(pkg)
+  const binDir = path.join(dir, "node_modules", ".bin")
+
+  const pick = async () => {
+    const files = await readdir(binDir).catch(() => [])
+    if (files.length === 0) return undefined
+    if (files.length === 1) return files[0]
+    // Multiple binaries — resolve from package.json bin field like npx does
+    const pkgJson = await Filesystem.readJson<{ bin?: string | Record<string, string> }>(
+      path.join(dir, "node_modules", pkg, "package.json"),
+    ).catch(() => undefined)
+    if (pkgJson?.bin) {
+      const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg
+      const bin = pkgJson.bin
+      if (typeof bin === "string") return unscoped
+      const keys = Object.keys(bin)
+      if (keys.length === 1) return keys[0]
+      return bin[unscoped] ? unscoped : keys[0]
+    }
+    return files[0]
+  }
+
+  const bin = await pick()
+  if (bin) return path.join(binDir, bin)
+
+  await rm(path.join(dir, "package-lock.json"), { force: true })
+  await add(pkg)
+  const resolved = await pick()
+  if (!resolved) return
+  return path.join(binDir, resolved)
+}