Skip to content

Commit b708e84

Browse files
thdxropencode
authored andcommitted
docs(opencode): annotate plugin loader flow (#23160)
1 parent 9b6c397 commit b708e84

1 file changed

Lines changed: 44 additions & 2 deletions

File tree

packages/opencode/src/plugin/loader.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,41 @@ import { ConfigPlugin } from "@/config/plugin"
1212
import { InstallationVersion } from "@/installation/version"
1313

1414
export namespace PluginLoader {
15+
// A normalized plugin declaration derived from config before any filesystem or npm work happens.
1516
export type Plan = {
1617
spec: string
1718
options: ConfigPlugin.Options | undefined
1819
deprecated: boolean
1920
}
21+
22+
// A plugin that has been resolved to a concrete target and entrypoint on disk.
2023
export type Resolved = Plan & {
2124
source: PluginSource
2225
target: string
2326
entry: string
2427
pkg?: PluginPackage
2528
}
29+
30+
// A plugin target we could inspect, but which does not expose the requested kind of entrypoint.
2631
export type Missing = Plan & {
2732
source: PluginSource
2833
target: string
2934
pkg?: PluginPackage
3035
message: string
3136
}
37+
38+
// A resolved plugin whose module has been imported successfully.
3239
export type Loaded = Resolved & {
3340
mod: Record<string, unknown>
3441
}
3542

3643
type Candidate = { origin: ConfigPlugin.Origin; plan: Plan }
3744
type Report = {
45+
// Called before each attempt so callers can log initial load attempts and retries uniformly.
3846
start?: (candidate: Candidate, retry: boolean) => void
47+
// Called when the package exists but does not provide the requested entrypoint.
3948
missing?: (candidate: Candidate, retry: boolean, message: string, resolved: Missing) => void
49+
// Called for operational failures such as install, compatibility, or dynamic import errors.
4050
error?: (
4151
candidate: Candidate,
4252
retry: boolean,
@@ -46,19 +56,25 @@ export namespace PluginLoader {
4656
) => void
4757
}
4858

59+
// Normalize a config item into the loader's internal representation.
4960
function plan(item: ConfigPlugin.Spec): Plan {
5061
const spec = ConfigPlugin.pluginSpecifier(item)
5162
return { spec, options: ConfigPlugin.pluginOptions(item), deprecated: isDeprecatedPlugin(spec) }
5263
}
5364

65+
// Resolve a configured plugin into a concrete entrypoint that can later be imported.
66+
//
67+
// The stages here intentionally separate install/target resolution, entrypoint detection,
68+
// and compatibility checks so callers can report the exact reason a plugin was skipped.
5469
export async function resolve(
5570
plan: Plan,
5671
kind: PluginKind,
5772
): Promise<
5873
| { ok: true; value: Resolved }
59-
| { ok: false; stage: "missing"; value: Missing }
60-
| { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown }
74+
| { ok: false; stage: "missing"; value: Missing }
75+
| { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown }
6176
> {
77+
// First make sure the plugin exists locally, installing npm plugins on demand.
6278
let target = ""
6379
try {
6480
target = await resolvePluginTarget(plan.spec)
@@ -67,6 +83,7 @@ export namespace PluginLoader {
6783
}
6884
if (!target) return { ok: false, stage: "install", error: new Error(`Plugin ${plan.spec} target is empty`) }
6985

86+
// Then inspect the target for the requested server/tui entrypoint.
7087
let base
7188
try {
7289
base = await createPluginEntry(plan.spec, target, kind)
@@ -86,6 +103,8 @@ export namespace PluginLoader {
86103
},
87104
}
88105

106+
// npm plugins can declare which opencode versions they support; file plugins are treated
107+
// as local development code and skip this compatibility gate.
89108
if (base.source === "npm") {
90109
try {
91110
await checkPluginCompatibility(base.target, InstallationVersion, base.pkg)
@@ -96,6 +115,7 @@ export namespace PluginLoader {
96115
return { ok: true, value: { ...plan, source: base.source, target: base.target, entry: base.entry, pkg: base.pkg } }
97116
}
98117

118+
// Import the resolved module only after all earlier validation has succeeded.
99119
export async function load(row: Resolved): Promise<{ ok: true; value: Loaded } | { ok: false; error: unknown }> {
100120
let mod
101121
try {
@@ -107,6 +127,8 @@ export namespace PluginLoader {
107127
return { ok: true, value: { ...row, mod } }
108128
}
109129

130+
// Run one candidate through the full pipeline: resolve, optionally surface a missing entry,
131+
// import the module, and finally let the caller transform the loaded plugin into any result type.
110132
async function attempt<R>(
111133
candidate: Candidate,
112134
kind: PluginKind,
@@ -116,11 +138,17 @@ export namespace PluginLoader {
116138
report: Report | undefined,
117139
): Promise<R | undefined> {
118140
const plan = candidate.plan
141+
142+
// Deprecated plugin packages are silently ignored because they are now built in.
119143
if (plan.deprecated) return
144+
120145
report?.start?.(candidate, retry)
146+
121147
const resolved = await resolve(plan, kind)
122148
if (!resolved.ok) {
123149
if (resolved.stage === "missing") {
150+
// Missing entrypoints are handled separately so callers can still inspect package metadata,
151+
// for example to load theme files from a tui plugin package that has no code entrypoint.
124152
if (missing) {
125153
const value = await missing(resolved.value, candidate.origin, retry)
126154
if (value !== undefined) return value
@@ -131,11 +159,15 @@ export namespace PluginLoader {
131159
report?.error?.(candidate, retry, resolved.stage, resolved.error)
132160
return
133161
}
162+
134163
const loaded = await load(resolved.value)
135164
if (!loaded.ok) {
136165
report?.error?.(candidate, retry, "load", loaded.error, resolved.value)
137166
return
138167
}
168+
169+
// The default behavior is to return the successfully loaded plugin as-is, but callers can
170+
// provide a finisher to adapt the result into a more specific runtime shape.
139171
if (!finish) return loaded.value as R
140172
return finish(loaded.value, candidate.origin, retry)
141173
}
@@ -149,6 +181,11 @@ export namespace PluginLoader {
149181
report?: Report
150182
}
151183

184+
// Resolve and load all configured plugins in parallel.
185+
//
186+
// If `wait` is provided, file-based plugins that initially failed are retried once after the
187+
// caller finishes preparing dependencies. This supports local plugins that depend on an install
188+
// step happening elsewhere before their entrypoint becomes loadable.
152189
export async function loadExternal<R = Loaded>(input: Input<R>): Promise<R[]> {
153190
const candidates = input.items.map((origin) => ({ origin, plan: plan(origin.spec) }))
154191
const list: Array<Promise<R | undefined>> = []
@@ -160,13 +197,18 @@ export namespace PluginLoader {
160197
let deps: Promise<void> | undefined
161198
for (let i = 0; i < candidates.length; i++) {
162199
if (out[i] !== undefined) continue
200+
201+
// Only local file plugins are retried. npm plugins already attempted installation during
202+
// the first pass, while file plugins may need the caller's dependency preparation to finish.
163203
const candidate = candidates[i]
164204
if (!candidate || pluginSource(candidate.plan.spec) !== "file") continue
165205
deps ??= input.wait()
166206
await deps
167207
out[i] = await attempt(candidate, input.kind, true, input.finish, input.missing, input.report)
168208
}
169209
}
210+
211+
// Drop skipped/failed entries while preserving the successful result order.
170212
const ready: R[] = []
171213
for (const item of out) if (item !== undefined) ready.push(item)
172214
return ready

0 commit comments

Comments
 (0)