| layout | docs |
|---|---|
| docsOrder | 140 |
| handlebars | false |
DOMStack coordinates page rendering, asset bundling, and file copying in a staged build. This guide explains the tools involved, the order of each phase, and how watch mode decides what to rebuild.
[[toc]]
domstack bundles the best tools for every technology in the stack:
jsandcssis bundled withesbuild.mdis processed with markdown-it.- static files are processed with cpx2.
tssupport via native typestripping in Node.js and esbuild.jsx/tsxsupport via esbuild.
These tools are treated as implementation details, but they may be exposed more in the future. The idea is that they can be swapped out for better tools in the future if they don't make it.
The one-shot builder discovers inputs using the shared file conventions, then records outputs from each build phase. The service worker is built last so manifest hooks can provide its build-time constants. Side-by-side blocks run in parallel; their arrows join before the next phase starts. On narrow screens, scroll a diagram horizontally to keep its labels readable.
flowchart TD accTitle: One-shot build accDescr: Discover inputs, run three asset tasks in parallel, build pages, then finalize the manifest and service worker before returning results. IDENTIFY["`**identifyPages()** Find pages, layouts, templates Find globals and settings`"] PREPARE["`**Prepare destination** Resolve manifest options Start parallel asset tasks`"] ESBUILD["`**buildEsbuild()** Bundle browser JS and CSS Record outputs`"] STATIC["`**buildStatic()** Copy static files if enabled Record outputs`"] COPY["`**buildCopy()** Copy extra directories Record outputs`"] PAGES["`**buildPages()** Start a fresh page worker Render pages and templates Apply layouts; record outputs`"] FINALIZE["`**Finalize build** Reconcile manifest if enabled Run hooks; build service worker`"] RESULTS["`**Return results** Discovery and build reports Manifest and warnings Write manifest JSON if requested`"] IDENTIFY --> PREPARE PREPARE --> ESBUILD & STATIC & COPY ESBUILD & STATIC & COPY --> PAGES PAGES --> FINALIZE --> RESULTS
The build process follows these key steps:
- Page identification - Scans the source directory to identify all pages, layouts, templates, and global assets
- Destination preparation - Ensures the destination directory is ready for the build output
- Parallel asset processing - Three operations run concurrently and record their outputs:
- JavaScript and CSS bundling via esbuild
- Static file copying (when enabled)
- Additional directory copying (from
--copyoptions)
- Page building - Initializes source-backed pages, derives global data, generates pages, and renders pages and templates with their declared data subscriptions
- Manifest reconciliation - When enabled, normalizes recorded outputs, hashes file contents, filters entries, computes a stable manifest version, and runs manifest hooks
- Service-worker building - Bundles the site service worker using any defines returned by manifest hooks; this output is not included in the already reconciled manifest
- Return results - Writes manifest JSON only when requested and returns the build results
This architecture allows for efficient parallel processing of independent tasks while maintaining the correct build order dependencies. The diagrams show successful execution; discovery and build errors stop later phases.
Each buildPages() call starts a fresh worker so server-side modules can be reloaded between watch builds.
Within that worker, source-backed pages are initialized before global data is computed.
Generated pages are downstream consumers of that data, not inputs to its producer.
The three source-page lanes show the work performed for each page type, not three separate concurrency pools.
flowchart TD
accTitle: Page worker stages
accDescr: Initialize Markdown, HTML, and JS or TS source pages in parallel. Derive global data from those pages, generate additional pages, then render pages and templates in parallel.
RESOLVE["`**Resolve once**
Defaults and global vars
Layouts and parent chains`"]
INIT["`**Parallel source-page init**
Concurrency: min(CPUs, 24)`"]
subgraph MD["Markdown page task"]
direction TB
MD_VARS["Resolve page.vars"]
MD_READ["`**mdBuilder()**
Read Markdown
Title and frontmatter`"]
MD_VARS --> MD_READ
end
subgraph HTML["HTML page task"]
direction TB
HTML_VARS["Resolve page.vars"]
HTML_READ["`**htmlBuilder()**
Read HTML source`"]
HTML_VARS --> HTML_READ
end
subgraph JS["JS / TS page task"]
direction TB
JS_VARS["Resolve page.vars"]
JS_READ["`**jsBuilder()**
Import page module
Read exported vars`"]
JS_VARS --> JS_READ
end
BIND["`**Finish each page init**
Merge vars and bind layouts
Collect assets and dataDeps`"]
DATA["`**global.data**
Receive initialized source PageData[]
Derive shared data`"]
SUBSCRIBE["`**Select declared data**
Project dataDeps for consumers
In watch: expand invalidated filters`"]
GENERATE["`**Run pages-file factories**
Consume declared data
Define generated pages`"]
GENERATED_INIT["`**Initialize generated pages**
Resolve vars, layouts, and assets
Bind declared data`"]
subgraph RENDER["Parallel rendering · shared concurrency budget"]
direction TB
PAGE_RENDER["`**pageWriter()**
Render source and generated pages
Wrap layouts inner to outer
Write outputs`"]
TEMPLATE_RENDER["`**templateBuilder()**
Render selected templates
Use declared data
Write outputs`"]
end
REPORT["`**Return page-build results**
Outputs, errors, and layout reports
Subscriptions in watch mode`"]
RESOLVE --> INIT
INIT --> MD & HTML & JS
MD & HTML & JS --> BIND
BIND --> DATA --> SUBSCRIBE --> GENERATE --> GENERATED_INIT
GENERATED_INIT --> RENDER --> REPORT
Selected *.pages.* factories produce definitions that go through the same page initialization as source-backed pages.
Each page, layout, template, and pages-file factory receives only the global-data keys it declares through dataDeps.
Layout subscriptions contribute to page invalidation, but each layout still receives its own data projection while rendering.
Page initialization uses a concurrency limit of min(CPUs, 24).
The final page and template rendering queues run in parallel, splitting that concurrency budget between them.
Variable Resolution Layers, from lowest to highest precedence:
- Domstack defaults - Internal defaults such as the default
layout: 'root'. - Global vars - Site-wide variables from
global.vars.js(resolved once). - Layout vars - Optional
export const varsfrom the resolved layout chain, merged outermost to innermost. - Page-specific vars vary by type:
- MD pages:
page.vars.jsplus builder vars from frontmatter. - HTML pages:
page.vars.js. - JS pages: exported
varspluspage.vars.js.
- MD pages:
Global data is not a variable-resolution layer.
It is resolved separately and projected into each consumer's data argument according to dataDeps.
Running domstack --watch or domstack -w performs an initial build, watches the source inputs, and serves dest with live reload.
Use domstack --watch-only when another process serves the output.
Watch mode coordinates three independent watchers:
- esbuild uses
context.watch()for global, layout, and page client bundles, styles, page-scoped Web Workers, and the site service worker. - chokidar watches page, layout, template, generated-pages, variable, and settings modules. DOMStack uses the changed file and its dependency maps to choose a rebuild scope.
- cpx2 watches static assets under
srcand directories supplied with--copy, copying or removing their destination files directly.
Chokidar events pass through a pure planner before any rebuild executes.
The planner reads an explicit snapshot of discovery, dependency maps, and the previous page-build outcome; it does not perform I/O or mutate that state.
DomStack owns the watch session, serializes events, executes plans, and releases its watchers, esbuild context, and server on shutdown.
flowchart TD
accTitle: Watch planning and execution
accDescr: Serialized Chokidar events produce a pure watch plan. DOMStack executes it, replans after bundle discovery when needed, and retains the last successful routing after page-build errors.
EVENT["Chokidar event"] --> QUEUE["Serialize in the watch session"]
subgraph PLANNING["Pure planning · no I/O"]
direction TB
CLASSIFY["Shared file conventions"]
PLAN["`**planWatchEvent()**
Inspect watch snapshot`"]
CLASSIFY --> PLAN
end
QUEUE --> PLANNING
PLANNING --> EXECUTE{"Execute plan"}
EXECUTE -->|Skip| SKIP["No page rebuild"]
EXECUTE -->|Full| FULL["`Rediscover inputs
Restart esbuild`"]
EXECUTE -->|Restart| RESTART["`Rediscover bundle entries
Restart esbuild`"]
RESTART --> BUNDLE["`**planBundleChange()**
Use refreshed discovery
Keep successful layout routing`"]
BUNDLE --> EXECUTE
EXECUTE -->|Pages| PAGES["`**buildPages()**
Full or filtered page phase`"]
FULL --> PAGES
PAGES --> SUCCESS{"Page build succeeded?"}
SUCCESS -->|Yes| SAVE["`Reconcile owned outputs
Refresh routing and subscriptions`"]
SUCCESS -->|No| RETAIN["`Keep successful ownership and routing
Require a full page retry`"]
Bundle replanning returns only a page plan or a skip; it does not restart esbuild again. After a page-build failure, the next page-producing plan retries the complete page phase rather than trusting incremental filters. Manifest-settings changes and service-worker entry additions or removals skip page rendering unless the module also has server-side consumers. The one-shot manifest pipeline is not part of watch execution.
Note
The filenames below use .ts by default.
You can also use .js, and TypeScript client bundles can use .tsx.
See Supported file types for all available extensions.
DOMStack uses these rebuild scopes:
- esbuild only: esbuild updates an existing browser entry without rendering HTML.
- Targeted page/template rebuild: DOMStack renders only the affected source-backed pages or templates.
- Targeted generated-pages rebuild: DOMStack renders and reconciles only the outputs owned by affected
*.pages.tsfiles. - Full page/template rebuild: DOMStack renders every source-backed and generated page and every template without restarting esbuild.
- Full rebuild: DOMStack rediscovers the source tree, restarts esbuild, renders all pages and templates, and refreshes its dependency maps.
Like templates, generated-pages modules rebuild when their own source or imported dependencies change. When a targeted build recomputes global data, DOMStack compares top-level values with the previous successful build and adds only subscribers of changed keys to the rebuild set.
| Change | Rebuild scope |
|---|---|
Existing page.ts, page.html, page.md, or adjacent page.vars.ts |
That page, plus subscribers of any changed global-data keys |
A module imported by a TypeScript page or page.vars.ts |
Pages that depend on it, plus subscribers of any changed global-data keys |
Existing *.layout.ts or a module it imports |
Source-backed pages and generated-page owners using the affected layout |
Existing *.template.ts or a module it imports |
Affected templates |
Existing *.pages.ts |
Generated outputs owned by that file, then refresh dependency maps |
A module imported by *.pages.ts |
Generated outputs owned by the importing files, then refresh dependency maps |
global.data.ts |
Consumers subscribed to top-level keys whose values changed |
global.vars.ts, markdown-it.settings.ts, esbuild.settings.ts, or their tracked imports |
Full rebuild and reset of retained global-data state |
domstack-manifest.settings.ts |
No rebuild. The manifest pipeline is disabled in watch mode |
| Existing client, style, Web Worker, or service-worker entry | esbuild only, unless the same module also has server-side consumers |
Static asset under src or a file under a --copy directory |
cpx2 copies or removes the output directly |
Adding or removing a file changes the set of discovered build inputs:
| Added or removed file | Rebuild scope |
|---|---|
Site service-worker.ts |
Restart esbuild. No page rebuild |
global.client.ts or global.css |
Restart esbuild and rebuild all pages |
| Layout client or style | Restart esbuild and rebuild source-backed pages and generated-page owners using that layout |
| Page client, style, or Web Worker | Restart esbuild and rebuild that page |
| Any other page, layout, template, generated-pages, variable, or settings file | Full rebuild |
When a full page/template rebuild or targeted generated-pages rebuild no longer claims an output from the previous successful build, DOMStack removes that obsolete page or template output from dest without touching outputs owned by unaffected files.
DOMStack uses @11ty/dependency-tree-typescript to statically analyze ESM imports.
The page watcher observes tracked imports within the source tree, including JSON files.
DOMStack maintains dependency maps for:
- Layout dependencies, source-backed pages using each layout, and generated-page owner layout membership
- TypeScript pages and adjacent page-variable dependencies
- Template dependencies
- Generated-pages module dependencies
- Current esbuild entry points
The maps are created after the initial build and refreshed after successful page builds and structural rediscovery. Layout routing and generated-output ownership use reports from successful page builds. Dependency analysis is best-effort. When DOMStack cannot safely determine a targeted scope, it falls back to a broader rebuild or skips an unrelated changed module.
esbuild tracks browser-entry dependencies independently.
A module imported only by client.ts triggers rebundling without rendering page HTML.
If the module also has server-side consumers, DOMStack rebuilds those consumers too.
Watch mode uses stable filenames for esbuild entry outputs:
[dir]/[name]
Production builds use content-hashed entry filenames:
[dir]/[name]-[hash]
Shared chunks remain content-hashed in both modes:
chunks/[ext]/[name]-[hash]
Page HTML points to stable entry files during watch mode. esbuild can update an entry and its chunk imports without requiring DOMStack to render the page again.
Watch mode builds and rebundles the site service worker, but it does not finalize, return, or write the DOMStack manifest.
Editing domstack-manifest.settings.ts does not trigger a watch rebuild unless it is also imported by server-side code.
Use domstack --serve when testing manifest-driven cache behavior.
It runs a one-shot build and serves the result without watch-mode filenames or live-reload HTML injection.
Add --domstackManifest only when the service worker or test needs the public domstack-manifest.json file.
Chokidar events are serialized through a promise chain. Each page rebuild or esbuild restart completes before the next queued filesystem event is processed, preventing overlapping DOMStack rebuilds during rapid saves.