hyperhive/frontend/packages/dashboard/build.mjs
iris e14887164b jobq-graph: author JobqGraph in real JSX, not hand-written h() calls
Mara on PR#3315: "shouldnt the pattern be that the old dashboard has a
dep on preact and has a preact instance running for the jobq view?
then we could get rid of a lot of extra plumbing" - right: the plain
h() authoring existed only to dodge adding JSX support to the
dashboard's esbuild config, and that dodge is exactly the plumbing to
remove now that the dashboard already depends on preact.

- JobqGraph.js -> JobqGraph.jsx, rewritten in real JSX.
- dashboard/build.mjs: added jsx: 'automatic', jsxImportSource: 'preact'
  to the JS-bundle esbuild call (esbuild already picks the jsx loader
  for .jsx by extension; this just sets the transform mode, matching
  swarm-ui's config). No other entry in that bundle uses JSX today.
- shared/package.json: export target updated to the .jsx file.

The CSS-as-page-level-@import structure is unchanged and stays that
way regardless of JSX: dashboard bundles this component transitively
through one esbuild call whose .css loader is 'text' (for the
shadow-DOM components that need their CSS as a literal string), and
esbuild's loader map is global per call, not per-module - importing
CSS from this component would silently pick up that loader too.
Explained in the file's own top comment.

Verified: npm run build (whole workspace) and npm run typecheck
(swarm-ui) clean. Re-ran the same headless-chromium screenshot against
a mock GET /api/jobq/graph payload as the previous verification -
pixel-identical to the h()-based version, confirming this is a pure
authoring-style refactor with no behavior change.
2026-08-16 15:18:25 +02:00

139 lines
7.2 KiB
JavaScript

// esbuild build for @hive/dashboard. Output layout (`dist/`):
//
// dist/index.html H0M3 menu hub — served by the Rust
// router at GET / (the landing page)
// dist/dashboard.html the operator dashboard SPA — served
// at GET /dashboard.html
// dist/flow.html served at GET /flow.html
// dist/logs.html served at GET /logs.html (AGENT/SYSTEM/AUDIT)
// dist/core.html served at GET /core.html (C0R3: kept
// state / container load)
// dist/builds.html served at GET /builds.html (BU1LDS:
// rebuild queue / meta inputs / build logs)
// dist/credentials.html served at GET /credentials.html
// (CR3D3NTIALS: matrix + github accounts)
// dist/static/home.js index.html (H0M3) entry — menu tiles +
// matrix-tile gating + identity line
// dist/static/tabs.js dashboard.html entry — tab renderers +
// tab routing + refreshState
// dist/static/flow.js /flow.html entry — broker terminal +
// @-mention composer
// dist/static/logs.js /logs.html entry — agent/system/audit
// log viewer sub-tabs
// dist/static/builds.js /builds.html entry — rebuild queue,
// meta inputs, build log history
// dist/static/credentials.js /credentials.html entry — matrix +
// github account sub-tabs
// dist/static/{home,tabs,flow,logs,builds,credentials}.js.map source map
// siblings
// dist/static/common.css loaded by every page (@hive/shared
// imports + shared typography/badges/
// buttons/inbox/side-panel)
// dist/static/dashboard.css dashboard.html only (dashboard-specific)
// dist/static/flow.css /flow.html only (flow chrome + composer)
// dist/static/logs.css /logs.html only (log viewer sub-tabs)
// dist/static/builds.css /builds.html only (build lifecycle hub)
// dist/static/home.css index.html (H0M3) only (tile grid)
// dist/static/credentials.css /credentials.html only (matrix + github
// account forms)
//
// Each JS entry bundles `./common.js` (DOM helpers, Panel singleton,
// NOTIF, path linkification) independently — esbuild inlines the shared
// module into each bundle rather than emitting a shared chunk (no
// `splitting: true`). The Rust binary mounts `dist/` as a
// `tower_http::ServeDir` fallback (`append_index_html_on_directories`
// serves index.html at /); the layout above keeps every URL the HTML
// files reference reachable without rewriting paths in the HTML. The
// dashboard SPA lives at /dashboard.html (a plain file, not a /dashboard/
// prefix) so it never shadows the exact-match /dashboard/stream +
// /dashboard/history SSE routes registered before the ServeDir fallback.
import { build } from 'esbuild';
import { mkdirSync, copyFileSync, rmSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
const src = (p) => resolve(here, 'src', p);
const dist = (p) => resolve(here, 'dist', p);
const staticDir = (p) => resolve(here, 'dist', 'static', p);
rmSync(dist(''), { recursive: true, force: true });
mkdirSync(staticDir(''), { recursive: true });
// Bundle the JS entries. ES-module output, browser target, no minify
// (line-aligned source aids debugging; minification belongs in a later
// follow-up once asset sizes warrant it). esbuild writes each entry
// to `static/<name>.js` based on the entryPoint basename.
await build({
entryPoints: [src('tabs.js'), src('flow.js'), src('logs.js'), src('home.js'), src('settings.js'), src('stats.js'), src('core.js'), src('builds.js'), src('credentials.js')],
outdir: staticDir(''),
bundle: true,
format: 'esm',
platform: 'browser',
target: ['es2022'],
sourcemap: true,
logLevel: 'info',
// `@hive/shared/modal.js` and `hive-btn.js` import their shadow-DOM
// component CSS (hive-dialog.css, hive-toast.css, hive-btn.css) as raw
// text via a plain `import css from './foo.css'` — the `text` loader turns
// that into a string constant at bundle time instead of erroring on an
// unrecognised extension. None of these JS entries import a `.css`
// file any other way, so this doesn't collide with the separate
// page-stylesheet bundling below (`loader: { '.css': 'css' }`), which
// runs as its own esbuild invocation over different entry points.
loader: { '.css': 'text' },
// `@hive/shared/jobq-graph.js` resolves to a real `.jsx` file
// (`JobqGraph.jsx`), pulled in transitively by `builds.js` — esbuild
// already picks the `jsx` loader for `.jsx` by extension, this just
// sets the transform mode to match swarm-ui's (which also authors
// this file). No other entry here uses JSX today; this doesn't turn
// any plain `.js` file into one, `.js` still parses as plain JS.
jsx: 'automatic',
jsxImportSource: 'preact',
});
// Stream-worker entry (#448). Lives in a separate bundle: SharedWorker
// scripts run in a different global (`self` is the worker scope, no
// `window`) so they can't be inlined into tabs.js / flow.js. Output is
// at `static/stream-worker.js`; common.js's `openStream` references
// `/static/stream-worker.js` as the SharedWorker URL. `format: 'iife'`
// matches the classic-script load (`new SharedWorker(url, name)` with
// no `{ type: 'module' }`); Firefox is the #448 target and module
// SharedWorker support there is patchy, so keeping the worker as a
// classic script + IIFE bundle is the compatible default. argus nit
// on #453: if a future contributor adds an `import` to this bundle,
// the IIFE format will surface it as a build error rather than
// silently shipping broken code.
await build({
entryPoints: [src('stream-worker.js')],
outdir: staticDir(''),
bundle: true,
format: 'iife',
platform: 'browser',
target: ['es2022'],
sourcemap: true,
logLevel: 'info',
});
// Bundle CSS — one entry per page. esbuild resolves @import including
// the package re-exports from @hive/shared. Each page loads colors.css
// (the standalone base16 palette — the theme swap contract, its own file
// so a swap replaces only it) + theme.css (the semantic derivation
// layer) + common.css (shared typography, badges, buttons, inbox, side
// panel) plus its own page-specific bundle.
for (const entry of ['colors.css', 'theme.css', 'common.css', 'dashboard.css', 'flow.css', 'logs.css', 'home.css', 'settings.css', 'stats.css', 'core.css', 'builds.css', 'credentials.css']) {
await build({
entryPoints: [src(entry)],
outfile: staticDir(entry),
bundle: true,
loader: { '.css': 'css' },
logLevel: 'info',
});
}
for (const html of ['index.html', 'dashboard.html', 'flow.html', 'logs.html', 'settings.html', 'stats.html', 'core.html', 'builds.html', 'credentials.html']) {
copyFileSync(src(html), dist(html));
}
console.log('dashboard build ok →', dist(''));