PAD + islands

From source to page

What {vite 'src/main.js'} writes for this request and why: the manifest the build left, the chunks of each island, and the dev server that takes over while it runs.

{vite}$padViteBuildbuild/hot type=modulemodulepreload
Live
The build: www/islands/build/

Start npm run dev in _frontend/ and reload: {vite} then asks the dev server instead.

What {vite 'src/main.js'} wrote into the head
<script type="module" src="/pad/islands/build/assets/main-2Gb33P07.js"></script>
The server's time, ticking in a Solid island
-
The manifest - every source and what it became
SourceFileKindCSS
src/adapters/preact.jsassets/preact-IvGl7Sg9.jsloaded on demand
src/adapters/react.jsassets/react-DMbKTkY7.jsloaded on demand
src/adapters/solid.jsassets/solid-DNc0Ypxi.jsloaded on demand
src/adapters/svelte.jsassets/svelte-D94bdpAJ.jsloaded on demand
src/adapters/vue.jsassets/vue-D2b7WoT0.jsloaded on demand
src/islands/CartBadge.svelteassets/CartBadge-Da_ed-6b.jsloaded on demandassets/CartBadge-Bv7sRs5B.css
src/islands/CartSummary.vueassets/CartSummary-Cp-v3eII.jsloaded on demandassets/CartSummary-F7s383VX.css
src/islands/Clock.solid.jsxassets/Clock.solid-Ct-gs6od.jsloaded on demand
src/islands/Counter.preact.jsassets/Counter.preact-v7abP-yK.jsloaded on demand
src/islands/Counter.react.jsxassets/Counter.react-BFt9JEJ7.jsloaded on demand
src/islands/Counter.solid.jsxassets/Counter.solid-B_zGEf73.jsloaded on demand
src/islands/Counter.svelteassets/Counter-Batfu1wT.jsloaded on demand
src/islands/Counter.vueassets/Counter-h07JzQ8B.jsloaded on demand
src/islands/LoadLog.preact.jsassets/LoadLog.preact-CIBY1b39.jsloaded on demand
src/islands/OrderCard.react.tsxassets/OrderCard.react-C735Z2LI.jsloaded on demand
src/islands/Shelf.react.jsxassets/Shelf.react-DiwPuzdd.jsloaded on demand
src/main.jsassets/main-2Gb33P07.jsentry

How it is built

PAD, on the server the islands, built by Vite

<div class="cols-2">
  <div>
    <div class="panel-title">{if $dev}The dev server runs: {$dev}{else}The build: www/islands/build/{/if}</div>
    <p class="muted small">{if $dev}{open}vite{close} asks the dev server - every save shows at once.{else}Start <code>npm run dev</code> in <code>_frontend/</code> and reload: {open}vite{close} then asks the dev server instead.{/if}</p>
    <div class="panel-title" style="margin-top: 18px">What {open}vite 'src/main.js'{close} wrote into the head</div>
    {echo $written | highlight('html')}
  </div>
  <div>
    <div class="panel-title">The server's time, ticking in a Solid island</div>
    <div class="clock-box" data-island="Clock" data-props="{^clock}"><span class="clock">-</span></div>
  </div>
</div>

{if $sourceCount}
<div class="panel-title" style="margin-top: 26px">The manifest - every source and what it became</div>
<table class="table">
  <thead><tr><th>Source</th><th>File</th><th>Kind</th><th>CSS</th></tr></thead>
  <tbody>
    {sources}<tr><td><code>{$source}</code></td><td><code>{$file}</code></td><td>{$kind}</td><td><code>{$css}</code></td></tr>{/sources}
  </tbody>
