diff --git a/packages/opencode/src/plugin/loader.ts b/packages/opencode/src/plugin/loader.ts index 0245d311e0eb..c3af01266410 100644 --- a/packages/opencode/src/plugin/loader.ts +++ b/packages/opencode/src/plugin/loader.ts @@ -11,164 +11,164 @@ import { import { ConfigPlugin } from "@/config/plugin" import { InstallationVersion } from "@/installation/version" -export namespace PluginLoader { - export type Plan = { - spec: string - options: ConfigPlugin.Options | undefined - deprecated: boolean - } - export type Resolved = Plan & { - source: PluginSource - target: string - entry: string - pkg?: PluginPackage - } - export type Missing = Plan & { - source: PluginSource - target: string - pkg?: PluginPackage - message: string - } - export type Loaded = Resolved & { - mod: Record - } +export type Plan = { + spec: string + options: ConfigPlugin.Options | undefined + deprecated: boolean +} +export type Resolved = Plan & { + source: PluginSource + target: string + entry: string + pkg?: PluginPackage +} +export type Missing = Plan & { + source: PluginSource + target: string + pkg?: PluginPackage + message: string +} +export type Loaded = Resolved & { + mod: Record +} - type Candidate = { origin: ConfigPlugin.Origin; plan: Plan } - type Report = { - start?: (candidate: Candidate, retry: boolean) => void - missing?: (candidate: Candidate, retry: boolean, message: string, resolved: Missing) => void - error?: ( - candidate: Candidate, - retry: boolean, - stage: "install" | "entry" | "compatibility" | "load", - error: unknown, - resolved?: Resolved, - ) => void - } +type Candidate = { origin: ConfigPlugin.Origin; plan: Plan } +type Report = { + start?: (candidate: Candidate, retry: boolean) => void + missing?: (candidate: Candidate, retry: boolean, message: string, resolved: Missing) => void + error?: ( + candidate: Candidate, + retry: boolean, + stage: "install" | "entry" | "compatibility" | "load", + error: unknown, + resolved?: Resolved, + ) => void +} - function plan(item: ConfigPlugin.Spec): Plan { - const spec = ConfigPlugin.pluginSpecifier(item) - return { spec, options: ConfigPlugin.pluginOptions(item), deprecated: isDeprecatedPlugin(spec) } +function plan(item: ConfigPlugin.Spec): Plan { + const spec = ConfigPlugin.pluginSpecifier(item) + return { spec, options: ConfigPlugin.pluginOptions(item), deprecated: isDeprecatedPlugin(spec) } +} + +export async function resolve( + plan: Plan, + kind: PluginKind, +): Promise< + | { ok: true; value: Resolved } + | { ok: false; stage: "missing"; value: Missing } + | { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown } +> { + let target = "" + try { + target = await resolvePluginTarget(plan.spec) + } catch (error) { + return { ok: false, stage: "install", error } } + if (!target) return { ok: false, stage: "install", error: new Error(`Plugin ${plan.spec} target is empty`) } - export async function resolve( - plan: Plan, - kind: PluginKind, - ): Promise< - | { ok: true; value: Resolved } - | { ok: false; stage: "missing"; value: Missing } - | { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown } - > { - let target = "" - try { - target = await resolvePluginTarget(plan.spec) - } catch (error) { - return { ok: false, stage: "install", error } + let base + try { + base = await createPluginEntry(plan.spec, target, kind) + } catch (error) { + return { ok: false, stage: "entry", error } + } + if (!base.entry) + return { + ok: false, + stage: "missing", + value: { + ...plan, + source: base.source, + target: base.target, + pkg: base.pkg, + message: `Plugin ${plan.spec} does not expose a ${kind} entrypoint`, + }, } - if (!target) return { ok: false, stage: "install", error: new Error(`Plugin ${plan.spec} target is empty`) } - let base + if (base.source === "npm") { try { - base = await createPluginEntry(plan.spec, target, kind) + await checkPluginCompatibility(base.target, InstallationVersion, base.pkg) } catch (error) { - return { ok: false, stage: "entry", error } - } - if (!base.entry) - return { - ok: false, - stage: "missing", - value: { - ...plan, - source: base.source, - target: base.target, - pkg: base.pkg, - message: `Plugin ${plan.spec} does not expose a ${kind} entrypoint`, - }, - } - - if (base.source === "npm") { - try { - await checkPluginCompatibility(base.target, InstallationVersion, base.pkg) - } catch (error) { - return { ok: false, stage: "compatibility", error } - } + return { ok: false, stage: "compatibility", error } } - return { ok: true, value: { ...plan, source: base.source, target: base.target, entry: base.entry, pkg: base.pkg } } } + return { ok: true, value: { ...plan, source: base.source, target: base.target, entry: base.entry, pkg: base.pkg } } +} - export async function load(row: Resolved): Promise<{ ok: true; value: Loaded } | { ok: false; error: unknown }> { - let mod - try { - mod = await import(row.entry) - } catch (error) { - return { ok: false, error } - } - if (!mod) return { ok: false, error: new Error(`Plugin ${row.spec} module is empty`) } - return { ok: true, value: { ...row, mod } } +export async function load(row: Resolved): Promise<{ ok: true; value: Loaded } | { ok: false; error: unknown }> { + let mod + try { + mod = await import(row.entry) + } catch (error) { + return { ok: false, error } } + if (!mod) return { ok: false, error: new Error(`Plugin ${row.spec} module is empty`) } + return { ok: true, value: { ...row, mod } } +} - async function attempt( - candidate: Candidate, - kind: PluginKind, - retry: boolean, - finish: ((load: Loaded, origin: ConfigPlugin.Origin, retry: boolean) => Promise) | undefined, - missing: ((value: Missing, origin: ConfigPlugin.Origin, retry: boolean) => Promise) | undefined, - report: Report | undefined, - ): Promise { - const plan = candidate.plan - if (plan.deprecated) return - report?.start?.(candidate, retry) - const resolved = await resolve(plan, kind) - if (!resolved.ok) { - if (resolved.stage === "missing") { - if (missing) { - const value = await missing(resolved.value, candidate.origin, retry) - if (value !== undefined) return value - } - report?.missing?.(candidate, retry, resolved.value.message, resolved.value) - return +async function attempt( + candidate: Candidate, + kind: PluginKind, + retry: boolean, + finish: ((load: Loaded, origin: ConfigPlugin.Origin, retry: boolean) => Promise) | undefined, + missing: ((value: Missing, origin: ConfigPlugin.Origin, retry: boolean) => Promise) | undefined, + report: Report | undefined, +): Promise { + const plan = candidate.plan + if (plan.deprecated) return + report?.start?.(candidate, retry) + const resolved = await resolve(plan, kind) + if (!resolved.ok) { + if (resolved.stage === "missing") { + if (missing) { + const value = await missing(resolved.value, candidate.origin, retry) + if (value !== undefined) return value } - report?.error?.(candidate, retry, resolved.stage, resolved.error) + report?.missing?.(candidate, retry, resolved.value.message, resolved.value) return } - const loaded = await load(resolved.value) - if (!loaded.ok) { - report?.error?.(candidate, retry, "load", loaded.error, resolved.value) - return - } - if (!finish) return loaded.value as R - return finish(loaded.value, candidate.origin, retry) + report?.error?.(candidate, retry, resolved.stage, resolved.error) + return } - - type Input = { - items: ConfigPlugin.Origin[] - kind: PluginKind - wait?: () => Promise - finish?: (load: Loaded, origin: ConfigPlugin.Origin, retry: boolean) => Promise - missing?: (value: Missing, origin: ConfigPlugin.Origin, retry: boolean) => Promise - report?: Report + const loaded = await load(resolved.value) + if (!loaded.ok) { + report?.error?.(candidate, retry, "load", loaded.error, resolved.value) + return } + if (!finish) return loaded.value as R + return finish(loaded.value, candidate.origin, retry) +} - export async function loadExternal(input: Input): Promise { - const candidates = input.items.map((origin) => ({ origin, plan: plan(origin.spec) })) - const list: Array> = [] - for (const candidate of candidates) { - list.push(attempt(candidate, input.kind, false, input.finish, input.missing, input.report)) - } - const out = await Promise.all(list) - if (input.wait) { - let deps: Promise | undefined - for (let i = 0; i < candidates.length; i++) { - if (out[i] !== undefined) continue - const candidate = candidates[i] - if (!candidate || pluginSource(candidate.plan.spec) !== "file") continue - deps ??= input.wait() - await deps - out[i] = await attempt(candidate, input.kind, true, input.finish, input.missing, input.report) - } +type Input = { + items: ConfigPlugin.Origin[] + kind: PluginKind + wait?: () => Promise + finish?: (load: Loaded, origin: ConfigPlugin.Origin, retry: boolean) => Promise + missing?: (value: Missing, origin: ConfigPlugin.Origin, retry: boolean) => Promise + report?: Report +} + +export async function loadExternal(input: Input): Promise { + const candidates = input.items.map((origin) => ({ origin, plan: plan(origin.spec) })) + const list: Array> = [] + for (const candidate of candidates) { + list.push(attempt(candidate, input.kind, false, input.finish, input.missing, input.report)) + } + const out = await Promise.all(list) + if (input.wait) { + let deps: Promise | undefined + for (let i = 0; i < candidates.length; i++) { + if (out[i] !== undefined) continue + const candidate = candidates[i] + if (!candidate || pluginSource(candidate.plan.spec) !== "file") continue + deps ??= input.wait() + await deps + out[i] = await attempt(candidate, input.kind, true, input.finish, input.missing, input.report) } - const ready: R[] = [] - for (const item of out) if (item !== undefined) ready.push(item) - return ready } + const ready: R[] = [] + for (const item of out) if (item !== undefined) ready.push(item) + return ready } + +export * as PluginLoader from "./loader"