Kaynağa Gözat

docs(opencode): annotate plugin loader flow (#23160)

Dax 3 ay önce
ebeveyn
işleme
b708e8431e
1 değiştirilmiş dosya ile 44 ekleme ve 2 silme
  1. 44 2
      packages/opencode/src/plugin/loader.ts

+ 44 - 2
packages/opencode/src/plugin/loader.ts

@@ -12,31 +12,41 @@ import { ConfigPlugin } from "@/config/plugin"
 import { InstallationVersion } from "@/installation/version"
 
 export namespace PluginLoader {
+  // A normalized plugin declaration derived from config before any filesystem or npm work happens.
   export type Plan = {
     spec: string
     options: ConfigPlugin.Options | undefined
     deprecated: boolean
   }
+
+  // A plugin that has been resolved to a concrete target and entrypoint on disk.
   export type Resolved = Plan & {
     source: PluginSource
     target: string
     entry: string
     pkg?: PluginPackage
   }
+
+  // A plugin target we could inspect, but which does not expose the requested kind of entrypoint.
   export type Missing = Plan & {
     source: PluginSource
     target: string
     pkg?: PluginPackage
     message: string
   }
+
+  // A resolved plugin whose module has been imported successfully.
   export type Loaded = Resolved & {
     mod: Record<string, unknown>
   }
 
   type Candidate = { origin: ConfigPlugin.Origin; plan: Plan }
   type Report = {
+    // Called before each attempt so callers can log initial load attempts and retries uniformly.
     start?: (candidate: Candidate, retry: boolean) => void
+    // Called when the package exists but does not provide the requested entrypoint.
     missing?: (candidate: Candidate, retry: boolean, message: string, resolved: Missing) => void
+    // Called for operational failures such as install, compatibility, or dynamic import errors.
     error?: (
       candidate: Candidate,
       retry: boolean,
@@ -46,19 +56,25 @@ export namespace PluginLoader {
     ) => void
   }
 
+  // Normalize a config item into the loader's internal representation.
   function plan(item: ConfigPlugin.Spec): Plan {
     const spec = ConfigPlugin.pluginSpecifier(item)
     return { spec, options: ConfigPlugin.pluginOptions(item), deprecated: isDeprecatedPlugin(spec) }
   }
 
+  // Resolve a configured plugin into a concrete entrypoint that can later be imported.
+  //
+  // The stages here intentionally separate install/target resolution, entrypoint detection,
+  // and compatibility checks so callers can report the exact reason a plugin was skipped.
   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 }
+      | { ok: false; stage: "missing"; value: Missing }
+      | { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown }
   > {
+    // First make sure the plugin exists locally, installing npm plugins on demand.
     let target = ""
     try {
       target = await resolvePluginTarget(plan.spec)
@@ -67,6 +83,7 @@ export namespace PluginLoader {
     }
     if (!target) return { ok: false, stage: "install", error: new Error(`Plugin ${plan.spec} target is empty`) }
 
+    // Then inspect the target for the requested server/tui entrypoint.
     let base
     try {
       base = await createPluginEntry(plan.spec, target, kind)
@@ -86,6 +103,8 @@ export namespace PluginLoader {
         },
       }
 
+    // npm plugins can declare which opencode versions they support; file plugins are treated
+    // as local development code and skip this compatibility gate.
     if (base.source === "npm") {
       try {
         await checkPluginCompatibility(base.target, InstallationVersion, base.pkg)
@@ -96,6 +115,7 @@ export namespace PluginLoader {
     return { ok: true, value: { ...plan, source: base.source, target: base.target, entry: base.entry, pkg: base.pkg } }
   }
 
+  // Import the resolved module only after all earlier validation has succeeded.
   export async function load(row: Resolved): Promise<{ ok: true; value: Loaded } | { ok: false; error: unknown }> {
     let mod
     try {
@@ -107,6 +127,8 @@ export namespace PluginLoader {
     return { ok: true, value: { ...row, mod } }
   }
 
+  // Run one candidate through the full pipeline: resolve, optionally surface a missing entry,
+  // import the module, and finally let the caller transform the loaded plugin into any result type.
   async function attempt<R>(
     candidate: Candidate,
     kind: PluginKind,
@@ -116,11 +138,17 @@ export namespace PluginLoader {
     report: Report | undefined,
   ): Promise<R | undefined> {
     const plan = candidate.plan
+
+    // Deprecated plugin packages are silently ignored because they are now built in.
     if (plan.deprecated) return
+
     report?.start?.(candidate, retry)
+
     const resolved = await resolve(plan, kind)
     if (!resolved.ok) {
       if (resolved.stage === "missing") {
+        // Missing entrypoints are handled separately so callers can still inspect package metadata,
+        // for example to load theme files from a tui plugin package that has no code entrypoint.
         if (missing) {
           const value = await missing(resolved.value, candidate.origin, retry)
           if (value !== undefined) return value
@@ -131,11 +159,15 @@ export namespace PluginLoader {
       report?.error?.(candidate, retry, resolved.stage, resolved.error)
       return
     }
+
     const loaded = await load(resolved.value)
     if (!loaded.ok) {
       report?.error?.(candidate, retry, "load", loaded.error, resolved.value)
       return
     }
+
+    // The default behavior is to return the successfully loaded plugin as-is, but callers can
+    // provide a finisher to adapt the result into a more specific runtime shape.
     if (!finish) return loaded.value as R
     return finish(loaded.value, candidate.origin, retry)
   }
@@ -149,6 +181,11 @@ export namespace PluginLoader {
     report?: Report
   }
 
+  // Resolve and load all configured plugins in parallel.
+  //
+  // If `wait` is provided, file-based plugins that initially failed are retried once after the
+  // caller finishes preparing dependencies. This supports local plugins that depend on an install
+  // step happening elsewhere before their entrypoint becomes loadable.
   export async function loadExternal<R = Loaded>(input: Input<R>): Promise<R[]> {
     const candidates = input.items.map((origin) => ({ origin, plan: plan(origin.spec) }))
     const list: Array<Promise<R | undefined>> = []
@@ -160,6 +197,9 @@ export namespace PluginLoader {
       let deps: Promise<void> | undefined
       for (let i = 0; i < candidates.length; i++) {
         if (out[i] !== undefined) continue
+
+        // Only local file plugins are retried. npm plugins already attempted installation during
+        // the first pass, while file plugins may need the caller's dependency preparation to finish.
         const candidate = candidates[i]
         if (!candidate || pluginSource(candidate.plan.spec) !== "file") continue
         deps ??= input.wait()
@@ -167,6 +207,8 @@ export namespace PluginLoader {
         out[i] = await attempt(candidate, input.kind, true, input.finish, input.missing, input.report)
       }
     }
+
+    // Drop skipped/failed entries while preserving the successful result order.
     const ready: R[] = []
     for (const item of out) if (item !== undefined) ready.push(item)
     return ready