@@ -12,31 +12,41 @@ import { ConfigPlugin } from "@/config/plugin"
1212import { InstallationVersion } from "@/installation/version"
1313
1414export 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