UNPKG

@kalxjs/core

Version:

A modern JavaScript framework for building user interfaces with reactive state, composition API, and built-in performance optimizations

342 lines (297 loc) 9.47 kB
/** * @kalxjs/core - Islands Architecture * Implements Astro/Qwik-style islands for shipping minimal JavaScript * * Features: * - Isolate interactive components * - Zero JS for static content * - Resumability pattern * - Fine-grained lazy loading * - Automatic code splitting per island * * @module @kalxjs/core/islands */ import { defineAsyncComponent } from '../component/dynamic/index.js'; import { hydrate } from '../ssr/selective-hydration.js'; /** * Island component registry */ const islandRegistry = new Map(); let islandIdCounter = 0; /** * Define an island component * Islands are interactive components that hydrate independently * * @param {object} component - Component definition * @param {object} options - Island options * @returns {object} - Island component * * @example * ```js * const Counter = defineIsland({ * name: 'Counter', * render() { * return h('button', { onClick: () => count.value++ }, count.value); * } * }, { * when: 'interaction', // When to hydrate: 'load', 'idle', 'visible', 'interaction' * only: 'client' // Where to render: 'client', 'server', 'both' * }); * ``` */ export function defineIsland(component, options = {}) { const { when = 'load', // Hydration strategy only = 'both', // Rendering strategy props = null, // Static props clientOnly = false, serverOnly = false } = options; // Generate unique island ID const islandId = `island-${++islandIdCounter}`; console.log(`[islands] Defining island: ${component.name || islandId}`); console.log(` - Hydration: ${when}`); console.log(` - Rendering: ${only}`); // Register island islandRegistry.set(islandId, { component, options, when, only }); // Create island wrapper component const islandComponent = { name: `Island_${component.name || islandId}`, __isIsland: true, __islandId: islandId, setup(props, context) { // Check if we're on server const isServer = typeof window === 'undefined'; // Server-side rendering if (isServer) { if (only === 'client' || serverOnly) { // Don't render on server return () => createIslandPlaceholder(islandId, props); } // Render component on server if (component.setup) { return component.setup(props, context); } return component.render ? component.render.bind(component) : null; } // Client-side hydration if (only === 'server' || clientOnly) { // Already hydrated, just return return component.setup ? component.setup(props, context) : null; } // Apply hydration strategy return setupIslandHydration(component, islandId, when, props, context); }, render: component.render }; return islandComponent; } /** * Create placeholder for island (SSR) */ function createIslandPlaceholder(islandId, props) { return { type: 'div', props: { 'data-island-id': islandId, 'data-island-props': JSON.stringify(props || {}), style: 'display: contents;' }, children: [] }; } /** * Setup island hydration on client */ function setupIslandHydration(component, islandId, when, props, context) { // Find island element const islandEl = document.querySelector(`[data-island-id="${islandId}"]`); if (!islandEl) { console.warn(`[islands] Island element not found: ${islandId}`); return component.setup ? component.setup(props, context) : null; } // Determine when to hydrate switch (when) { case 'load': // Hydrate immediately hydrateIsland(islandEl, component, props, context); break; case 'idle': // Hydrate when browser is idle if ('requestIdleCallback' in window) { requestIdleCallback(() => { hydrateIsland(islandEl, component, props, context); }); } else { setTimeout(() => hydrateIsland(islandEl, component, props, context), 200); } break; case 'visible': // Hydrate when island becomes visible const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { hydrateIsland(islandEl, component, props, context); observer.disconnect(); } }); }); observer.observe(islandEl); break; case 'interaction': // Hydrate on first interaction const events = ['click', 'mouseenter', 'focus', 'touchstart']; const hydrateOnEvent = () => { hydrateIsland(islandEl, component, props, context); events.forEach(event => { islandEl.removeEventListener(event, hydrateOnEvent); }); }; events.forEach(event => { islandEl.addEventListener(event, hydrateOnEvent, { once: true, passive: true }); }); break; default: hydrateIsland(islandEl, component, props, context); } return component.setup ? component.setup(props, context) : null; } /** * Hydrate an island */ function hydrateIsland(element, component, props, context) { console.log('[islands] Hydrating island:', element.dataset.islandId); try { // Parse serialized props const serializedProps = element.dataset.islandProps; const hydratedProps = serializedProps ? JSON.parse(serializedProps) : props; // Create component instance const instance = component.setup ? component.setup(hydratedProps, context) : null; // Mount component if (instance && typeof instance === 'function') { const vnode = instance(); // TODO: Actually render and patch the DOM // This would integrate with the VDOM system } // Mark as hydrated element.dataset.islandHydrated = 'true'; console.log('[islands] Island hydrated successfully'); } catch (error) { console.error('[islands] Hydration error:', error); } } /** * Static island - never hydrates, pure HTML * Perfect for content that never changes */ export function defineStaticIsland(component) { return defineIsland(component, { when: 'never', only: 'server' }); } /** * Client-only island - only renders on client * Useful for components that depend on browser APIs */ export function defineClientIsland(component, when = 'load') { return defineIsland(component, { when, only: 'client' }); } /** * Interactive island - hydrates on interaction * Great for components like modals, dropdowns */ export function defineInteractiveIsland(component) { return defineIsland(component, { when: 'interaction', only: 'both' }); } /** * Visible island - hydrates when scrolled into view * Perfect for below-the-fold content */ export function defineVisibleIsland(component) { return defineIsland(component, { when: 'visible', only: 'both' }); } /** * Get all registered islands */ export function getRegisteredIslands() { return Array.from(islandRegistry.entries()).map(([id, data]) => ({ id, name: data.component.name, when: data.when, only: data.only })); } /** * Serialize islands for SSR * Generates script tags to hydrate islands on client */ export function serializeIslands() { const islands = getRegisteredIslands(); if (islands.length === 0) { return ''; } let script = '<script type="module">\n'; script += '// KALXJS Islands Hydration\n'; script += `window.__KALXJS_ISLANDS__ = ${JSON.stringify(islands)};\n`; script += '</script>\n'; return script; } /** * Auto-split code per island * Each island gets its own JS chunk */ export function createIslandBundle(islandId) { const island = islandRegistry.get(islandId); if (!island) { throw new Error(`Island not found: ${islandId}`); } // Return dynamic import return { id: islandId, chunkName: `island-${islandId}`, loader: () => Promise.resolve(island.component) }; } /** * Zero JS optimization - strip all JS from static islands */ export function optimizeStaticIslands(html) { // Remove scripts from static-only islands return html.replace( /<div data-island-id="[^"]+" data-island-only="server"[^>]*>[\s\S]*?<\/div>/g, (match) => { // Remove event handlers and other JS artifacts return match .replace(/on\w+="[^"]*"/g, '') .replace(/data-v-[a-f0-9]+/g, ''); } ); } export default { defineIsland, defineStaticIsland, defineClientIsland, defineInteractiveIsland, defineVisibleIsland, getRegisteredIslands, serializeIslands, createIslandBundle, optimizeStaticIslands };