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:

  1. Registration & Bootstrapping: Coralite loads your plugin via definePlugin. It initializes both server and client configurations, setting up global contexts that will be available to components and pages.
  2. Server Execution: During the build process, Coralite triggers server-side lifecycle hooks (like onPageSet or onBeforeBuild). If your plugin provides a server.context, its methods become available to component server() blocks and page server handlers.
  3. 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.
  4. Client Hydration & Execution: In the browser, Coralite initializes the client.context. Plugin hooks like onBeforeComponentRender fire as components mount, and your utilities become available via the context object in component client() and page client() 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.

javascript
Code copied!
  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.

javascript
Code copied!
  // 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:

javascript
Code copied!
  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:

html
Code copied!
  <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.

javascript
Code copied!
  // 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...')
        };
      };
    }
  }
javascript
Code copied!
  // 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.

javascript
Code copied!
  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.

Start Building with Coralite!

Use the scaffolding script to get jump started into your next project with Coralite

Copied commandline!