|
|
@@ -11,164 +11,162 @@ 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<string, unknown>
|
|
|
- }
|
|
|
+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<string, unknown>
|
|
|
+}
|
|
|
|
|
|
- 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 }
|
|
|
+ return { ok: false, stage: "compatibility", 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: 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<R>(
|
|
|
- candidate: Candidate,
|
|
|
- kind: PluginKind,
|
|
|
- retry: boolean,
|
|
|
- finish: ((load: Loaded, origin: ConfigPlugin.Origin, retry: boolean) => Promise<R | undefined>) | undefined,
|
|
|
- missing: ((value: Missing, origin: ConfigPlugin.Origin, retry: boolean) => Promise<R | undefined>) | undefined,
|
|
|
- report: Report | undefined,
|
|
|
- ): Promise<R | undefined> {
|
|
|
- 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<R>(
|
|
|
+ candidate: Candidate,
|
|
|
+ kind: PluginKind,
|
|
|
+ retry: boolean,
|
|
|
+ finish: ((load: Loaded, origin: ConfigPlugin.Origin, retry: boolean) => Promise<R | undefined>) | undefined,
|
|
|
+ missing: ((value: Missing, origin: ConfigPlugin.Origin, retry: boolean) => Promise<R | undefined>) | undefined,
|
|
|
+ report: Report | undefined,
|
|
|
+): Promise<R | undefined> {
|
|
|
+ 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<R> = {
|
|
|
- items: ConfigPlugin.Origin[]
|
|
|
- kind: PluginKind
|
|
|
- wait?: () => Promise<void>
|
|
|
- finish?: (load: Loaded, origin: ConfigPlugin.Origin, retry: boolean) => Promise<R | undefined>
|
|
|
- missing?: (value: Missing, origin: ConfigPlugin.Origin, retry: boolean) => Promise<R | undefined>
|
|
|
- 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<R = Loaded>(input: Input<R>): Promise<R[]> {
|
|
|
- const candidates = input.items.map((origin) => ({ origin, plan: plan(origin.spec) }))
|
|
|
- const list: Array<Promise<R | undefined>> = []
|
|
|
- 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<void> | 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<R> = {
|
|
|
+ items: ConfigPlugin.Origin[]
|
|
|
+ kind: PluginKind
|
|
|
+ wait?: () => Promise<void>
|
|
|
+ finish?: (load: Loaded, origin: ConfigPlugin.Origin, retry: boolean) => Promise<R | undefined>
|
|
|
+ missing?: (value: Missing, origin: ConfigPlugin.Origin, retry: boolean) => Promise<R | undefined>
|
|
|
+ report?: Report
|
|
|
+}
|
|
|
+
|
|
|
+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>> = []
|
|
|
+ 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<void> | 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
|
|
|
}
|