flake-explorer docs

Frontend

The viewer is a Svelte 5 SPA in app/, written entirely in runes mode and bundled by Bun.build with bun-plugin-svelte — there is no Vite or separate bundler config; the server builds the bundle in memory at startup (src/build-app.ts, see Build & infra). Entry is app/main.ts: it initializes theme, font scale, pane widths, and hash routing on the app singleton, kicks off the manifest load, and mounts app/App.svelte. The data it renders is described in Data schema.

State architecture

All state lives in one AppState class instance exported as app from app/lib/state.svelte.ts. Two rules keep it fast on large flakes:

Loading is modeled as slots: a ConfigSlot is "loading", an error object, or { data, indexes }. Errors carry an optional permanent flag — in a static export a missing document can never be fetched, so components hide the retry button. Small UI state (search query, expanded sets, hover, tooltip, pane widths, theme) uses plain $state and SvelteSet.

Dual data mode

app/lib/data.ts gives static export and serve mode one code path. loadJson(name) first looks for an embedded <script type="application/json" id="data:<name>"> tag; if present its text is parsed, otherwise the document is fetched from ./data/<name>. isStatic() is defined as "does an embedded manifest.json tag exist" — a single-file export always embeds the manifest and the serve mode never does (it only embeds about.json), so that one tag is the static-mode signal. hasEmbedded() lets state.svelte.ts fail fast with a permanent error when a static export omits a config blob or file source instead of issuing a doomed fetch.

URL hash deep-linking

app/lib/hash.ts encodes the current selection as the hash path and view filters behind ?:

Form Selection
#/o/<output.path.dots> outputs-tree node (non-module)
#/c/<configId> configuration
#/c/<configId>/m/<moduleId> module within a configuration
#/f/<fileId> file
#/i/<inputName> flake input
?q=<search>&all=1 filters: search text, "all options" toggle

%, ?, and / are escaped in every segment; output-path segments additionally escape . because it is the path separator there. History semantics live in state.svelte.ts: select() compares old and new selection with sameSelection() — a genuine selection change calls pushState, while filter-only changes (and setFilters) call replaceState, so Back walks selections without replaying keystrokes. initRouting() applies the hash at startup and on hashchange; a deep link decoded before the manifest arrives is re-followed once loadManifest() completes.

Supporting modules

Module Role
app/lib/indexes.ts Pure data-shaping: file identity resolution (self / input / patched-input / unknown), the left-pane module tree, and the O(1) maps behind hover cross-highlighting (fileToNodes, imports/importedBy)
app/lib/segments.ts Source-view segmentation: unions server-computed tree-sitter highlight runs with client-computed per-line file-reference intervals so one segment can be both colored and a clickable link
app/lib/color.ts Stable colors: first ~12 registered keys get the theme's curated CVD-validated slots (--s1..--s12); beyond that FNV-1a hash → golden-angle hue → OKLCH at theme-tuned lightness/chroma
app/lib/themes.ts The two THEMES stops (light/dark warm-paper palettes) as complete CSS custom-property sets applied inline on :root, plus the gen params color.ts uses for generated colors
app/lib/url.ts Web links for locked inputs: webUrl() normalizes git+https etc., commitUrl() builds per-host commit permalinks (github.com, gitlab.com, codeberg.org)

Component map

App.svelte lays out a header, a three-pane grid with draggable splitters, and overlays. The 18 components in app/components/ group as:

Area Components
Chrome Header, Splitter, Tooltip (option hover card), AboutModal
Left pane OutputsTreeOutputBranch (generic outputs), TreeNode (module trees of nixos/darwin configurations)
Center pane Stage switches on the selection → ModuleDetail, FileDetail, InputDetail, with Legend as the no-selection view; shared pieces OptionRow, SourceView, InputProvenance
Right pane FileListFileTreeBranch
Shared Dot ("the one true dot" — colored marker with optional disclosure triangle), tree-connectors.css

Cross-highlighting works through the state singleton: hovering a file sets app.hover, and highlightedNodes / highlightedFiles (derived from the precomputed indexes) tint the matching tree nodes in the other panes.

See Architecture for how this fits the extractor and server, and Testing for how components are tested under happy-dom.