</table>
{/if}
<?php

  // What {vite 'src/main.js'} wrote into this page's <head>, and where it came from: the
  // manifest of the build - every source and the file it became - or the dev server, while it
  // runs. The clock is a Solid island started from the server's time.

  $dev      = padViteDev ();
  $manifest = $dev === '' ? ( padViteManifest () ?? [] ) : [];
  $written  = trim ( padViteTags ( [ 'src/main.js' ], FALSE, FALSE ) );

  $sources = [];

  foreach ( $manifest as $source => $one )
    if ( ! str_starts_with ( $source, '_' ) )
      $sources [] = [ 'source' => $source,
                      'file'   => $one ['file'],
                      'kind'   => ( $one ['isEntry'] ?? FALSE ) ? 'entry' : ( ( $one ['isDynamicEntry'] ?? FALSE ) ? 'loaded on demand' : 'chunk' ),
                      'css'    => implode ( ', ', $one ['css'] ?? [] ) ];

  $sourceCount = count ( $sources );
  $clock       = [ 'now' => time () ];

?>
// The build of the islands application: the components of every framework in src/, made
// into www/islands/build/ with a manifest - {vite 'src/main.js'} in the templates reads it.
//
//   npm install
//   npm run build      www/islands/build/ - what the pages load
//   npm run dev        the dev server: its address goes into build/hot, and {vite} asks it
//
// Each framework compiles its own files: React *.react.jsx/tsx, Solid *.solid.jsx, Vue
// *.vue, Svelte *.svelte; Preact needs no compiler here - its components are written with
// htm. PAD_ISLANDS_WWW names www/islands/ when the project is built from a copy elsewhere.

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import vue from '@vitejs/plugin-vue';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import solid from 'vite-plugin-solid';

const here = path.dirname(fileURLToPath(import.meta.url));
const www = process.env.PAD_ISLANDS_WWW || path.resolve(here, '../../../www/islands');
const outDir = path.join(www, 'build');

// The dev server's address in build/hot while it runs, gone when it stops: {vite} asks the
// dev server while the file is there, and the build when it is not.

function padHot() {
  const hot = path.join(outDir, 'hot');
  const clear = () => fs.rmSync(hot, { force: true });
  return {
    name: 'pad-hot',
    apply: 'serve',
    configureServer(server) {
      server.httpServer?.once('listening', () => {
        const { port } = server.httpServer.address();
        fs.mkdirSync(outDir, { recursive: true });
        fs.writeFileSync(hot, `http://localhost:${port}`);
      });
      // however the dev server stops - Ctrl-C, a kill, its own exit - the file goes with it
      server.httpServer?.once('close', clear);
      process.once('exit', clear);
      for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'])
        process.once(signal, () => process.exit());
    }
  };
}

export default defineConfig({
  root: here,
  base: './',
  plugins: [
    react({ include: /\.react\.[jt]sx$/ }),
    solid({ include: /\.solid\.jsx$/ }),
    vue(),
    svelte(),
    padHot()
  ],
  build: {
    outDir,
    emptyOutDir: true,
    manifest: true,
    rollupOptions: { input: [ 'src/main.js' ] }
  },
  server: { port: 5173, strictPort: true, cors: true }
});
{
  "name": "pad-islands",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "types": "cd ../../.. && php apps/cli/pad types islands examples/typed --out=apps/islands/_frontend/src/types/pad.d.ts",
    "check": "tsc --noEmit"
  },
  "devDependencies": {
    "vite": "^6",
    "@vitejs/plugin-react": "^4",
    "@vitejs/plugin-vue": "^5",
    "@sveltejs/vite-plugin-svelte": "^5",
    "vite-plugin-solid": "^2",
    "typescript": "^5",
    "@types/react": "^18.3",
    "@types/react-dom": "^18.3"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "vue": "^3.5",
    "svelte": "^5",
    "preact": "^10",
    "htm": "^3",
    "solid-js": "^1.9"
  }
}
// A clock in Solid, started from the server's time: only the text node changes each second.
import { createSignal, onCleanup } from 'solid-js';

export default function Clock(props) {
  const offset = props.now * 1000 - Date.now();
  const [now, setNow] = createSignal(new Date(Date.now() + offset));
  const timer = setInterval(() => setNow(new Date(Date.now() + offset)), 1000);
  onCleanup(() => clearInterval(timer));
  return <span class="clock">{now().toLocaleTimeString()}</span>;
}