Islands of any framework

PAD writes the page and marks the islands; one small runtime mounts a component of any framework on each; Vite builds the components and {vite} links the build.

1. The island, in the template

An element names its component and carries its props - the ^ sigil writes a field as JSON for the attribute - and data-load says when its code is fetched. What stands inside shows until the component takes over.

<div data-island="VueCounter" data-load="visible" data-props="{^counter}">
  <p>{$counter.start}</p>
</div>

{vite 'src/main.js'}

2. The registry, in the build

_frontend/src/main.js names every island and pairs it with its framework. Both are fetched by import() when an island of the name is due - a page without a Vue island loads no Vue:

// The islands of every page, by the name a template gives them in data-island. Each entry
// pairs a component with its framework; nothing of either is fetched before an island of
// that name is on the page and due to load (pad-islands.js).

import './theme.js';
import { islands, react, vue, svelte, preact, solid } from './pad-islands.js';

islands({
  ReactCounter:  react(()  => import('./islands/Counter.react.jsx')),
  VueCounter:    vue(()    => import('./islands/Counter.vue')),
  SvelteCounter: svelte(() => import('./islands/Counter.svelte')),
  PreactCounter: preact(() => import('./islands/Counter.preact.js')),
  SolidCounter:  solid(()  => import('./islands/Counter.solid.jsx')),

  Shelf:         react(()  => import('./islands/Shelf.react.jsx')),
  CartSummary:   vue(()    => import('./islands/CartSummary.vue')),
  CartBadge:     svelte(() => import('./islands/CartBadge.svelte')),

  OrderCard:     react(()  => import('./islands/OrderCard.react.tsx')),

  LoadLog:       preact(() => import('./islands/LoadLog.preact.js')),
  Clock:         solid(()  => import('./islands/Clock.solid.jsx'))
});

3. An adapter per framework

An adapter is a dozen lines: it mounts a component on an element with props and answers how to unmount it. Each is a module of its own, so it travels with its framework:

import { mount, unmount } from 'svelte';

export const name = 'Svelte';

export function island(Component) {
  return (element, props) => {
    element.innerHTML = '';
    const instance = mount(Component, { target: element, props });
    return () => unmount(instance);
  };
}

4. Talking to PAD, and to each other

pad-islands.js also exports get(page, params) - the JSON of a page's $padExpose - post(page, body) with the session's CSRF token, and store(name, initial): state any framework subscribes to. A React island writes it, a Vue and a Svelte island show it.

5. Build and dev server

cd apps/islands/_frontend
npm install
npm run build     # www/islands/build/ with .vite/manifest.json
npm run dev       # the dev server; build/hot holds its address

{vite 'src/main.js'} reads the manifest and writes the entry's CSS, its chunks as modulepreload and its module script. While npm run dev runs, a hook of vite.config.js writes the dev server's address into build/hot, and {vite} asks the dev server instead - a save in the editor shows at once.

Each framework compiles its own files: *.react.jsx, *.solid.jsx, *.vue, *.svelte. Preact needs no compiler - its components are written with htm.