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.
  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 every component's server block.
  3. Discovery & Bundling: Coralite analyzes components and identifies 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['plugin-name'] object in component 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 a defineComponent. 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 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 };
    }
  });

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 client blocks using client.context.

Context utilities are namespaced on the context object available in every component's client execution block (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.`);
      }
    }
  });

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!