Files
ppeasy_vg12/resources/js/vanguard.ts
T
2026-10-09 08:21:06 +02:00

136 lines
4.5 KiB
TypeScript

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<string, DefineComponent>();
const slots = new Map<string, Component>();
const widgets = new Map<string, Component>();
const registered = new Set<string>();
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<string, unknown>): 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<void> =
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<DefineComponent> {
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);
}