Authoring Plugins
Plugins are the primary way to extend Coralite's functionality. The framework's architecture is designed to be highly extensible via the definePlugin function.
With plugins, you can hook deep into the build lifecycle, register custom components globally, bundle external scripts, and inject custom helper functions directly into the frontend.
The Conceptual Flow #
Authoring a plugin involves understanding how Coralite synthesizes server-side rendering and client-side hydration. When you register a plugin, Coralite integrates it through the following flow:
- Registration & Bootstrapping: Coralite loads your plugin via
definePlugin. It initializes bothserverandclientconfigurations, setting up global contexts that will be available to components and pages. - Server Execution: During the build process, Coralite triggers server-side lifecycle hooks (like
onPageSetoronBeforeBuild). If your plugin provides aserver.context, its methods become available to componentserver()blocks and pageserverhandlers. - Discovery & Bundling: Coralite analyzes components and pages, identifying which plugin utilities are being used. It automatically bundles necessary dependencies and prepares the client-side runtime.
- Client Hydration & Execution: In the browser, Coralite initializes the
client.context. Plugin hooks likeonBeforeComponentRenderfire as components mount, and your utilities become available via the context object in componentclient()and pageclient()blocks.
Server-Side Lifecycle Hooks #
Plugins can hook into almost every phase of Coralite's build lifecycle to perform actions when files are processed or rendering occurs.
Hooks receive a context object containing the app instance, the plugin's config, and relevant lifecycle data.
export default definePlugin({
name: 'my-hook-plugin',
server: {
// Fire when a new page is discovered
onPageSet: async ({ page, data, app }) => {
console.log(`Processing page: ${data.path.pathname}`);
// You can mutate the page object directly
page.meta.processedBy = 'my-hook-plugin';
},
// Fire before a page is rendered to HTML
onBeforePageRender: async ({ elements, page, app }) => {
// 'elements' is the parsed HTML AST.
// You can inject or modify elements here.
elements.root.children.push({
type: 'tag',
name: 'div',
attribs: { id: 'plugin-injected' },
children: [{ type: 'text', data: 'Hello from Plugin!' }]
});
}
}
});
For a full list of available hooks and their arguments, see the API Reference.
Server-Side Context #
While client.context is for the browser, server.context allows you to provide utilities to the server block of components and pages (server). This is useful for providing data fetching helpers, logging, or shared business logic that should only run during the build.
Just like the client context, server.context uses a Two-Phase Resolver. The first phase allows for global setup and inter-plugin registration, while the second phase provides the actual utilities to the component or page instance.
// Plugin definition
export default definePlugin({
name: 'data-helper',
server: {
context: (pluginContext) => {
// Phase 1: Global setup. Mutate pluginContext to share with other plugins.
pluginContext.db = initializeDB();
return (instanceContext) => {
// Phase 2: Per-instance resolver.
// Use instanceContext to access other plugins.
const logger = instanceContext['logger-plugin'];
return {
fetchSecure: async (url) => {
logger.log(`Fetching: ${url}`);
const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${pluginContext.config.token}` }
});
return response.json();
}
};
}
}
}
});
// Usage in a component
export default defineComponent({
async server({ 'data-helper': helper }) {
const data = await helper.fetchSecure('https://api.example.com/data');
return { data };
}
});
Ubiquitous Ambient Context (context) #
When a plugin provides lightweight, synchronous utilities (such as an i18n translation helper $t, formatters, or math utilities) that need to be universally accessible across all execution blocks — including component and page server(), getters, client(), slots, and style — declare top-level context instead of splitting into separate server.context and client.context blocks.
Top-level context uses the curried Two-Phase Resolver pattern: Phase 1 runs at plugin initialization, and Phase 2 lazily resolves per instance context:
import { definePlugin } from 'coralite';
export default definePlugin({
name: '$t',
config: {
defaultLocale: { type: String, default: 'en' }
},
context: ({ app, config }) => {
const dict = {
en: { welcome: 'Welcome', submit: 'Submit' },
es: { welcome: 'Bienvenido', submit: 'Enviar' }
};
return (instanceContext) => {
const locale = instanceContext.route?.params?.lang || config.defaultLocale;
return (key) => dict[locale]?.[key] || key;
};
}
});
Because top-level context is available directly in getters, zero-JS static components can resolve localized copy during SSR and client hydration without needing to define a boilerplate server() block:
<template id="greeting-banner">
<h1></h1>
</template>
<script type="module">
export const getters = {
heading: ({ $t }) => $t('welcome')
};
</script>
Mutual Exclusivity Rule (CORALITE-P211): A plugin must define either top-level context (for ubiquitous, synchronous ambient helpers) or environment-specific server.context / client.context (for asynchronous, environment-specialized tasks). Defining both top-level context and environment-specific context on the same plugin throws CORALITE-P211.
Global Component Registration #
Plugins can distribute their own HTML components. By providing an array of file paths to the components property in your plugin configuration, Coralite automatically reads, parses, and registers these files as global components during initialization.
This means users of your plugin can instantly use your custom tags (e.g., <my-awesome-slider>) without having to copy the HTML files into their own project's src/components/ directory.
Injecting Client Helpers #
The most powerful feature of Coralite plugins is the ability to provide reusable functionality directly to component and page client blocks using client.context.
Context utilities are namespaced on the context object available in component client() and page client execution blocks (e.g. context['my-plugin']). However, because Coralite is a static site generator, there is a strict boundary between server code and client code.
The Two-Phase Resolver #
Helpers must be structured using a Two-Phase Resolver function that executes during setup. The outermost function receives the plugin context (Phase 1, runs on the server and contains the global application instance and plugin configuration), and it must return a second function that receives the local Web Component instance context (Phase 2). This second function then returns an object containing the actual utility functions accessible on the client.
To use client-only APIs (like window, document, or the component's AbortSignal), the returned helper functions encapsulate that logic within their own bodies.
Inter-Plugin Communication #
Because Phase 1 of the resolver runs during initialization, you can use it to share services between plugins. Any property added to the pluginContext object in Phase 1 becomes available to all subsequent plugins.
// Plugin A: Provides a shared service
client: {
context: (pluginContext) => {
pluginContext.mySharedService = { log: (msg) => console.log(msg) };
return (instanceContext) => ({});
}
}
// Plugin B: Consumes the shared service
client: {
context: (pluginContext) => {
const service = pluginContext.mySharedService;
return (instanceContext) => {
return {
doSomething: () => service.log('Doing something...')
};
};
}
}
// Server-side definition in definePlugin
client: {
context: (pluginContext) => {
// Phase 1: Runs on the SERVER and receives pluginContext (including config)
const apiKey = pluginContext.config.apiKey;
// Share data with other plugins in Phase 1
pluginContext.sharedClientData = { version: '1.0' };
return (instanceContext) => {
// Phase 2: Receives the local Web Component instance context (signal, state, etc.)
// and the namespaced proxy to other plugins.
const { signal } = instanceContext;
const uiPlugin = instanceContext['ui-plugin'];
// Phase 3: Return the actual helper object accessible in the browser
return {
trackEvent: (buttonElement, eventName) => {
uiPlugin.showToast(`Tracking: ${eventName}`);
buttonElement.addEventListener('click', () => {
window.navigator.sendBeacon('https://api.analytics.com', JSON.stringify({
key: apiKey,
event: eventName
}));
}, { signal });
}
};
}
}
}
Client-Side Lifecycle Hooks #
Just like on the server, plugins can hook into the client-side lifecycle of every component. This is useful for global monitoring, automatic event tracking, or managing global state.
onBeforeComponentRender(instanceContext): Fires before a component'sclientblock executes.onAfterComponentRender(instanceContext): Fires after a component'sclientblock executes.onDisconnected(instanceContext): Fires when a component is removed from the DOM.
export default definePlugin({
name: 'client-monitor',
client: {
onBeforeComponentRender: (context) => {
console.log(`Component ${context.id} is mounting...`);
},
onDisconnected: (context) => {
console.log(`Component ${context.id} was removed.`);
}
}
});
Testing Plugin Context #
Use testing.mocks.plugins in mode: 'testing' to shallow-merge mocked methods over a plugin's resolved server.context or client.context. The real Phase 1 and Phase 2 functions still run, so use a stub plugin when the real plugin performs external I/O or side effects during setup.
For focused unit tests, inject a plugin-shaped object directly with options.context on loadComponent or loadPage. This bypasses plugin setup entirely.
Zero-Config Dependencies #
Unlike earlier versions of Coralite, V1 removes the need for manual dependency arrays like client.imports or client.components. The underlying AST parser automatically detects dynamic imports (e.g., await import('my-analytics-sdk')) inside the actual runtime logic of your context utilities and bundles them automatically.
Ready to build your plugin? Consult the strict API signatures and configuration options in the definePlugin API Reference.