mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-04-21 13:21:17 +08:00
refactor: unwrap ConfigPlugin namespace to flat exports + self-reexport (#22876)
This commit is contained in:
@@ -4,81 +4,81 @@ import { pathToFileURL } from "url"
|
||||
import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared"
|
||||
import path from "path"
|
||||
|
||||
export namespace ConfigPlugin {
|
||||
const Options = z.record(z.string(), z.unknown())
|
||||
export type Options = z.infer<typeof Options>
|
||||
const Options = z.record(z.string(), z.unknown())
|
||||
export type Options = z.infer<typeof Options>
|
||||
|
||||
// Spec is the user-config value: either just a plugin identifier, or the identifier plus inline options.
|
||||
// It answers "what should we load?" but says nothing about where that value came from.
|
||||
export const Spec = z.union([z.string(), z.tuple([z.string(), Options])])
|
||||
export type Spec = z.infer<typeof Spec>
|
||||
// Spec is the user-config value: either just a plugin identifier, or the identifier plus inline options.
|
||||
// It answers "what should we load?" but says nothing about where that value came from.
|
||||
export const Spec = z.union([z.string(), z.tuple([z.string(), Options])])
|
||||
export type Spec = z.infer<typeof Spec>
|
||||
|
||||
export type Scope = "global" | "local"
|
||||
export type Scope = "global" | "local"
|
||||
|
||||
// Origin keeps the original config provenance attached to a spec.
|
||||
// After multiple config files are merged, callers still need to know which file declared the plugin
|
||||
// and whether it should behave like a global or project-local plugin.
|
||||
export type Origin = {
|
||||
spec: Spec
|
||||
source: string
|
||||
scope: Scope
|
||||
}
|
||||
|
||||
export async function load(dir: string) {
|
||||
const plugins: ConfigPlugin.Spec[] = []
|
||||
|
||||
for (const item of await Glob.scan("{plugin,plugins}/*.{ts,js}", {
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
dot: true,
|
||||
symlink: true,
|
||||
})) {
|
||||
plugins.push(pathToFileURL(item).href)
|
||||
}
|
||||
return plugins
|
||||
}
|
||||
|
||||
export function pluginSpecifier(plugin: Spec): string {
|
||||
return Array.isArray(plugin) ? plugin[0] : plugin
|
||||
}
|
||||
|
||||
export function pluginOptions(plugin: Spec): Options | undefined {
|
||||
return Array.isArray(plugin) ? plugin[1] : undefined
|
||||
}
|
||||
|
||||
// Path-like specs are resolved relative to the config file that declared them so merges later on do not
|
||||
// accidentally reinterpret `./plugin.ts` relative to some other directory.
|
||||
export async function resolvePluginSpec(plugin: Spec, configFilepath: string): Promise<Spec> {
|
||||
const spec = pluginSpecifier(plugin)
|
||||
if (!isPathPluginSpec(spec)) return plugin
|
||||
|
||||
const base = path.dirname(configFilepath)
|
||||
const file = (() => {
|
||||
if (spec.startsWith("file://")) return spec
|
||||
if (path.isAbsolute(spec) || /^[A-Za-z]:[\\/]/.test(spec)) return pathToFileURL(spec).href
|
||||
return pathToFileURL(path.resolve(base, spec)).href
|
||||
})()
|
||||
|
||||
const resolved = await resolvePathPluginTarget(file).catch(() => file)
|
||||
|
||||
if (Array.isArray(plugin)) return [resolved, plugin[1]]
|
||||
return resolved
|
||||
}
|
||||
|
||||
// Dedupe on the load identity (package name for npm specs, exact file URL for local specs), but keep the
|
||||
// full Origin so downstream code still knows which config file won and where follow-up writes should go.
|
||||
export function deduplicatePluginOrigins(plugins: Origin[]): Origin[] {
|
||||
const seen = new Set<string>()
|
||||
const list: Origin[] = []
|
||||
|
||||
for (const plugin of plugins.toReversed()) {
|
||||
const spec = pluginSpecifier(plugin.spec)
|
||||
const name = spec.startsWith("file://") ? spec : parsePluginSpecifier(spec).pkg
|
||||
if (seen.has(name)) continue
|
||||
seen.add(name)
|
||||
list.push(plugin)
|
||||
}
|
||||
|
||||
return list.toReversed()
|
||||
}
|
||||
// Origin keeps the original config provenance attached to a spec.
|
||||
// After multiple config files are merged, callers still need to know which file declared the plugin
|
||||
// and whether it should behave like a global or project-local plugin.
|
||||
export type Origin = {
|
||||
spec: Spec
|
||||
source: string
|
||||
scope: Scope
|
||||
}
|
||||
|
||||
export async function load(dir: string) {
|
||||
const plugins: Spec[] = []
|
||||
|
||||
for (const item of await Glob.scan("{plugin,plugins}/*.{ts,js}", {
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
dot: true,
|
||||
symlink: true,
|
||||
})) {
|
||||
plugins.push(pathToFileURL(item).href)
|
||||
}
|
||||
return plugins
|
||||
}
|
||||
|
||||
export function pluginSpecifier(plugin: Spec): string {
|
||||
return Array.isArray(plugin) ? plugin[0] : plugin
|
||||
}
|
||||
|
||||
export function pluginOptions(plugin: Spec): Options | undefined {
|
||||
return Array.isArray(plugin) ? plugin[1] : undefined
|
||||
}
|
||||
|
||||
// Path-like specs are resolved relative to the config file that declared them so merges later on do not
|
||||
// accidentally reinterpret `./plugin.ts` relative to some other directory.
|
||||
export async function resolvePluginSpec(plugin: Spec, configFilepath: string): Promise<Spec> {
|
||||
const spec = pluginSpecifier(plugin)
|
||||
if (!isPathPluginSpec(spec)) return plugin
|
||||
|
||||
const base = path.dirname(configFilepath)
|
||||
const file = (() => {
|
||||
if (spec.startsWith("file://")) return spec
|
||||
if (path.isAbsolute(spec) || /^[A-Za-z]:[\\/]/.test(spec)) return pathToFileURL(spec).href
|
||||
return pathToFileURL(path.resolve(base, spec)).href
|
||||
})()
|
||||
|
||||
const resolved = await resolvePathPluginTarget(file).catch(() => file)
|
||||
|
||||
if (Array.isArray(plugin)) return [resolved, plugin[1]]
|
||||
return resolved
|
||||
}
|
||||
|
||||
// Dedupe on the load identity (package name for npm specs, exact file URL for local specs), but keep the
|
||||
// full Origin so downstream code still knows which config file won and where follow-up writes should go.
|
||||
export function deduplicatePluginOrigins(plugins: Origin[]): Origin[] {
|
||||
const seen = new Set<string>()
|
||||
const list: Origin[] = []
|
||||
|
||||
for (const plugin of plugins.toReversed()) {
|
||||
const spec = pluginSpecifier(plugin.spec)
|
||||
const name = spec.startsWith("file://") ? spec : parsePluginSpecifier(spec).pkg
|
||||
if (seen.has(name)) continue
|
||||
seen.add(name)
|
||||
list.push(plugin)
|
||||
}
|
||||
|
||||
return list.toReversed()
|
||||
}
|
||||
|
||||
export * as ConfigPlugin from "./plugin"
|
||||
|
||||
Reference in New Issue
Block a user