Creating Custom Integrations
Integrations in Ecopages add support for new templating engines or frameworks. This guide will show you how to create your own integration.
Basic Structure
An integration extends the IntegrationPlugin abstract class and must provide a renderer class:
import { IntegrationPlugin } from '@ecopages/core/plugins/integration-plugin';
import { CustomRenderer } from './custom-renderer';
class CustomIntegration extends IntegrationPlugin {
renderer = CustomRenderer;
staticBuildStep = 'render'; // 'render' (default) or 'fetch'
constructor() {
super({
name: 'custom-integration',
extensions: ['.custom'],
});
}
}Creating a Custom Renderer
The renderer handles the actual transformation of your templates. It extends the IntegrationRenderer class:
import { IntegrationRenderer } from '@ecopages/core/route-renderer/integration-renderer';
import type { IntegrationRendererRenderOptions, RouteRendererBody, EcoComponent } from '@ecopages/core/public-types';
class CustomRenderer extends IntegrationRenderer {
name = 'custom-renderer';
async render(options: IntegrationRendererRenderOptions): Promise<RouteRendererBody> {
const { Page, props, metadata, HtmlTemplate } = options;
// Transform the page content
const pageContent = await this.renderPage(Page, props);
// Render the final HTML using the template
return HtmlTemplate({
children: pageContent,
metadata,
});
}
private async renderPage(Page: EcoComponent, props: any): Promise<string> {
// Implement your rendering logic here
// For example, if Page is a function that returns a string:
return Page(props);
}
}Handling Dependencies
Manage assets and dependencies for your integration using the integrationDependencies array in the constructor:
import { AssetFactory } from '@ecopages/core/services/asset-processing-service';
class CustomIntegration extends IntegrationPlugin {
renderer = CustomRenderer;
constructor() {
super({
name: 'custom-integration',
extensions: ['.custom'],
integrationDependencies: [
AssetFactory.createFileScript({
importPath: './runtime.js',
position: 'head',
}),
AssetFactory.createFileStylesheet({
filepath: './styles.css',
}),
],
});
}
}Hot Module Replacement (HMR)
To support HMR in your custom integration, you need to implement an HmrStrategy and register it in your plugin.
Creating an HMR Strategy
The strategy determines how file changes are handled. It should identify matches and process the updates:
import {
HmrStrategy,
HmrStrategyType,
type HmrAction,
type DefaultHmrContext,
} from '@ecopages/core';
class CustomHmrStrategy extends HmrStrategy {
readonly type = HmrStrategyType.INTEGRATION;
constructor(private context: DefaultHmrContext) {
super();
}
// Determine if this strategy should handle the changed file
matches(filePath: string): boolean {
return filePath.endsWith('.custom');
}
// Process the change and return an action
async process(filePath: string): Promise<HmrAction> {
// Get the public URL for the changed file (if registered)
const watchedFiles = this.context.getWatchedFiles();
const outputUrl = watchedFiles.get(filePath) || filePath;
// You can perform rebuilds or code transformations here
// ...
return {
type: 'broadcast',
events: [
{
type: 'update',
path: outputUrl,
timestamp: Date.now(),
},
],
};
}
}Registering the Strategy
Override getHmrStrategy in your IntegrationPlugin to return your custom strategy:
class CustomIntegration extends IntegrationPlugin {
renderer = CustomRenderer;
override getHmrStrategy() {
if (!this.hmrManager) return undefined;
return new CustomHmrStrategy(this.hmrManager.getDefaultContext());
}
// You can also register specifier mappings for the browser
override setHmrManager(hmrManager: IHmrManager) {
super.setHmrManager(hmrManager);
hmrManager.registerSpecifierMap({
'custom-pkg': '/assets/vendors/custom-pkg-esm.js',
});
}
}Setup and Teardown
Handle initialization and cleanup of resources needed by your integration:
class CustomIntegration extends IntegrationPlugin {
renderer = CustomRenderer;
async setup(): Promise<void> {
await super.setup();
// Additional initialization logic
// e.g., Bun.plugin(customCompilerPlugin())
}
async teardown(): Promise<void> {
// Clean up resources when the process stops
}
}Using the Integration
Register your integration in the Ecopages configuration:
import { ConfigBuilder } from '@ecopages/core';
import { CustomIntegration } from './custom-integration';
const customIntegration = new CustomIntegration();
const config = await new ConfigBuilder().setIntegrations([customIntegration]).build();Best Practices
- Follow Single Responsibility Principle: Keep your renderer focused on transforming templates.
- Type Safety: Use TypeScript interfaces for props and state.
- Error Handling: Provide descriptive errors during rendering and HMR.
- Efficiency: Use
HmrStrategyto only update what's necessary. Avoid unnecesary rebuilds. - Caching: If your rendering or transformation is expensive, implement a caching mechanism.
- Graceful Degradation: Fall back to full page reload if a hot update cannot be performed safely.
Example: Complete Integration
Here's a complete example including the plugin, renderer, and HMR strategy:
import { IntegrationPlugin, IntegrationRenderer, HmrStrategy, HmrStrategyType } from '@ecopages/core';
import type {
IntegrationRendererRenderOptions,
RouteRendererBody,
EcoComponent,
HmrAction,
DefaultHmrContext,
} from '@ecopages/core/public-types';
class CustomRenderer extends IntegrationRenderer {
name = 'custom-renderer';
async render(options: IntegrationRendererRenderOptions): Promise<RouteRendererBody> {
const { Page, props, metadata, HtmlTemplate } = options;
return HtmlTemplate({
children: Page(props),
metadata,
});
}
}
class CustomHmrStrategy extends HmrStrategy {
readonly type = HmrStrategyType.INTEGRATION;
constructor(private context: DefaultHmrContext) {
super();
}
matches(filePath: string) {
return filePath.endsWith('.custom');
}
async process(filePath: string): Promise<HmrAction> {
const outputUrl = this.context.getWatchedFiles().get(filePath) || filePath;
return {
type: 'broadcast',
events: [{ type: 'update', path: outputUrl, timestamp: Date.now() }],
};
}
}
export class CustomIntegration extends IntegrationPlugin {
renderer = CustomRenderer;
constructor() {
super({
name: 'custom-integration',
extensions: ['.custom'],
});
}
override getHmrStrategy() {
return this.hmrManager
? new CustomHmrStrategy(this.hmrManager.getDefaultContext())
: undefined;
}
}