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:
- Computed tokens or slots
- 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({
tokens: {
greeting: ({ name }) => `Hello, ${name}!`
},
script: (context, helpers) => {
const btn = helpers.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',
method: (options, context) => {
// Server-side logic
return { ...context.values, custom: 'data' }
},
components: ['src/components/custom.html'],
onPageSet: async (data) => {
console.log('Page created:', data.path.pathname)
},
client: {
helpers: {
formatDate: (context) => {
return (date) => new Date(date).toLocaleDateString()
}
}
}
})
definePlugin Parameters #
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Unique identifier for the plugin |
method |
function |
No | Server-side function available in components |
components |
string[] |
No | Array of component file paths to include |
client |
object |
No | Client plugin configuration with helpers |
onPageSet |
function |
No | Hook for when a page is created |
onPageUpdate |
function |
No | Hook for when a page is updated |
onPageDelete |
function |
No | Hook for when a page is deleted |
onComponentSet |
function |
No | Hook for when a component is created |
onComponentUpdate |
function |
No | Hook for when a component is updated |
onComponentDelete |
function |
No | Hook for when a component is deleted |
onBeforePageRender |
function |
No | Hook called before rendering a page to a string |
onAfterPageRender |
function |
No | Hook called after rendering a page, before saving |
onBeforeBuild |
function |
No | Hook called at the start of the build process |
onAfterBuild |
function |
No | Hook called at the end of the build process |
Component-Level Plugins #
Component-level plugins provide methods that can be called inside defineComponent tokens and slots.
These run during the build process and have access to the component's context.
Plugin Method Context #
Plugin methods receive two parameters: options and context. The context contains:
| Property | Type | Description |
|---|---|---|
values |
Object |
Page metadata and element attributes |
document |
CoraliteComponent |
The HTML file being processed |
element |
CoraliteElement |
The specific element calling the method |
path |
Object |
File path information |
transform |
function |
Exposed transform method for serialization control and rendering logic |
Using Plugin Methods in Components #
Import plugin methods from coralite/plugins and use them inside defineComponent
tokens:
<template id="user-profile">
<div>
<h1 class="display">{{ name }}</h1>
<p>{{ bio }}</p>
<span>Joined: {{ joinDate }}</span>
</div>
</template>
<script type="module">
import { defineComponent } from 'coralite'
import { formatDate } from 'coralite/plugins'
export default defineComponent({
tokens: {
// Plugin method called with options
joinDate: (values) => formatDate({
format: 'long'
}, values.date)
}
})
</script>
Context Values Difference #
It's important to understand the difference between values passed to component scripts vs plugin methods:
<!-- Page: user.html -->
<head>
<meta name="name" content="Alice"></meta>
<title>User Profile</title>
</head>
<body>
<user-profile age="25"></user-profile>
</body>
// In defineComponent:
export default defineComponent({
tokens: {
// Plugin method receives:
// values = { $name: "Alice", $title: "User Profile", age: "25" }
userName: (values) => values.$name
},
script: (context, helpers) => {
// Component script receives:
// values = { userName: "Alice", age: 25 } (processed tokens)
}
})
Key Difference: Plugin methods get raw metadata/attributes, component scripts get processed token values.
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'
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)
}
}
}
})
}
Component-Level vs Page-Level #
| Feature | Component-Level | Page-Level |
|---|---|---|
| Registration | Imported in component script | Registered in coralite.config.js |
| Execution | Build time (per component) | Build time (per page) |
| Access | Inside defineComponent tokens | Global lifecycle hooks |
| Use Case | Data processing, token computation | HTML transformation, optimization |
| Context | Component context + element attributes | Full page context + DOM tree |
Lifecycle Hooks #
Hooks allow plugins to respond to specific events in the Coralite lifecycle:
const analyticsPlugin = definePlugin({
name: 'analytics',
onPageSet: async (data) => {
// Called when a new page is created
console.log('New page:', data.path.pathname)
await trackPageView(data.path.pathname)
},
onPageUpdate: async ({ elements, newValue, oldValue }) => {
// Called when a page is updated
console.log('Page updated:', newValue.path.pathname)
},
onPageDelete: async (value) => {
// Called when a page is deleted
console.log('Page deleted:', value.path.pathname)
},
onComponentSet: async (component) => {
// Called when a component is created
console.log('New component:', component.id)
},
onComponentUpdate: async (component) => {
// Called when a component is updated
console.log('Component updated:', component.id)
},
onComponentDelete: async (component) => {
// Called when a component is deleted
console.log('Component deleted:', component.id)
}
})
Client Plugins & Helpers #
Client plugins provide client-side helpers that are available in component scripts. To safely bridge global plugin configuration with local Web Component instances, Coralite requires client.helpers to be authored using a strict Two-Phase Currying System.
The Two-Phase Currying System #
- Phase 1 (Global Context): The outermost function receives the
globalContext(containingimportsand globalconfig). - Phase 2 (Local Instance Context): It must return a second function that receives the local Web Component instance context (
{ values, root, signal }). Here,rootis the component'sHTMLElement(or Document), andsignalis anAbortSignal. - Phase 3 (Execution): It finally returns the actual callable utility function used by the user's script in the browser.
Note on signal: The signal is tied natively to the component's mount lifecycle. It triggers .abort() automatically when an imperative component is unmounted from the DOM, making it perfect for cleaning up event listeners (e.g. element.addEventListener('click', fn, { signal })). For declarative components, signal is null.
import { definePlugin } from 'coralite/plugins'
export const myCustomPlugin = definePlugin({
name: 'my-custom-plugin',
client: {
helpers: {
// Phase 1: Receives Global Context
myHelper(globalContext) {
// Phase 2: Receives Local Instance Context
return ({ values, root, signal }) => {
// Phase 3: The actual function accessible via context.helpers.myHelper
return function (selector) {
// The plugin queries elements from the local component root
const element = root.querySelector(selector);
// Example using the signal to prevent memory leaks
if (signal) {
element.addEventListener('click', () => console.log('clicked'), { signal });
}
return element;
}
}
}
}
}
})
Usage in Components #
Once injected, the user interacts strictly with the Phase 3 Execution function.
// In component script:
export default defineComponent({
script: (context) => {
const { helpers } = context;
// helpers.myHelper is already curried with Global and Local Context
const btn = helpers.myHelper('.btn-class');
}
})
Built-in Plugins #
Coralite includes three built-in plugins that provide core functionality:
defineComponent #
The defineComponent plugin is required for all dynamic components. It provides the wrapper for
tokens, slots, and script execution:
import { defineComponent } from 'coralite'
export default defineComponent({
tokens: {
// Computed tokens that run at build time
fullName: ({ firstName, lastName }) => `${firstName} ${lastName}`,
formattedDate: ({ date }) => new Date(date).toLocaleDateString()
},
slots: {
// Custom slot processing
content: (slotNodes, values) => {
// Transform slot content based on values
return slotNodes
}
},
script: ({ values, refs }) => {
// This function runs in the client-side
// It has access to computed tokens and can use refs
const btn = refs('actionBtn');
btn.addEventListener('click', () => {
console.log('Hello!');
});
}
})
When to Use #
- When your component needs computed tokens
- 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 script processing
refs Helper #
The refs helper provides DOM element access at runtime using the factory pattern:
<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({
script: (context, helpers) => {
// helpers.refs is the resolver function (factory was executed)
const refs = helpers.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'
})
}
})
</script>
How refs Works #
- Factory Definition:
refs({ refs })receives context and returns resolver - Factory Execution: ScriptManager calls
refs(context)automatically - Helper Injection: Result is injected as
helpers.refs - Component Usage: Scripts use
helpers.refsdirectly - DOM Access: Queries elements with generated unique
idattributes
Plugin Registration #
The refs helper is automatically available when you import the refs plugin:
// In your coralite.config.js
import { refsPlugin } from 'coralite/plugins'
export default {
plugins: [
refsPlugin
// Other plugins...
]
}
Metadata Plugin #
The metadata plugin automatically processes page metadata for use in components:
- Scans
<head>for<meta>tags - Exposes values as
meta_namevariables - Supports dynamic metadata generation via components
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 bothpkgandpathproperties. 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 all built-in plugins working together:
<template id="counter">
<div class="counter">
<h2>Count: {{ count }}</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({
tokens: {
// Computed token
count: ({ initial }) => parseInt(initial) || 0
},
script: (context, helpers) => {
// Use refs helper
const refs = helpers.refs
const increment = refs('increment')
const decrement = refs('decrement')
let count = context.values.count
increment.addEventListener('click', () => {
count++
document.querySelector('h2').textContent = `Count: ${count}`
})
decrement.addEventListener('click', () => {
count--
document.querySelector('h2').textContent = `Count: ${count}`
})
}
})
</script>