mirror of
https://github.com/anomalyco/opencode.git
synced 2026-06-01 19:05:38 +00:00
Refactor npm config handling (#24565)
This commit is contained in:
@@ -1,22 +1,14 @@
|
||||
export * as Npm from "./npm"
|
||||
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import npa from "npm-package-arg"
|
||||
import semver from "semver"
|
||||
// @ts-expect-error npm does not publish types for this internal config API.
|
||||
import Config from "@npmcli/config"
|
||||
// @ts-expect-error npm does not publish types for this internal config API.
|
||||
import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js"
|
||||
import { Effect, Schema, Context, Layer, Option, FileSystem, Stream } from "effect"
|
||||
import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
import { Global } from "./global"
|
||||
import { EffectFlock } from "./util/effect-flock"
|
||||
import { makeRuntime } from "./effect/runtime"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
|
||||
import { CrossSpawnSpawner } from "./cross-spawn-spawner"
|
||||
import { NpmConfig } from "./npm-config"
|
||||
|
||||
export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", {
|
||||
add: Schema.Array(Schema.String).pipe(Schema.optional),
|
||||
@@ -40,46 +32,18 @@ export interface Interface {
|
||||
}[]
|
||||
},
|
||||
) => Effect.Effect<void, EffectFlock.LockError | InstallFailedError>
|
||||
readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect<boolean>
|
||||
readonly which: (pkg: string, bin?: string) => Effect.Effect<Option.Option<string>>
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -110,39 +74,13 @@ export const layer = Layer.effect(
|
||||
const global = yield* Global.Service
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const flock = yield* EffectFlock.Service
|
||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||
const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg))
|
||||
const runView = Effect.fnUntraced(function* (cmd: string[]) {
|
||||
const handle = yield* spawner.spawn(
|
||||
ChildProcess.make(cmd[0], cmd.slice(1), {
|
||||
extendEnv: true,
|
||||
}),
|
||||
)
|
||||
const [stdout, stderr] = yield* Effect.all(
|
||||
[Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
|
||||
{ concurrency: 2 },
|
||||
)
|
||||
const code = yield* handle.exitCode
|
||||
if (code !== 0 || !stdout.trim()) {
|
||||
return yield* Effect.fail(stderr || stdout || `Failed to run ${cmd.join(" ")}`)
|
||||
}
|
||||
return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.String))(stdout)
|
||||
}, Effect.scoped)
|
||||
const viewLatestVersion = Effect.fnUntraced(function* (pkg: string) {
|
||||
return yield* runView(["npm", "view", pkg, "dist-tags.latest", "--json"]).pipe(
|
||||
Effect.catch(() =>
|
||||
runView(["pnpm", "view", pkg, "dist-tags.latest", "--json"]).pipe(
|
||||
Effect.catch(() => runView(["bun", "pm", "view", pkg, "dist-tags.latest", "--json"])),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
const reify = (input: { dir: string; add?: string[] }) =>
|
||||
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 npmOptions = yield* NpmConfig.load(input.dir)
|
||||
const arborist = new Arborist({
|
||||
...npmOptions,
|
||||
path: input.dir,
|
||||
@@ -172,18 +110,6 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
const outdated = Effect.fn("Npm.outdated")(function* (pkg: string, cachedVersion: string) {
|
||||
const latestVersion = yield* viewLatestVersion(pkg).pipe(Effect.option)
|
||||
if (Option.isNone(latestVersion)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const range = /[\s^~*xX<>|=]/.test(cachedVersion)
|
||||
if (range) return !semver.satisfies(latestVersion.value, cachedVersion)
|
||||
|
||||
return semver.lt(cachedVersion, latestVersion.value)
|
||||
})
|
||||
|
||||
const add = Effect.fn("Npm.add")(function* (pkg: string) {
|
||||
const dir = directory(pkg)
|
||||
const name = (() => {
|
||||
@@ -309,7 +235,6 @@ export const layer = Layer.effect(
|
||||
return Service.of({
|
||||
add,
|
||||
install,
|
||||
outdated,
|
||||
which,
|
||||
})
|
||||
}),
|
||||
@@ -320,7 +245,6 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(AppFileSystem.layer),
|
||||
Layer.provide(Global.layer),
|
||||
Layer.provide(NodeFileSystem.layer),
|
||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||
)
|
||||
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
@@ -337,10 +261,6 @@ export async function add(...args: Parameters<Interface["add"]>) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function outdated(...args: Parameters<Interface["outdated"]>) {
|
||||
return runPromise((svc) => svc.outdated(...args))
|
||||
}
|
||||
|
||||
export async function which(...args: Parameters<Interface["which"]>) {
|
||||
const resolved = await runPromise((svc) => svc.which(...args))
|
||||
return Option.getOrUndefined(resolved)
|
||||
|
||||
Reference in New Issue
Block a user