Coralite Plugin System

Coralite plugins are extensible modules that integrate into the Coralite framework's lifecycle, enabling developers to customize and enhance its behavior through hooks, data manipulation, and dynamic content generation.

Component Types #

Coralite supports two types of components, each with different requirements:

Static Components #

Static components are simple HTML files that support basic token replacement but don't require any script processing:

HTML
Code copied!
<template id="simple-component">
  <div>
    <h1 class="display">{{ title }}</h1>
    <p>{{ description }}</p>
  </div>
</template>

No script tag needed - Coralite will process token replacements automatically.

Dynamic Components #

Dynamic components require a script tag and are used when you need:

HTML
Code copied!
<template id="dynamic-component">
  <div>
    <h1 class="display">{{ greeting }}</h1>
    <button type="button" ref="actionBtn">Click me</button>
  </div>
</template>

<script type="module">  
  import { defineComponent } from 'coralite'
  
  export default defineComponent({
    attributes: {
      name: {
        type: String,
        default: 'Guest'
      }
    },
    getters: {
      greeting: (state) => `Hello, ${state.name}!`
    },
    client: ({ refs }) => {
      const btn = refs('actionBtn')
      btn.addEventListener('click', () => {
        console.log('Button clicked!')
      })
    }
  })
</script>

Requires defineComponent as the default export.

Creating Plugins #

Use the definePlugin function to define a new plugin with configuration options:

JavaScript
Code copied!
  import { definePlugin } from 'coralite'
  
  const myPlugin = definePlugin({
    name: 'my-plugin',
    server: {
      context: (pluginContext) => {
        return {
          myServerMethod: () => {
            return { custom: 'data' }
          }
        }
      },
      components: ['src/components/custom.html'],
      onPageSet: async (data) => {
        console.log('Page created:', data.path.pathname)
      }
    },
    client: {
      context: (pluginContext) => (instanceContext) => {
        return {
          formatDate: (date) => new Date(date).toLocaleDateString()
        }
      }
    }
  })

definePlugin Parameters #

Parameter Type Required Description
name string Yes Unique identifier for the plugin
server object No Server-side configuration, components, context, and lifecycle hooks
client object No Client-side runtime configuration, context resolver, and browser hooks

Server Context Binding #

Plugins can expose custom methods and data to component server() blocks via server.context. This runs strictly on the server during the build process.

Defining Server Context #

