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:
<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:
- Attributes or server state fetching
- Derived state via Getters
- Client-side JavaScript execution
- Access to plugin methods
- DOM element references
<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:
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:
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:
<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:
// 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:
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.
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 #
- Phase 1 (Global Context): The outermost function receives the
pluginContext(containing globalconfig). Use this phase to initialize shared state or load global modules. - 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 }). - 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 })).
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:
// 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):
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:
<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="...">tocontext.page.meta.lang. - Maps
<title>tocontext.page.meta.title. - Maps
<meta name="viewport" content="...">tocontext.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
srcproperty, it recursively creates the destination directory and copies the file/folder using Node'scp. - NPM Package Resolution: If
srcis missing, it requires bothpkgandpathoptions. It uses Node'screateRequireandrequire.resolveto traversenode_modulesand locate the root directory of the requestedpkg. It then appends thepathto 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:
<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>