feat(ui): highlight source server-side with a near-monochrome Shiki theme (CG-43)

The viewer's code block stops lexing with a hand-rolled dialect table and
reads real TextMate grammars instead, run once in `/api/source`.

Three things make that safe to depend on:

* Highlighting never fails a request. A missing grammar, an oversized
  slice, an ESM import that did not resolve — every one of them answers
  `engine: 'plain'` with a reason and the source still goes out.
* Identifiers survive whatever token boundaries a grammar chose. Every
  code token is split into identifier runs before it goes on the wire, so
  the graph's call-site overlay claims a token the highlighter produced
  rather than re-cutting the line. `assignRefs` now matches on a token's
  text rather than on the class a grammar gave it, so a language that
  scopes type names as `storage.type` still links.
* The theme classifies rather than colours: its foregrounds are sentinels
  the server maps back to class names, and the viewer paints them from
  CSS custom properties — one token stream serves light and dark with no
  refetch, and the ramp lives only in app.css.

Comments move from --ink-3 to a new --code-comment. --ink-3 measures
3.46:1 on paper and 3.00:1 on the hot-line tint, both under AA for 12.5px
text; --code-comment is the smallest step along the same ramp that clears
4.5:1 on every background a code line can have, and stays quieter than
the strings and numbers above it.

Shipping: @shikijs/core and @shikijs/engine-javascript are runtime
dependencies (no wasm, no native module); @shikijs/langs stays a
devDependency and `npm run build:textmate` writes only the closure the
engine's 40-odd languages reach — 56 grammars, 2.6 MB, against 11 MB for
all 722. check-ui-build.mjs asserts the tree after every build and inside
every release archive.
This commit is contained in:
Colby McHenry
2026-08-27 01:23:10 -05:00
parent 87afc50e76
commit 2ad836d935
22 changed files with 2117 additions and 405 deletions
+38 -1
View File
@@ -13,6 +13,11 @@
* where tsc puts the TERMINAL ui, so a mis-pointed outDir silently deletes
* modules the CLI requires at startup.
*
* The pruned TextMate grammars in dist/textmate/ are checked the same way and
* for the same reason: without them every file the viewer shows falls back to
* unhighlighted text, which looks like a styling bug rather than a missing
* build step.
*
* 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.
@@ -90,4 +95,36 @@ for (const compiled of [join('bin', 'codegraph.js'), 'index.js', join('ui', 'shi
}
}
console.log(`[check-ui-build] dist/viewer ok (index.html + ${assets} referenced asset(s)); dist/ engine intact`);
// The pruned syntax grammars (scripts/prune-grammars.mjs). Their absence is
// survivable at runtime — source is served unhighlighted — which is exactly why
// it has to fail here: nothing downstream would ever complain.
const textmateDir = join(root, 'dist', 'textmate');
const manifestPath = join(textmateDir, 'manifest.json');
if (!existsSync(manifestPath)) {
fail(
`missing ${manifestPath}`,
staged
? 'this bundle was assembled before the syntax grammars were added, or dist/textmate was not copied'
: 'run `npm run build:textmate` (it needs @shikijs/langs from devDependencies)'
);
}
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
const languages = Object.keys(manifest.languages ?? {});
if (languages.length === 0) fail('dist/textmate/manifest.json lists no languages');
const grammarFiles = new Set(Object.values(manifest.languages).flat());
const missingGrammars = [...grammarFiles].filter(
(name) => !existsSync(join(textmateDir, `${name}.json`))
);
if (missingGrammars.length > 0) {
fail(
`dist/textmate is missing ${missingGrammars.length} grammar file(s): ${missingGrammars.join(', ')}`,
'the prune step was interrupted or dist/textmate was copied incompletely'
);
}
console.log(
`[check-ui-build] dist/viewer ok (index.html + ${assets} referenced asset(s)); ` +
`dist/textmate ok (${languages.length} languages, ${grammarFiles.size} grammars); dist/ engine intact`
);