The server.context property is a function that receives the plugin context (with the plugin's configuration and the global app instance) and returns an object of utilities:

JavaScript
Code copied!
  import { definePlugin } from 'coralite'
  
  export const dbPlugin = definePlugin({
    name: 'db-plugin',
    server: {
      config: {
        connectionString: 'mongodb://localhost:27017'
      },
      context: (pluginContext) => {
        const db = connect(pluginContext.config.connectionString);
        return {
          getUser: async (userId) => {
            return await db.users.findOne({ id: userId });
          }
        }
      }
    }
  })

Using Server Context in Components #

Component server() functions receive namespaced plugin contexts via their first parameter. You can extract methods from the namespace key matching your plugin's name:

HTML
Code copied!
<template id="user-profile">
  <div>
    <h1>{{ userName }}</h1>
  </div>
</template>

<script type="module">  
  import { defineComponent } from 'coralite'
  
  export default defineComponent({
    attributes: {
      userId: { type: String }
    },
    async server (context) {
      const { getUser } = context['db-plugin']
      const user = await getUser(context.state.userId)
      return {
        userName: user.name
      }
    }
  })
</script>

Note: Since server.context is evaluated once per build session rather than per-instance, any methods requiring instance-specific data (such as current component state) should accept the component context as a parameter when called.

Page-Level Plugins #

Page-level plugins use lifecycle hooks to modify the final HTML output. They work across all pages and components.

How Page-Level Plugins Work #

These plugins are registered in your coralite.config.js and automatically process pages during the build:

JavaScript
Code copied!
  // coralite.config.js
  import inlineCSSPlugin from './plugins/inline-css.js'
  
  export default {
    components: './components',
    pages: './pages',
    plugins: [
      inlineCSSPlugin({
        path: './styles',
        minify: true
      })
    ]
  }

Example: Inline CSS Plugin #

Here's how a page-level plugin transforms <link> tags to inline <style> tags:

JavaScript
Code copied!
  import { definePlugin } from 'coralite'
  import { readFile } from 'node:fs/promises'
  import { join, resolve } from 'node:path'
  
  /**
  * @param {Object} config -
  * @param {string} config.path -
  * @param {boolean} config.minify -
  */
  export default ({ path, minify } = {}) => {
    return definePlugin({
      name: 'inline-css',
      async onPageSet (context) {
        // Walk through all elements in the page
        let stack = [context.elements.root]
  
        while (stack.length > 0) {
          const node = stack.pop()
  
          if (node.type === 'tag'
          && node.name === 'link'
          && node.attribs.rel === 'stylesheet'
          && node.attribs['inline-css'] != null
          ) {
            // Read and inline CSS file
            const cssPath = resolve(join(path || '', node.attribs['inline-css']))
            const css = await readFile(cssPath, 'utf8')
  
            // Replace link with style tag
            node.name = 'style'
            node.attribs = {}
            node.children = [{
              type: 'text',
              data: css,
              parent: node
            }]
          }
  
          if (node.children) {
            stack.push(...node.children)
          }
        }
      }
    })
  }

Server-Side Lifecycle Hooks #

Hooks allow plugins to respond to specific events in the Coralite build lifecycle. These execute strictly on the server during the compilation and generation phases, making them perfect for generating sitemaps, indexing search data, or tracking build analytics. All hooks must reside inside the server block.

JavaScript
Code copied!
  import { definePlugin } from 'coralite'
  
  export const analyticsPlugin = definePlugin({
    name: 'analytics',
    server: {
      // Page Lifecycle
      onPageSet: async ({ page, elements, state, data, app }) => {
        // Called when a new page is created
        console.log('New page:', page.url.pathname)
      },
      onPageUpdate: async ({ page, elements, newValue, oldValue, app }) => {
        // Called when a page is updated
        console.log('Page updated:', page.url.pathname)
      },
      onPageDelete: async ({ ...data, app }) => {
        // Called when a page is deleted
        console.log('Page deleted.')
      },
  
      // Component Lifecycle
      onComponentSet: async ({ component, app }) => {
        // Called when a component is created
        console.log('New component:', component.id)
      },
      onComponentUpdate: async ({ component, app }) => {
        // Called when a component is updated
        console.log('Component updated:', component.id)
      },
      onComponentDelete: async ({ component, app }) => {
        // Called when a component is deleted
        console.log('Component deleted:', component.id)
      },
  
      // Build Orchestration
      onBeforeBuild: async ({ path, options, app }) => {
        console.log('Build process starting...');
      },
      onAfterBuild: async ({ results, error, duration }) => {
        console.log(`Build completed in ${duration}ms`);
      }
    }
  })

Client Plugins & Helpers #

Client plugins provide client-side utilities that are available in component client blocks. To safely bridge global plugin configuration with local Web Component instances, Coralite requires client.context to be authored using a Two-Phase Resolver.

The Two-Phase Resolver #

  1. Phase 1 (Global Context): The outermost function receives the pluginContext (containing global config). Use this phase to initialize shared state or load global modules.
  2. Phase 2 (Local Instance Context): It must return a second function that receives the local Web Component instance context ({ state, signal, root, instanceId, refs, observe }).
  3. Phase 3 (Helpers Object): The second function returns an object containing the helper methods. These helpers are exposed on the component's client context under the plugin's namespace key.

Note on signal: The signal is tied natively to the component's mount lifecycle. It triggers .abort() automatically when a component is unmounted from the DOM, making it perfect for cleaning up event listeners (e.g. element.addEventListener('click', fn, { signal })).

JavaScript
Code copied!
  import { definePlugin } from 'coralite'
  
  export const myCustomPlugin = definePlugin({
    name: 'my-custom-plugin',
    client: {
      context: (pluginContext) => {
        // Phase 1: Receives Plugin Context
        const globalValue = pluginContext.config?.value;
  
        return (instanceContext) => {
          // Phase 2: Receives Local Instance Context
          const { signal } = instanceContext;
  
          // Phase 3: Return the helper object accessible via context['my-custom-plugin']
          return {
            myHelper: (selector) => {
              const element = document.querySelector(selector);
              if (element && signal) {
                element.addEventListener('click', () => {
                  console.log('Clicked element with config value:', globalValue)
                }, { signal });
              }
              return element;
            }
          }
        }
      }
    }
  })

Usage in Components #

Once registered, component client blocks access the helpers object under the plugin's namespace:

JavaScript
Code copied!
  // In component client block:
  export default defineComponent({
    client: (context) => {
      const { myHelper } = context['my-custom-plugin'];
      const btn = myHelper('.btn-class');
    }
  })

Built-in Plugins #

Coralite includes built-in plugins that provide core functionality:

defineComponent #

The defineComponent plugin is required for all dynamic components. It provides the structured wrapper for your inputs (attributes), server-side fetching (server), derived state (getters), and client-side execution (client):

JavaScript
Code copied!
  import { defineComponent } from 'coralite'
  
  export default defineComponent({
    attributes: {
        firstName: { type: String, default: '' },
        lastName: { type: String, default: '' },
        date: { type: String }
    },
    getters: {
      fullName: (state) => `${state.firstName} ${state.lastName}`.trim(),
      formattedDate: (state) => state.date ? new Date(state.date).toLocaleDateString() : ''
    },
    slots: {
      // Custom Light DOM interception and transformation
      content (slotNodes, state) {
        // Transform slot content before it projects into the component
        return slotNodes
      }
    },
    // Behavior: Runs natively in the browser
    client ({ state, signal, refs }) {
      // The client block natively receives the unified reactive state
      const btn = refs('actionBtn')
      
      btn.addEventListener('click', () => {
        console.log(`Hello, ${state.fullName}!`)
      }, { signal })
    }
  })

When to Use #

  • When your component needs attributes or server state fetching
  • When you need derived state via getters
  • When you need custom slot processing
  • When you need client-side JavaScript
  • When you need to use plugin methods

When NOT to Use #

  • Simple static components with only token replacement
  • Components that don't need client-side execution

refs Helper #

The refs helper provides DOM element access at runtime:

HTML
Code copied!
<template id="my-component">
  <div>
    <span ref="author">Author Name</span>
    <button type="button" ref="actionBtn">Click me</button>
    <input type="text" ref="userInput"></input>
  </div>
</template>

<script type="module">  
  import { defineComponent } from 'coralite'
  
  export default defineComponent({
    client ({ signal, refs }) {
      // Get DOM elements by their ref name
      const button = refs('actionBtn')
      const input = refs('userInput')
      const author = refs('author')
  
      // Use the elements at runtime
      button.addEventListener('click', () => {
        author.textContent = input.value || 'Anonymous'
      }, { signal })
    }
  })
</script>

Metadata Plugin #

The metadata plugin automatically extracts SEO data from the document's <head> during the build and exposes it to the context.page.meta environment object:

  • Maps <html lang="..."> to context.page.meta.lang.
  • Maps <title> to context.page.meta.title.
  • Maps <meta name="viewport" content="..."> to context.page.meta.viewport.

See the Metadata Plugin Reference for full details.

Static Assets Plugin #

The staticAssetPlugin handles the orchestration of copying raw files (like .wasm, images, or pre-compiled scripts) directly into the final build directory.

Configuration #

It acts as a higher-order function that accepts an assets array and returns a configured Coralite plugin. Every object in the array must contain a dest (destination) property.

Lifecycle Hook #

It triggers on the onBeforeBuild hook, ensuring all required binary/static dependencies are physically present in the output directory before Coralite attempts to render pages or start the dev server.

Resolution Strategies #

  • Local Resolution: If the asset object contains a src property, it recursively creates the destination directory and copies the file/folder using Node's cp.
  • NPM Package Resolution: If src is missing, it requires both pkg and path options. It uses Node's createRequire and require.resolve to traverse node_modules and locate the root directory of the requested pkg. It then appends the path to the package root and copies the asset out of the dependency.

Complete Example #

Here's a complete example showing the "Smart State, Dumb Template" architecture and built-in plugins working together:

HTML
Code copied!
<template id="counter">
  <div class="counter">
    <h2>Count: {{ currentCount }}</h2>
    <button type="button" ref="increment">+</button>
    <button type="button" ref="decrement">-</button>
  </div>
</template>

<script type="module">  
  import { defineComponent } from 'coralite'
  
  export default defineComponent({
    attributes: {
      initial: {
        type: Number,
        default: 0
      }
    },
    server ({ state }) {
      return {
        currentCount: state.initial
      }
    },
    client ({ state, signal, refs }) {
      const increment = refs('increment')
      const decrement = refs('decrement')
  
      increment.addEventListener('click', () => {
        state.currentCount++
      }, { signal })
  
      decrement.addEventListener('click', () => {
        state.currentCount--
      }, { signal })
    }
  })
</script>

Start Building with Coralite!

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

Copied commandline!