parallel pre-load
parent
3341dba46e
commit
5c95616579
|
|
@ -193,14 +193,14 @@ export namespace TuiPlugin {
|
|||
await deps
|
||||
}
|
||||
|
||||
const loadOne = async (item: (typeof plugins)[number], retry = false) => {
|
||||
const prep = async (item: (typeof plugins)[number], retry = false) => {
|
||||
const spec = Config.pluginSpecifier(item)
|
||||
log.info("loading tui plugin", { path: spec, retry })
|
||||
const target = await resolvePluginTarget(spec).catch((error) => {
|
||||
log.error("failed to resolve tui plugin", { path: spec, retry, error })
|
||||
return
|
||||
})
|
||||
if (!target) return false
|
||||
if (!target) return
|
||||
const meta = await PluginMeta.touch(spec, target).catch((error) => {
|
||||
log.warn("failed to track tui plugin", { path: spec, retry, error })
|
||||
})
|
||||
|
|
@ -217,62 +217,74 @@ export namespace TuiPlugin {
|
|||
|
||||
const root = pluginRoot(spec, target)
|
||||
const install = makeInstallFn(getPluginMeta(config, item), root)
|
||||
|
||||
const mod = await import(target).catch((error) => {
|
||||
log.error("failed to load tui plugin", { path: spec, retry, error })
|
||||
return
|
||||
})
|
||||
if (!mod) return false
|
||||
if (!mod) return
|
||||
|
||||
for (const [name, entry] of uniqueModuleEntries(mod)) {
|
||||
if (!entry || typeof entry !== "object") {
|
||||
log.warn("ignoring non-object tui plugin export", {
|
||||
path: spec,
|
||||
name,
|
||||
type: entry === null ? "null" : typeof entry,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const slotPlugin = getTuiSlotPlugin(entry)
|
||||
if (slotPlugin) input.slots.register(slotPlugin)
|
||||
|
||||
const tuiPlugin = getTuiPlugin(entry)
|
||||
if (!tuiPlugin) continue
|
||||
await tuiPlugin(
|
||||
{
|
||||
...input,
|
||||
api: {
|
||||
command: input.api.command,
|
||||
route: input.api.route,
|
||||
ui: input.api.ui,
|
||||
keybind: input.api.keybind,
|
||||
theme: Object.create(input.api.theme, {
|
||||
install: {
|
||||
value: install,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Config.pluginOptions(item),
|
||||
)
|
||||
return {
|
||||
item,
|
||||
spec,
|
||||
mod,
|
||||
install,
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
for (const item of plugins) {
|
||||
const ok = await loadOne(item)
|
||||
if (ok) continue
|
||||
const loaded = await Promise.all(plugins.map((item) => prep(item)))
|
||||
|
||||
const spec = Config.pluginSpecifier(item)
|
||||
if (!spec.startsWith("file://")) continue
|
||||
for (let i = 0; i < plugins.length; i++) {
|
||||
let load = loaded[i]
|
||||
if (!load) {
|
||||
const item = plugins[i]
|
||||
if (!item) continue
|
||||
const spec = Config.pluginSpecifier(item)
|
||||
if (!spec.startsWith("file://")) continue
|
||||
await wait()
|
||||
load = await prep(item, true)
|
||||
}
|
||||
if (!load) continue
|
||||
|
||||
await wait()
|
||||
await loadOne(item, true)
|
||||
// Keep plugin execution sequential for deterministic side effects:
|
||||
// command registration order affects keybind/command precedence,
|
||||
// route registration is last-wins when ids collide,
|
||||
// and hook chains rely on stable plugin ordering.
|
||||
for (const [name, value] of uniqueModuleEntries(load.mod)) {
|
||||
if (!value || typeof value !== "object") {
|
||||
log.warn("ignoring non-object tui plugin export", {
|
||||
path: load.spec,
|
||||
name,
|
||||
type: value === null ? "null" : typeof value,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const slotPlugin = getTuiSlotPlugin(value)
|
||||
if (slotPlugin) input.slots.register(slotPlugin)
|
||||
|
||||
const tuiPlugin = getTuiPlugin(value)
|
||||
if (!tuiPlugin) continue
|
||||
await tuiPlugin(
|
||||
{
|
||||
...input,
|
||||
api: {
|
||||
command: input.api.command,
|
||||
route: input.api.route,
|
||||
ui: input.api.ui,
|
||||
keybind: input.api.keybind,
|
||||
theme: Object.create(input.api.theme, {
|
||||
install: {
|
||||
value: load.install,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Config.pluginOptions(load.item),
|
||||
)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await PluginMeta.persist().catch((error) => {
|
||||
|
|
|
|||
|
|
@ -82,13 +82,13 @@ export namespace Plugin {
|
|||
return value.server
|
||||
}
|
||||
|
||||
for (const item of plugins) {
|
||||
const prep = async (item: (typeof plugins)[number]) => {
|
||||
const spec = Config.pluginSpecifier(item)
|
||||
// ignore old codex plugin since it is supported first party now
|
||||
if (spec.includes("opencode-openai-codex-auth") || spec.includes("opencode-copilot-auth")) continue
|
||||
if (spec.includes("opencode-openai-codex-auth") || spec.includes("opencode-copilot-auth")) return
|
||||
log.info("loading plugin", { path: spec })
|
||||
const target = await resolvePlugin(spec)
|
||||
if (!target) continue
|
||||
if (!target) return
|
||||
const mod = await import(target).catch((err) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
log.error("failed to load plugin", { path: spec, error: message })
|
||||
|
|
@ -99,23 +99,35 @@ export namespace Plugin {
|
|||
})
|
||||
return
|
||||
})
|
||||
if (!mod) continue
|
||||
if (!mod) return
|
||||
return {
|
||||
item,
|
||||
spec,
|
||||
mod,
|
||||
}
|
||||
}
|
||||
|
||||
const loaded = await Promise.all(plugins.map((item) => prep(item)))
|
||||
for (const load of loaded) {
|
||||
if (!load) continue
|
||||
|
||||
// Keep plugin execution sequential so hook registration and execution
|
||||
// order remains deterministic across plugin runs.
|
||||
// Prevent duplicate initialization when plugins export the same function
|
||||
// as both a named export and default export (e.g., `export const X` and `export default X`).
|
||||
// uniqueModuleEntries keeps only the first export for each shared value reference.
|
||||
await (async () => {
|
||||
for (const [, entry] of uniqueModuleEntries(mod)) {
|
||||
for (const [, entry] of uniqueModuleEntries(load.mod)) {
|
||||
const server = getServerPlugin(entry)
|
||||
if (!server) throw new TypeError("Plugin export is not a function")
|
||||
hooks.push(await server(input, Config.pluginOptions(item)))
|
||||
hooks.push(await server(input, Config.pluginOptions(load.item)))
|
||||
}
|
||||
})().catch((err) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
log.error("failed to load plugin", { path: spec, error: message })
|
||||
log.error("failed to load plugin", { path: load.spec, error: message })
|
||||
Bus.publish(Session.Event.Error, {
|
||||
error: new NamedError.Unknown({
|
||||
message: `Failed to load plugin ${spec}: ${message}`,
|
||||
message: `Failed to load plugin ${load.spec}: ${message}`,
|
||||
}).toObject(),
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue