do-islands
v0.0.1-alpha.1
Published
A Vite plugin that enables Islands Architecture for Cloudflare Durable Objects, allowing you to return server-rendered HTML with selectively hydrated interactive components.
Readme
DO Islands Plugin
A Vite plugin that enables Islands Architecture for Cloudflare Durable Objects, allowing you to return server-rendered HTML with selectively hydrated interactive components.
Features
- 🏝️ Islands Architecture - Ship only the JavaScript needed for interactive components
- 🚀 Automatic Discovery - Automatically finds and bundles components in your islands directory
- ⚡ Optimized Bundles - Creates separate bundles per island with shared React chunk
- 🔥 HMR Support - Full hot module replacement during development
- 🎯 Zero Config - Works out of the box with sensible defaults
- 🏗️ SSR Ready - Built for Cloudflare Workers and Durable Objects
Installation
npm install do-islands-pluginQuick Start
1. Add the plugin to your Vite config
// vite.config.ts
import { defineConfig } from 'vite';
import { cloudflare } from '@cloudflare/vite-plugin';
import { islandPlugin } from 'do-islands-plugin';
export default defineConfig({
plugins: [
cloudflare(),
islandPlugin()
],
});2. Create island components
// src/islands/Counter.tsx
import * as React from 'react';
const { useState } = React;
interface CounterProps {
initialValue?: number;
}
export default function Counter({ initialValue = 0 }: CounterProps) {
const [count, setCount] = useState(initialValue);
return (
<div className="counter">
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}3. Use in your Durable Object
// src/my-durable-object.tsx
import { DurableObject } from 'cloudflare:workers';
import * as React from 'react';
import { renderToString } from 'react-dom/server';
import Counter from './islands/Counter';
export class MyDurableObject extends DurableObject {
async fetch(request: Request) {
// Server-side render the component
const html = renderToString(<Counter initialValue={10} />);
// Wrap in island marker
const islandHtml = `
<do-island component-id="counter" data-serialized-props='{"initialValue":10}'>
${html}
</do-island>
`;
// Return HTML page with island
return new Response(`
<!DOCTYPE html>
<html>
<body>
<h1>My Page</h1>
${islandHtml}
<script type="module" src="/island-bundle/counter.js"></script>
</body>
</html>
`, {
headers: { 'Content-Type': 'text/html' }
});
}
}4. Build and deploy
# Build client bundles
npm run build -- --mode client
# Build worker
npm run build
# Deploy
wrangler deployHow It Works
- Server Rendering: Your Durable Object renders React components to HTML using
renderToString - Island Markers: Components are wrapped in
<do-island>elements with serialized props - Selective Hydration: Only islands are hydrated on the client, keeping bundle sizes small
- Automatic Bundling: The plugin creates optimized bundles for each island component
Configuration
islandPlugin({
// Directory containing island components (default: 'src/islands')
islandsDir: 'src/components/islands',
// Output directory for client bundles (default: 'dist/client')
outputDir: 'dist/static'
})Advanced Usage
Using Decorators (Recommended)
Create a clean API with decorators for automatic route generation:
import { hydrate, generateRoutes } from './decorators';
export class MyDurableObject extends DurableObject {
private routes: Map<string, () => Promise<Response>>;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.routes = generateRoutes(this);
}
@hydrate({ route: '/counter', title: 'Counter Demo' })
async counter() {
const html = renderToString(<Counter initialValue={10} />);
const serialized = serializeIsland('counter', { initialValue: 10 }, html);
return createPageResponse(serialized.html, ['counter']);
}
async fetch(request: Request) {
const url = new URL(request.url);
const handler = this.routes.get(url.pathname);
return handler ? handler() : new Response('Not Found', { status: 404 });
}
}Multiple Islands on One Page
@hydrate({ route: '/dashboard', title: 'Dashboard' })
async dashboard() {
const counterHtml = renderToString(<Counter initialValue={5} />);
const timerHtml = renderToString(<Timer startTime={0} />);
const html = `
<div class="dashboard">
${serializeIsland('counter', { initialValue: 5 }, counterHtml).html}
${serializeIsland('timer', { startTime: 0 }, timerHtml).html}
</div>
`;
return createPageResponse(html, ['counter', 'timer']);
}Development
The plugin includes a development server middleware that serves island bundles with HMR support:
npm run devVisit your routes and see changes instantly without page reloads.
Production Considerations
Asset Serving
In production, serve the built assets from:
- Your Worker (for small bundles)
- Cloudflare R2/KV (for larger apps)
- A CDN (for best performance)
Example serving from Worker:
// src/index.tsx
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Serve island bundles
if (url.pathname.startsWith('/island-bundle/')) {
// In production, serve from R2, KV, or CDN
// For demo, you could embed the bundles
}
// Proxy to Durable Object
const id = env.MY_DURABLE_OBJECT.idFromName('singleton');
const stub = env.MY_DURABLE_OBJECT.get(id);
return stub.fetch(request);
}
}Bundle Size Optimization
The plugin automatically:
- Creates separate bundles per island
- Extracts React into a shared chunk
- Only loads JavaScript for islands on the current page
API Reference
Plugin Options
interface IslandPluginOptions {
// Directory to scan for island components
islandsDir?: string; // default: 'src/islands'
// Output directory for client bundles
outputDir?: string; // default: 'dist/client'
}Island Markup
<do-island
component-id="counter"
data-serialized-props='{"initialValue": 10}'
>
<!-- Server-rendered HTML -->
</do-island>Helper Functions
// Serialize an island with its props
serializeIsland(
componentId: string,
props: Record<string, any>,
serverHtml: string
): IslandResponse
// Create a page response with island tracking
createPageResponse(
html: string,
usedIslands: string[]
): PageResponseTroubleshooting
React Import Errors
Ensure all React imports use the namespace import:
import * as React from 'react';
const { useState, useEffect } = React;Islands Not Hydrating
Check that:
- The
component-idmatches the filename (lowercase) - Props are valid JSON in
data-serialized-props - The island bundle script is loaded
Build Errors
Make sure to build client bundles before the worker:
npm run build -- --mode client && npm run buildContributing
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
License
MIT
