feat(ui): scaffold the codegraph ui viewer as a Svelte 5 + Vite workspace (CG-40)

Adds `ui/` as an npm workspace (Svelte 5.56 + Vite 7, devDependencies only —
the engine's runtime dependencies are untouched) and chains its build into
`npm run build`, so the browser viewer ships inside `dist/` with everything
else: `build-bundle.sh` already copies `dist` wholesale and `pack-npm.sh`
packs that bundle.

Output is `dist/viewer/`, NOT `dist/ui/`: `src/ui/` is the engine's terminal
ui (shimmer progress + its worker) and tsc compiles it to `dist/ui/`, so
emitting there both deletes those modules — the CLI then dies at startup with
`Cannot find module '../ui/shimmer-progress'` — and would leave the static
server handing out compiled engine internals. The design spec is corrected to
match.

`scripts/check-ui-build.mjs` is the release guard: index.html must exist, be
non-trivial, and every local asset it references must be on disk, and the
compiled engine next door must still be intact. It runs after every UI build,
again in `build-bundle.sh` once the bundle stage has copied `dist`, and again
in `pack-npm.sh` once each archive is unpacked — so a broken viewer fails the
release instead of shipping a CLI that serves a 404.

`vite build` does not override an ambient NODE_ENV, so a shell or runner with
NODE_ENV=development silently shipped dev-mode Svelte (~13 kB of dev-only
runtime checks, warning in the user's console). The config now pins production
for `command === 'build'`; macOS and Windows ARM64 then emit byte-identical
bundle hashes.

The shell itself follows docs/design/codegraph-ui-design-spec.md §2–§3.1:
design tokens as CSS custom properties (light on bare `:root`, dark under both
`prefers-color-scheme` and `[data-theme="dark"]`), square corners, hairline
rules, one oxblood accent; top bar 48px / trail bar 34px / main; a hash router
over `#/s/<id>`, `#/file/<path>`, with `#/map` and `#/flow` reserved for phase
2. Fonts are vendored through @fontsource rather than fetched, so a local
reader works offline and never announces the project to a CDN.

Verified: clean `npm run build` from an empty dist on macOS and on the Windows
ARM64 VM (forward-slash asset URLs, CLI still starts, both assertion failure
modes exit 1); `dist/viewer` present in a real darwin-arm64 bundle and in the
packed npm platform package; shell geometry, tokens, all seven routes, both
themes and font loading checked in headless Chromium with no console errors;
`npm test` unaffected.
This commit is contained in:
Colby McHenry
2026-08-26 15:55:10 -05:00
parent 6a056ec5db
commit a72f22a6d3
27 changed files with 2970 additions and 4 deletions
+47
View File
@@ -0,0 +1,47 @@
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
// The viewer is emitted straight into the engine's `dist/` tree so it ships
// with everything else: `build-bundle.sh` copies `dist` wholesale and
// `pack-npm.sh` packs that bundle, so nothing extra has to be taught about it.
//
// NOT `dist/ui` — that name is already taken. `src/ui/` is the engine's
// TERMINAL ui (shimmer progress + its worker) and tsc compiles it to
// `dist/ui/`, so emitting here would both clobber it (emptyOutDir) and, worse,
// leave the CLI serving compiled engine internals as static files.
//
// `fileURLToPath` (not a bare '../dist/viewer') keeps this a native path on
// Windows, where Rollup resolves outDir against the platform separator.
const outDir = fileURLToPath(new URL('../dist/viewer', import.meta.url));
export default defineConfig(({ command }) => {
// `vite build` does NOT override an ambient NODE_ENV, and Svelte compiles in
// dev mode when it sees one — a shell (or a CI runner) with
// NODE_ENV=development silently ships a viewer carrying Svelte's dev-only
// runtime checks: ~13 kB larger, slower, and warning in the user's console.
// A release artifact must not depend on the machine that built it.
if (command === 'build') process.env.NODE_ENV = 'production';
return {
plugins: [svelte()],
// Relative asset URLs: the CLI serves this at '/', but a relative base also
// survives being opened from the filesystem or mounted under a sub-path.
base: './',
build: {
// Scoped to dist/viewer — `emptyOutDir` must never be allowed to widen
// to dist/, which holds the compiled engine tsc wrote moments earlier.
outDir,
emptyOutDir: true,
target: 'es2022',
// A localhost reader has the sources on disk already; sourcemaps would
// double the bundle in every platform archive for no one's benefit.
sourcemap: false,
chunkSizeWarningLimit: 1024,
},
server: {
host: '127.0.0.1',
port: 5174,
},
};
});