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:
@@ -63,8 +63,17 @@ echo "[bundle] building app"
|
||||
STAGE="$WORK/codegraph-${TARGET}"
|
||||
mkdir -p "$STAGE/lib" "$STAGE/bin"
|
||||
cp -R "$ROOT/dist" "$STAGE/lib/dist"
|
||||
# The browser viewer rides along inside dist/viewer (built by `npm run build`
|
||||
# above). Fail here rather than shipping a bundle whose `codegraph ui` serves
|
||||
# a 404 — the copy is verified, not assumed.
|
||||
node "$ROOT/scripts/check-ui-build.mjs" --root "$STAGE/lib"
|
||||
cp "$ROOT/package.json" "$ROOT/package-lock.json" "$STAGE/lib/"
|
||||
echo "[bundle] installing production dependencies"
|
||||
# The staged package.json declares the `ui` workspace but the bundle carries
|
||||
# no ui/ source — only its build output. That is fine: ui/ has dev
|
||||
# dependencies only, so --omit=dev skips the workspace outright and no link
|
||||
# is created. (If a future npm starts erroring on the absent folder, stage a
|
||||
# stub ui/package.json before this line rather than editing the lock.)
|
||||
( cd "$STAGE/lib" && npm ci --omit=dev --ignore-scripts >/dev/null 2>&1 )
|
||||
rm -f "$STAGE/lib/package-lock.json"
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Assert that the browser viewer actually built.
|
||||
*
|
||||
* `codegraph ui` serves dist/viewer/ as static files. If that tree is missing
|
||||
* or half-written, the CLI still starts and the browser gets a 404 — a failure
|
||||
* that would otherwise surface after the release is published. So the build
|
||||
* fails here instead: index.html must exist, be non-trivial, and every local
|
||||
* asset it references must be on disk next to it.
|
||||
*
|
||||
* It also re-asserts that the compiled engine is still there. The viewer build
|
||||
* empties its own output directory, and `dist/ui/` — the obvious name — is
|
||||
* where tsc puts the TERMINAL ui, so a mis-pointed outDir silently deletes
|
||||
* modules the CLI requires at startup.
|
||||
*
|
||||
* Usage: node scripts/check-ui-build.mjs [--root <dir>]
|
||||
* --root directory holding dist/ (default: the repo root). The release
|
||||
* bundler points this at its staging dir to verify the copy.
|
||||
*/
|
||||
import { existsSync, readFileSync, statSync } from 'node:fs';
|
||||
import { dirname, join, resolve, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const rootFlag = argv.indexOf('--root');
|
||||
const staged = rootFlag >= 0 && Boolean(argv[rootFlag + 1]);
|
||||
const root = staged
|
||||
? resolve(argv[rootFlag + 1])
|
||||
: resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
const viewerDir = join(root, 'dist', 'viewer');
|
||||
const indexHtml = join(viewerDir, 'index.html');
|
||||
|
||||
function fail(message, hint) {
|
||||
console.error(`[check-ui-build] ${message}`);
|
||||
if (hint) console.error(`[check-ui-build] ${hint}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(indexHtml)) {
|
||||
fail(
|
||||
`missing ${indexHtml}`,
|
||||
staged
|
||||
? 'this bundle predates the UI or was assembled from a stale archive — rebuild it with scripts/build-bundle.sh'
|
||||
: 'the UI workspace did not build — run `npm run build:ui` (or `npm ci` if ui/ has no node_modules)'
|
||||
);
|
||||
}
|
||||
|
||||
const html = readFileSync(indexHtml, 'utf8');
|
||||
if (html.length < 200 || !/<div id="app">/.test(html)) {
|
||||
fail(`${indexHtml} does not look like the built viewer (${html.length} bytes)`);
|
||||
}
|
||||
|
||||
// Every local src=/href= in the document must resolve inside dist/ui. This is
|
||||
// what catches a partial write: index.html naming a hashed bundle that the
|
||||
// build never emitted.
|
||||
const referenced = [...html.matchAll(/\s(?:src|href)="([^"]+)"/g)].map((m) => m[1]);
|
||||
const local = referenced.filter(
|
||||
(url) => !/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(url) && !url.startsWith('#')
|
||||
);
|
||||
|
||||
const missing = [];
|
||||
let assets = 0;
|
||||
for (const url of local) {
|
||||
const rel = url.replace(/^\.\//, '').replace(/[?#].*$/, '');
|
||||
if (!rel) continue;
|
||||
const onDisk = join(viewerDir, ...rel.split('/'));
|
||||
if (!existsSync(onDisk) || !statSync(onDisk).isFile()) missing.push(rel);
|
||||
else assets += 1;
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
fail(
|
||||
`index.html references ${missing.length} file(s) that are not in dist/viewer: ${missing.join(', ')}`,
|
||||
'the UI build was interrupted or dist/viewer was copied incompletely'
|
||||
);
|
||||
}
|
||||
|
||||
if (assets === 0) {
|
||||
fail('index.html references no bundled assets — the UI build produced no JS/CSS');
|
||||
}
|
||||
|
||||
// The viewer build must never have eaten the tsc output next door.
|
||||
for (const compiled of [join('bin', 'codegraph.js'), 'index.js', join('ui', 'shimmer-progress.js')]) {
|
||||
if (!existsSync(join(root, 'dist', compiled))) {
|
||||
fail(
|
||||
`dist/${compiled.split(sep).join('/')} is missing — the compiled engine is incomplete`,
|
||||
"if this appeared with a UI change, check ui/vite.config.ts: build.outDir must stay dist/viewer, and emptyOutDir must never point at a directory tsc writes (dist/ui is the TERMINAL ui)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[check-ui-build] dist/viewer ok (index.html + ${assets} referenced asset(s)); dist/ engine intact`);
|
||||
@@ -55,6 +55,10 @@ for archive in "${archives[@]}"; do
|
||||
nodefile="node"
|
||||
;;
|
||||
esac
|
||||
# The browser viewer must survive the archive round-trip too: a tar/zip that
|
||||
# dropped dist/viewer would publish a platform package whose `codegraph ui`
|
||||
# serves a 404.
|
||||
node "$ROOT/scripts/check-ui-build.mjs" --root "$pkgdir/lib"
|
||||
VERSION="$VERSION" SCOPE="$SCOPE" TARGET="$target" OSV="$os" ARCHV="$arch" NODEFILE="$nodefile" \
|
||||
node -e '
|
||||
const fs=require("fs");
|
||||
|
||||
Reference in New Issue
Block a user