import type { Component, DefineComponent } from 'vue'; /** * The registration surface handed to every plugin bundle. */ export interface PluginRegistrar { /** Register an Inertia page, e.g. `page('announcements::Index', Index)`. */ page(name: string, component: DefineComponent): void; /** * Register a slot component by name, e.g. * `slot('announcements::NavbarBell', Bell)`. * * Which position a component fills is decided server side by * Vanguard::slot('navbar:items', NavbarItemsSlot::class), keeping placement * next to the permission and data logic that drives it. */ slot(name: string, component: Component): void; /** Register a dashboard widget component by name. */ widget(name: string, component: Component): void; } export type PluginCallback = (registrar: PluginRegistrar) => void; /** * The queue shim installed inline in the root template. Plugin bundles call * `Vanguard.plugin(...)` which pushes onto `_q` until the real runtime boots, * so plugin scripts and the app bundle may load in any order. */ interface VanguardShim { _q?: Array<[string, PluginCallback]>; plugin(slug: string, callback: PluginCallback): void; } const pages = new Map(); const slots = new Map(); const widgets = new Map(); const registered = new Set(); const registrar: PluginRegistrar = { page(name, component) { pages.set(name, component); }, slot(name, component) { slots.set(name, component); }, widget(name, component) { widgets.set(name, component); }, }; function registerPlugin(slug: string, callback: PluginCallback): void { if (registered.has(slug)) { return; } registered.add(slug); callback(registrar); } /** * Replace the inline shim with the real runtime and drain anything that was * queued while the app bundle was still loading. * * `libs` are the shared dependencies plugin bundles externalise rather than * bundling themselves, which keeps a plugin bundle to just its own components. */ export function installVanguardRuntime(libs: Record): void { const shim = (window as unknown as { Vanguard?: VanguardShim }).Vanguard; const queued = shim?._q ?? []; (window as unknown as { Vanguard: unknown }).Vanguard = { ...libs, plugin: registerPlugin, resolvePage, resolveSlot, resolveWidget, }; for (const [slug, callback] of queued) { registerPlugin(slug, callback); } } /** * Resolves once every deferred script in the document has run. * * Plugin bundles are module scripts at the end of the body, so they execute * after the app bundle that calls createInertiaApp. Waiting for them is what * lets the first page of a visit be a plugin page. * * Built at module load, which happens while the app bundle is still executing, * because `readyState` is already "interactive" by then: the spec sets it * before running deferred scripts, so checking it later would short circuit and * report ready while plugin bundles were still queued. The load listener is a * backstop for the window between DOMContentLoaded and readyState "complete". */ const pluginsLoaded: Promise = document.readyState === 'complete' ? Promise.resolve() : new Promise((resolve) => { document.addEventListener('DOMContentLoaded', () => resolve(), { once: true }); window.addEventListener('load', () => resolve(), { once: true }); }); /** Resolve a namespaced plugin page such as `announcements::Index`. */ export function resolvePage(name: string): DefineComponent | Promise { const registered = pages.get(name); if (registered) { return registered; } return pluginsLoaded.then(() => { const component = pages.get(name); if (!component) { throw new Error( `[Vanguard] Inertia page "${name}" was not registered by any plugin. ` + `Did you publish the plugin assets (php artisan vendor:publish --tag=public)?`, ); } return component; }); } /** Resolve a slot component by name, e.g. `announcements::NavbarBell`. */ export function resolveSlot(name: string): Component | undefined { return slots.get(name); } /** Resolve a dashboard widget component by name. */ export function resolveWidget(name: string): Component | undefined { return widgets.get(name); }