flake-explorer docs

Frontend

The viewer is a Svelte 5 SPA in web/, 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 (scripts/build-app.ts, see Build & infra). Entry is web/main.ts: it initializes theme, text size, pane widths, and hash routing on the app singleton, kicks off the manifest load, and mounts web/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 web/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

web/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

web/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
#/c/<configId>/opt/<loc.dots> option within a configuration (OptionDetail)
#/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 and option-loc 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.

The header box does two things at once: the query keeps live-filtering both trees (subtreeMatches label matching, unchanged), and SearchBox additionally opens a categorized dropdown (Options / Packages / Files / Inputs) built by web/lib/search.ts — pure, unit-tested corpus + ranking (rankMatch: exact > exact segment > segment prefix > substring; customized options first). Packages, files, and inputs come from the manifest; options come from loaded config blobs (ConfigIndexes.optionLocsLower), because they are on-demand documents — the dropdown's footer lists unloaded configurations with a load-in-place button (loading/errored slots render through AsyncSlot — error + retry — since loadConfig no-ops on an occupied slot). A static export auto-loads every embedded config on first search focus (the blobs are local, so a complete corpus is free); the dynamic server never auto-triggers extraction.

Supporting modules

Module Role
web/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)
web/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
web/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
web/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
web/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 web/components/ group as:

Area Components
Chrome Header, SearchBox (tree filter + unified results dropdown), 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.