Single target plugin entrypoints (#19467)

This commit is contained in:
Sebastian
2026-03-28 00:44:46 +01:00
committed by GitHub
parent 02b19bc3d7
commit f3997d8082
18 changed files with 292 additions and 62 deletions

View File

@@ -17,12 +17,15 @@ import { errorMessage } from "@/util/error"
import { Installation } from "@/installation"
import {
checkPluginCompatibility,
getDefaultPlugin,
isDeprecatedPlugin,
parsePluginSpecifier,
pluginSource,
readPluginId,
readV1Plugin,
resolvePluginEntrypoint,
resolvePluginId,
resolvePluginTarget,
type PluginSource,
} from "./shared"
export namespace Plugin {
@@ -35,6 +38,8 @@ export namespace Plugin {
type Loaded = {
item: Config.PluginSpec
spec: string
target: string
source: PluginSource
mod: Record<string, unknown>
}
@@ -112,7 +117,8 @@ export namespace Plugin {
const resolved = await resolvePlugin(spec)
if (!resolved) return
if (pluginSource(spec) === "npm") {
const source = pluginSource(spec)
if (source === "npm") {
const incompatible = await checkPluginCompatibility(resolved, Installation.VERSION)
.then(() => false)
.catch((err) => {
@@ -156,14 +162,17 @@ export namespace Plugin {
return {
item,
spec,
target,
source,
mod,
}
}
async function applyPlugin(load: Loaded, input: PluginInput, hooks: Hooks[]) {
const plugin = getDefaultPlugin(load.mod) as PluginModule | undefined
if (plugin?.server) {
hooks.push(await plugin.server(input, Config.pluginOptions(load.item)))
const plugin = readV1Plugin(load.mod, load.spec, "server", "detect")
if (plugin) {
await resolvePluginId(load.source, load.spec, load.target, readPluginId(plugin.id, load.spec))
hooks.push(await (plugin as PluginModule).server(input, Config.pluginOptions(load.item)))
return
}

View File

@@ -21,6 +21,7 @@ export function parsePluginSpecifier(spec: string) {
export type PluginSource = "file" | "npm"
export type PluginKind = "server" | "tui"
type PluginMode = "strict" | "detect"
export function pluginSource(spec: string): PluginSource {
return spec.startsWith("file://") ? "file" : "npm"
@@ -123,6 +124,40 @@ export function readPluginId(id: unknown, spec: string) {
return value
}
export function readV1Plugin(
mod: Record<string, unknown>,
spec: string,
kind: PluginKind,
mode: PluginMode = "strict",
) {
const value = mod.default
if (!isRecord(value)) {
if (mode === "detect") return
throw new TypeError(`Plugin ${spec} must default export an object with ${kind}()`)
}
if (mode === "detect" && !("id" in value) && !("server" in value) && !("tui" in value)) return
const server = "server" in value ? value.server : undefined
const tui = "tui" in value ? value.tui : undefined
if (server !== undefined && typeof server !== "function") {
throw new TypeError(`Plugin ${spec} has invalid server export`)
}
if (tui !== undefined && typeof tui !== "function") {
throw new TypeError(`Plugin ${spec} has invalid tui export`)
}
if (server !== undefined && tui !== undefined) {
throw new TypeError(`Plugin ${spec} must default export either server() or tui(), not both`)
}
if (kind === "server" && server === undefined) {
throw new TypeError(`Plugin ${spec} must default export an object with server()`)
}
if (kind === "tui" && tui === undefined) {
throw new TypeError(`Plugin ${spec} must default export an object with tui()`)
}
return value
}
export async function resolvePluginId(source: PluginSource, spec: string, target: string, id: string | undefined) {
if (source === "file") {
if (id) return id
@@ -135,15 +170,3 @@ export async function resolvePluginId(source: PluginSource, spec: string, target
}
return pkg.json.name.trim()
}
export function getDefaultPlugin(mod: Record<string, unknown>) {
// A single default object keeps v1 detection explicit and avoids scanning exports.
const value = mod.default
if (!isRecord(value)) return
const server = "server" in value ? value.server : undefined
const tui = "tui" in value ? value.tui : undefined
if (server !== undefined && typeof server !== "function") return
if (tui !== undefined && typeof tui !== "function") return
if (server === undefined && tui === undefined) return
return value
}