Docs Architecture
Build-time discovery
The author writes markup with a static marker; the build finds it and adds the registration module; the runtime finds the root on its first event. No component is imported for the interaction, and the mechanism stays inspectable at every step.
On this page
Two plugins
svelteLean() returns two Vite plugins. svelte-lean:scan runs before
the Svelte compiler (enforce: 'pre') and reads the source of every included .svelte file for static data-slean="…" markers. svelte-lean:inject runs after the compiler (enforce: 'post') and
appends one import statement per registration module to the compiled JavaScript of that file.
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { svelteLean } from '@svelte-lean/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [svelteLean(), sveltekit()]
});// What the compiled module of that page receives from the plugin, appended after the
// compiled code (imports are hoisted, so it evaluates first):
import '@svelte-lean/primitives/tabs/register';The exact format: one import '<specifier>'; per module, single quotes, sorted,
de-duplicated, appended after the compiled code and terminated by a newline. Appending keeps every
existing line and column in place, so the Svelte compiler's source map stays valid; ES module imports
are hoisted, so the registration evaluates before the component code. The source markup is never rewritten.
Bundlers de-duplicate modules: a hundred files that use tabs still produce one registration
module in the graph, and registration is idempotent, so a manual import next to an injected one
changes nothing. The plugin pair works whether it is placed before or after sveltekit(); put it first so the scanner sees the source before any preprocessor.
Static-only contract
Discovered: data-slean="tabs", data-slean='tabs', data-slean=tabs. Not discovered: an expression or an interpolation. A dynamic
marker is recorded in the manifest and reported once per file with its line and column, together
with the fix.
<!-- Not discovered: the identifier is an expression -->
<div data-slean={kind}>…</div>
<!-- Discovered: a static identifier -->
<div data-slean="tabs">…</div>Dynamic behavior identifiers cannot be auto-discovered.
Found:
src/lib/Panel.svelte:12:2 data-slean={kind}
Use a static identifier such as data-slean="tabs", or import the registration module
manually (for example import '@svelte-lean/primitives/tabs/register').The scanner is lexical and does not use the Svelte compiler. It skips <script> and <style> content, HTML comments, quoted attribute values and {…} expressions in text (including strings, templates, regular expressions and
comments inside them), so a marker inside a string literal is not a false positive. Markers
inside {#if} and {#each} blocks are found like any other markup. It
does not see markup produced at runtime through {@html …}, and it does not scan node_modules by default: a library that renders Svelte Lean markup either is added
to include or imports its registration modules itself.
Manifest
A production build writes a manifest: every behavior found, the module injected for it (null for native and unknown behaviors), the files that use it, the dynamic markers and the unknown names.
It is a build artifact for diagnostics and tooling and is not shipped to the browser.
{
"version": 1,
"behaviors": {
"button": { "files": ["src/lib/Toolbar.svelte"], "module": null },
"tabs": {
"files": ["src/routes/settings/+page.svelte"],
"module": "@svelte-lean/primitives/tabs/register"
}
},
"dynamic": {
"src/lib/Panel.svelte": [{ "line": 12, "expression": "{kind}" }]
},
"unknown": []
}The manifest of the build you are reading, read at prerender time from node_modules/.svelte-lean/manifest.json:
| Behavior | Registration module | Files |
|---|---|---|
| accordion | none (native) | src/lib/primitives/Gallery.svelte, src/routes/primitives/accordion/+page.svelte |
| alert | none (native) | src/lib/primitives/Gallery.svelte, src/routes/primitives/alert/+page.svelte |
| alert-dialog | none (native) | src/routes/primitives/alert-dialog/+page.svelte |
| autocomplete | none (native) | src/routes/primitives/autocomplete/+page.svelte |
| avatar | none (native) | src/lib/primitives/Gallery.svelte, src/routes/primitives/avatar/+page.svelte |
| badge | none (native) | src/lib/primitives/Gallery.svelte, src/routes/primitives/badge/+page.svelte |
| breadcrumb | none (native) | src/routes/primitives/breadcrumb/+page.svelte |
| button | none (native) | src/lib/components/CopyButton.svelte, src/lib/components/ThemeToggle.svelte, src/lib/components/TopNav.svelte, src/lib/home/ListenerLab.svelte, src/lib/home/Stage.svelte, src/lib/primitives/Gallery.svelte, src/routes/demos/settings/SettingsApp.svelte, src/routes/docs/architecture/native-first/+page.svelte, src/routes/performance/results/+page.svelte, src/routes/performance/results/RowsSpecimen.svelte, src/routes/primitives/alert-dialog/+page.svelte, src/routes/primitives/alert/+page.svelte, src/routes/primitives/badge/+page.svelte, src/routes/primitives/button-group/+page.svelte, src/routes/primitives/button/+page.svelte, src/routes/primitives/card/+page.svelte, src/routes/primitives/combobox/+page.svelte, src/routes/primitives/dialog/+page.svelte, src/routes/primitives/drawer/+page.svelte, src/routes/primitives/empty/+page.svelte, src/routes/primitives/kbd/+page.svelte, src/routes/primitives/listbox/+page.svelte, src/routes/primitives/menu/+page.svelte, src/routes/primitives/popover/+page.svelte, src/routes/primitives/spinner/+page.svelte, src/routes/primitives/toast/+page.svelte, src/routes/primitives/toolbar/+page.svelte, src/routes/primitives/tooltip/+page.svelte, src/routes/proof/+page.svelte, src/routes/styling/themes/+page.svelte, src/routes/styling/variants/+page.svelte, src/routes/table/clipboard-export/CsvExport.svelte, src/routes/table/clipboard-export/RangeClipboard.svelte, src/routes/table/columns/Visibility.svelte, src/routes/table/editing/Editing.svelte, src/routes/table/pagination/Pagination.svelte, src/routes/table/rows/NestedTable.svelte, src/routes/table/rows/RowAnimation.svelte, src/routes/table/rows/RowEvents.svelte, src/routes/table/selection/Selection.svelte, src/routes/table/server-data/EmptyStates.svelte, src/routes/table/server-data/LoadingEmpty.svelte, src/routes/table/server-data/ServerData.svelte, src/routes/table/state-persistence/PersistedState.svelte |
| button-group | none (native) | src/routes/primitives/button-group/+page.svelte |
| calendar | @svelte-lean/primitives/calendar/register | src/lib/primitives/Gallery.svelte, src/routes/primitives/calendar/+page.svelte, src/routes/primitives/date-picker/+page.svelte |
| card | none (native) | src/routes/primitives/card/+page.svelte |
| carousel | none (native) | src/routes/primitives/carousel/+page.svelte |
| checkbox | none (native) | /vercel/path0/packages/table/dist/table/data-table.svelte, src/lib/primitives/Gallery.svelte, src/routes/demos/settings/SettingsApp.svelte, src/routes/primitives/checkbox/+page.svelte, src/routes/primitives/dialog/+page.svelte, src/routes/primitives/drawer/+page.svelte, src/routes/proof/+page.svelte, src/routes/table/layout/DensityBorders.svelte, src/routes/table/server-data/LoadingEmpty.svelte |
| color-field | none (native) | src/routes/primitives/color-field/+page.svelte |
| combobox | @svelte-lean/primitives/combobox/register | src/lib/home/Stage.svelte, src/lib/primitives/Gallery.svelte, src/routes/docs/architecture/runtime-tiers/+page.svelte, src/routes/primitives/combobox/+page.svelte, src/routes/proof/+page.svelte |
| context-menu | @svelte-lean/primitives/context-menu/register | src/routes/primitives/context-menu/+page.svelte |
| date-field | none (native) | src/routes/primitives/date-field/+page.svelte |
| date-picker | @svelte-lean/primitives/date-picker/register | src/lib/primitives/Gallery.svelte, src/routes/primitives/date-picker/+page.svelte |
| descriptions | none (native) | src/routes/primitives/descriptions/+page.svelte |
| dialog | none (native) | src/lib/home/Stage.svelte, src/lib/primitives/Gallery.svelte, src/routes/demos/settings/SettingsApp.svelte, src/routes/docs/architecture/native-first/+page.svelte, src/routes/primitives/combobox/+page.svelte, src/routes/primitives/dialog/+page.svelte, src/routes/proof/+page.svelte, src/routes/styling/variants/+page.svelte |
| disclosure | none (native) | src/lib/home/Stage.svelte, src/lib/primitives/Gallery.svelte, src/routes/demos/settings/SettingsApp.svelte, src/routes/primitives/disclosure/+page.svelte, src/routes/proof/+page.svelte |
| drawer | none (native) | src/routes/primitives/drawer/+page.svelte |
| empty | none (native) | src/routes/primitives/empty/+page.svelte |
| field | none (native) | src/routes/primitives/field/+page.svelte, src/routes/primitives/input/+page.svelte |
| file-drop | @svelte-lean/primitives/file-drop/register | src/routes/primitives/file-drop/+page.svelte |
| file-field | none (native) | src/routes/primitives/file-field/+page.svelte |
| hover-card | @svelte-lean/primitives/hover-card/register | src/lib/primitives/Gallery.svelte, src/routes/primitives/hover-card/+page.svelte |
| input | none (native) | src/routes/primitives/field/+page.svelte, src/routes/primitives/input/+page.svelte, src/routes/styling/themes/+page.svelte, src/routes/table/server-data/EmptyStates.svelte |
| kbd | none (native) | src/routes/primitives/kbd/+page.svelte |
| listbox | @svelte-lean/primitives/listbox/register | src/lib/primitives/Gallery.svelte, src/routes/primitives/listbox/+page.svelte, src/routes/proof/+page.svelte |
| menu | @svelte-lean/primitives/menu/register | src/lib/home/Stage.svelte, src/lib/primitives/Gallery.svelte, src/routes/demos/settings/SettingsApp.svelte, src/routes/primitives/button-group/+page.svelte, src/routes/primitives/context-menu/+page.svelte, src/routes/primitives/menu/+page.svelte, src/routes/primitives/menubar/+page.svelte, src/routes/proof/+page.svelte, src/routes/table/columns/Visibility.svelte |
| menubar | @svelte-lean/primitives/menubar/register | src/routes/primitives/menubar/+page.svelte |
| meter | none (native) | src/routes/primitives/meter/+page.svelte |
| number-field | @svelte-lean/primitives/number-field/register | src/lib/primitives/Gallery.svelte, src/routes/primitives/number-field/+page.svelte |
| otp-field | none (native) | src/lib/primitives/Gallery.svelte, src/routes/primitives/otp-field/+page.svelte |
| pagination | none (native) | src/routes/primitives/pagination/+page.svelte |
| popover | none (native) | src/lib/home/Stage.svelte, src/lib/primitives/Gallery.svelte, src/routes/demos/settings/SettingsApp.svelte, src/routes/docs/architecture/native-first/+page.svelte, src/routes/primitives/listbox/+page.svelte, src/routes/primitives/popover/+page.svelte, src/routes/proof/+page.svelte, src/routes/styling/variants/+page.svelte, src/routes/table/sorting/HeaderSort.svelte |
| progress | none (native) | src/lib/primitives/Gallery.svelte, src/routes/primitives/progress/+page.svelte |
| radio-group | none (native) | src/lib/home/TableLab.svelte, src/lib/primitives/Gallery.svelte, src/routes/demos/settings/SettingsApp.svelte, src/routes/primitives/radio-group/+page.svelte, src/routes/proof/+page.svelte, src/routes/table/layout/DensityBorders.svelte, src/routes/table/localization-rtl/Messages.svelte, src/routes/table/selection/Selection.svelte |
| range-slider | @svelte-lean/primitives/range-slider/register | src/lib/primitives/Gallery.svelte, src/routes/primitives/range-slider/+page.svelte |
| rating | none (native) | src/lib/primitives/Gallery.svelte, src/routes/primitives/rating/+page.svelte |
| scroll-area | none (native) | src/routes/primitives/scroll-area/+page.svelte |
| segmented | none (native) | src/lib/primitives/Gallery.svelte, src/routes/primitives/field/+page.svelte, src/routes/primitives/segmented/+page.svelte |
| select | @svelte-lean/primitives/select/register | src/lib/primitives/Gallery.svelte, src/routes/primitives/select/+page.svelte |
| separator | none (native) | src/routes/primitives/separator/+page.svelte |
| skeleton | none (native) | src/routes/primitives/skeleton/+page.svelte |
| slider | none (native) | src/lib/primitives/Gallery.svelte, src/routes/primitives/slider/+page.svelte |
| spinner | none (native) | src/routes/primitives/skeleton/+page.svelte, src/routes/primitives/spinner/+page.svelte |
| splitter | @svelte-lean/primitives/splitter/register | src/lib/primitives/Gallery.svelte, src/routes/primitives/splitter/+page.svelte |
| steps | none (native) | src/lib/primitives/Gallery.svelte, src/routes/primitives/steps/+page.svelte |
| switch | none (native) | src/lib/home/Stage.svelte, src/lib/home/TableLab.svelte, src/lib/primitives/Gallery.svelte, src/routes/demos/settings/SettingsApp.svelte, src/routes/primitives/switch/+page.svelte, src/routes/proof/+page.svelte |
| tabs | @svelte-lean/primitives/tabs/register | src/lib/components/ExampleFrame.svelte, src/lib/components/InstallBlock.svelte, src/lib/components/TabsRoots.svelte, src/lib/home/Compare.svelte, src/lib/home/Stage.svelte, src/lib/primitives/Gallery.svelte, src/routes/demos/settings/SettingsApp.svelte, src/routes/docs/architecture/dom-protocol/+page.svelte, src/routes/docs/architecture/runtime-tiers/+page.svelte, src/routes/primitives/tabs/+page.svelte, src/routes/proof/+page.svelte, src/routes/styling/variants/+page.svelte |
| tag | none (native) | src/lib/primitives/Gallery.svelte, src/routes/primitives/tag/+page.svelte |
| timeline | none (native) | src/routes/primitives/timeline/+page.svelte |
| toast | @svelte-lean/primitives/toast/register | src/lib/primitives/Gallery.svelte, src/routes/primitives/toast/+page.svelte |
| toggle | @svelte-lean/primitives/toggle/register | src/routes/primitives/toggle/+page.svelte, src/routes/primitives/toolbar/+page.svelte |
| toggle-group | @svelte-lean/primitives/toggle-group/register | src/lib/primitives/Gallery.svelte, src/routes/primitives/toggle-group/+page.svelte, src/routes/primitives/toolbar/+page.svelte |
| toolbar | @svelte-lean/primitives/toolbar/register | src/routes/primitives/toolbar/+page.svelte |
| tooltip | @svelte-lean/primitives/tooltip/register | src/lib/primitives/Gallery.svelte, src/routes/primitives/tooltip/+page.svelte |
| tree | @svelte-lean/primitives/tree/register | src/lib/primitives/Gallery.svelte, src/routes/primitives/tree/+page.svelte |
Build report
With report: true the plugin prints what the scan observed after the client bundle is
written. It states no size and no listener count: those are measured by the consumer fixtures against
real production output, not estimated by the plugin.
svelte-lean build report
native-only button, dialog
runtime tabs -> @svelte-lean/primitives/tabs/register
src/routes/settings/+page.svelte
unknown none
dynamic none
manifest node_modules/.svelte-lean/manifest.jsonOptions
svelteLean({
include: ['**/*.svelte'],
exclude: ['**/node_modules/**'],
package: '@svelte-lean/primitives',
behaviors: {},
manifest: true,
report: false,
warnDynamic: true,
ssr: 'skip'
});| Name | Type | Default | Description |
|---|---|---|---|
# include | string | RegExp | (string | RegExp)[] | ['**/*.svelte'] | Files to scan. Globs match the path relative to the Vite root and the absolute path; style and ?raw sub-requests are never touched. |
# exclude | string | RegExp | (string | RegExp)[] | ['**/node_modules/**'] | Files never scanned or injected into. Tested after include. |
# package | string | '@svelte-lean/primitives' | Re-bases the built-in registration specifiers onto another package that provides the same modules. |
# behaviors | Record<string, string | false> | {} | Overrides and extends the built-in table: a module specifier to inject, or false for a behavior that needs no runtime. |
# manifest | boolean | string | true | Write node_modules/.svelte-lean/manifest.json (or the given path) at the end of a production build. Not written in dev or after a failed build. |
# report | boolean | false | Print the build report after the client bundle is written; SSR builds are silent. |
# warnDynamic | boolean | true | Warn once per file about data-slean={expression} markers, through the plugin context so the warning carries file and position. |
# ssr | 'skip' | 'inject' | 'skip' | Whether server modules also receive the imports. Registration modules are SSR-safe, so this is a server bundle size choice only. |
The built-in table maps every Tier 1 and Tier 2 primitive to its register module and every Tier
0 primitive (plus the radio vocabulary of ADR 0001) to no runtime. A test in the primitives
package keeps this table equal to the package's own metadata, so a new Tier 1 primitive cannot exist
without a registration module here. A name outside the table is reported once and injects nothing.
The shared router
What the injected module does is small. registerBehavior() in @svelte-lean/core stores the definition and installs at most one listener per event
type the definition declares, on the document runtime. Only event types some registered behavior
uses are installed; pointer-move and scroll are never installed generically. Routing walks event.composedPath() from the target outward and looks at the data-slean attribute of each element on the way, so nested behaviors work and no selector
runs over the document.
// @svelte-lean/core, in outline: one listener per event type a registered behavior declares.
// Routing walks composedPath() from the target outward and looks at dataset.slean on the way;
// no selector runs over the document, and a root inserted later is found on its first event.
registerBehavior('tabs', {
tier: 1,
events: {
click({ root, target, event }) { … },
keydown({ root, target, event }) { … }
}
});Discovery is lazy through interaction: a root inserted after registration is found on its first
event, so a MutationObserver over the document is not needed and is not used. The observer
would do work on every unrelated DOM update and create controllers for instances nobody touches; both
are what the tier model exists to avoid. Registration is idempotent and safe to evaluate more than
once, and installation waits until a browser environment exists, so the same module imports under
Node.
import { createRuntime } from '@svelte-lean/core';
import { tabs } from '@svelte-lean/primitives/tabs';
// A second runtime for a shadow root or an iframe document; the default one is the document.
const runtime = createRuntime({ root: shadowRoot });
runtime.register('tabs', tabs);The listener count is a release gate: 1000 tabs roots on one page keep one click and one keydown listener, counted by the runtime and by an independent wrapper
around addEventListener (listeners.spec.ts). The proof page repeats the reading in your browser.
Measured
The production fixtures in the repository assert the module graph the plugin produces. The tabs-only fixture ships a Svelte Lean chunk of 1521 B brotli; the manual-registration fixture, which imports the register module by hand without the plugin, ships 1521 B brotli with the same module set asserted; tabs and menu together with one kernel ship 2351 B brotli. The fixture that uses only native markup ships no chunk at all.
Source
- packages/vite/src:
scanner.ts,plugin.ts,manifest.ts,report.ts,behaviors.ts - packages/vite/tests: scanner, plugin, and a real
vite buildof a fixture application in both plugin orders - core/src/runtime.ts and registry.ts: the router and the idempotent registry
- ADR 0004 and ADR 0005
- fixtures/: the consumer builds and their assertions