Skip to content

Commit e2ec56e

Browse files
committed
Implement progressive watching
1 parent 90ad05f commit e2ec56e

File tree

20 files changed

+1408
-313
lines changed

20 files changed

+1408
-313
lines changed

.claude/settings.local.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,24 @@
33
"allow": [
44
"Bash(node --test:*)",
55
"Bash(git checkout:*)",
6-
"Bash(npm test:*)"
6+
"Bash(npm test:*)",
7+
"Bash(node -e:*)",
8+
"Bash(npm install:*)",
9+
"Bash(node --input-type=module:*)",
10+
"Bash(npm run test:node-test:*)",
11+
"Bash(npm run test:neostandard:*)",
12+
"WebFetch(domain:github.com)",
13+
"Bash(npx eslint:*)",
14+
"Bash(git -C /Users/bret/Developer/domstack branch:*)",
15+
"Bash(git -C /Users/bret/Developer/domstack-2 branch --show-current)",
16+
"Bash(npx neostandard:*)",
17+
"Bash(ls:*)",
18+
"Bash(git -C /Users/bret/Developer/domstack log --format=\"%s%n%n%b\" b15def119ce084a6ffa0f5d65deaac483132b9a4 -1)",
19+
"Bash(git -C /Users/bret/Developer/domstack log --format=\"%s%n%n%b\" 90ad05fb08faee843ea2ef451cacd726e3ac6012 -1)",
20+
"Bash(gh pr view:*)",
21+
"Bash(gh api:*)",
22+
"Bash(git -C /Users/bret/Developer/domstack log --oneline master..HEAD -- README.md)",
23+
"Bash(git -C /Users/bret/Developer/domstack diff master -- README.md)"
724
]
825
}
926
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package-lock.json
55
public
66
coverage
77
.tap
8+
.tmp-*
89

910
# Generated types
1011

README.md

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ export const vars = {
250250
favoriteCookie: 'Chocolate Chip with Sea Salt'
251251
}
252252

253-
export default const page: PageFunction<typeof vars}> = async ({
253+
const page: PageFunction<typeof vars}> = async ({
254254
vars
255255
}) => {
256256
return /* html */`<div>
@@ -259,6 +259,7 @@ export default const page: PageFunction<typeof vars}> = async ({
259259
</div>`
260260
}
261261

262+
export default page
262263
```
263264

264265
It is recommended to use some level of template processing over raw string templates so that HTML is well-formed and variable values are properly escaped. Here is a more realistic TypeScript example that uses [`preact`](https://preactjs.com/) with [`htm`](https://github.com/developit/htm) and `domstack` page introspection.
@@ -277,7 +278,7 @@ export const vars = {
277278
favoriteCake: 'Chocolate Cloud Cake'
278279
}
279280

280-
export default const blogIndex: PageFunction<BlogVars> = async ({
281+
const blogIndex: PageFunction<BlogVars> = async ({
281282
vars: { favoriteCake },
282283
pages
283284
}) => {
@@ -289,6 +290,8 @@ export default const blogIndex: PageFunction<BlogVars> = async ({
289290
</ul>
290291
</div>`
291292
}
293+
294+
export default blogIndex
292295
```
293296

294297
### Page Styles
@@ -486,7 +489,7 @@ type RootLayoutVars = {
486489
basePath?: string
487490
}
488491

489-
export default const defaultRootLayout: LayoutFunction<RootLayoutVars> = ({
492+
const defaultRootLayout: LayoutFunction<RootLayoutVars> = ({
490493
vars: {
491494
title,
492495
siteName = 'Domstack',
@@ -526,6 +529,8 @@ export default const defaultRootLayout: LayoutFunction<RootLayoutVars> = ({
526529
</html>
527530
`
528531
}
532+
533+
export default defaultRootLayout
529534
```
530535

531536
If your `src` folder doesn't have a `root.layout.js` file somewhere in it, `domstack` will use the default [`default.root.layout.js`](./lib/defaults/default.root.layout.js) file it ships. The default `root` layout includes a special boolean variable called `defaultStyle` that lets you disable a default page style (provided by [mine.css](http://github.com/bcomnes/mine.css)) that it ships with.
@@ -746,7 +751,7 @@ interface TemplateVars {
746751
testVar: string;
747752
}
748753

749-
export default const simpleTemplate: TemplateFunction<TemplateVars> = async ({
754+
const simpleTemplate: TemplateFunction<TemplateVars> = async ({
750755
vars: {
751756
foo,
752757
testVar
@@ -756,6 +761,8 @@ export default const simpleTemplate: TemplateFunction<TemplateVars> = async ({
756761
757762
This is just a file with access to global vars: ${foo}`
758763
}
764+
765+
export default simpleTemplate
759766
```
760767

761768
### Object template
@@ -790,7 +797,7 @@ interface TemplateVars {
790797
testVar: string;
791798
}
792799

793-
export default const objectArrayTemplate: TemplateFunction<TemplateVars> = async ({
800+
const objectArrayTemplate: TemplateFunction<TemplateVars> = async ({
794801
vars: {
795802
foo,
796803
testVar
@@ -811,6 +818,8 @@ This is just a file with access to global vars: ${testVar}`,
811818
}
812819
]
813820
}
821+
822+
export default objectArrayTemplate
814823
```
815824

816825
### AsyncIterator template
@@ -825,7 +834,7 @@ interface TemplateVars {
825834
testVar: string;
826835
}
827836

828-
export default const templateIterator: TemplateAsyncIterator<TemplateVars> = async function * ({
837+
const templateIterator: TemplateAsyncIterator<TemplateVars> = async function * ({
829838
vars: {
830839
foo,
831840
testVar
@@ -847,6 +856,8 @@ This is just a file with access to global vars: ${testVar}`,
847856
outputName: 'yielded-2.txt'
848857
}
849858
}
859+
860+
export default templateIterator
850861
```
851862

852863
### RSS Feed Template Example
@@ -872,7 +883,7 @@ interface TemplateVars {
872883
language: string;
873884
}
874885

875-
export default const feedsTemplate: TemplateAsyncIterator<TemplateVars> = async function * ({
886+
const feedsTemplate: TemplateAsyncIterator<TemplateVars> = async function * ({
876887
vars: {
877888
siteName,
878889
siteDescription,
@@ -921,6 +932,8 @@ export default const feedsTemplate: TemplateAsyncIterator<TemplateVars> = async
921932
outputName: './feeds/feed.xml'
922933
}
923934
}
935+
936+
export default feedsTemplate
924937
```
925938

926939
## Global Assets
@@ -984,7 +997,7 @@ type GlobalData = {
984997
blogPostsHtml: string
985998
}
986999

987-
export default const buildGlobalData: AsyncGlobalDataFunction<GlobalData> = async ({ pages }) => {
1000+
const buildGlobalData: AsyncGlobalDataFunction<GlobalData> = async ({ pages }) => {
9881001
const blogPosts = pages
9891002
.filter(p => p.vars?.layout === 'blog' && p.vars?.publishDate)
9901003
.sort((a, b) => new Date(b.vars.publishDate) - new Date(a.vars.publishDate))
@@ -1004,6 +1017,8 @@ export default const buildGlobalData: AsyncGlobalDataFunction<GlobalData> = asyn
10041017

10051018
return { blogPostsHtml }
10061019
}
1020+
1021+
export default buildGlobalData
10071022
```
10081023

10091024
The returned object is stamped onto every page's vars before rendering, so any page or layout can read the derived data via `vars`:
@@ -1036,10 +1051,12 @@ import { polyfillNode } from 'esbuild-plugin-polyfill-node'
10361051
// BuildOptions re-exported from esbuild
10371052
import type { BuildOptions } from '@domstack/static'
10381053

1039-
export default const esbuildSettingsOverride = async (esbuildSettings: BuildOptions): Promise<BuildOptions> => {
1054+
const esbuildSettingsOverride = async (esbuildSettings: BuildOptions): Promise<BuildOptions> => {
10401055
esbuildSettings.plugins = [polyfillNode()]
10411056
return esbuildSettings
10421057
}
1058+
1059+
export default esbuildSettingsOverride
10431060
```
10441061

10451062
Important esbuild settings you may want to set here are:
@@ -1061,7 +1078,7 @@ import markdownItContainer from 'markdown-it-container'
10611078
import markdownItPlantuml from 'markdown-it-plantuml'
10621079
import type { MarkdownIt } from 'markdown-it'
10631080

1064-
export default const markdownItSettingsOverride = async (md: MarkdownIt) => {
1081+
const markdownItSettingsOverride = async (md: MarkdownIt) => {
10651082
// Add custom plugins
10661083
md.use(markdownItContainer, 'spoiler', {
10671084
validate: (params: string) => {
@@ -1081,13 +1098,15 @@ export default const markdownItSettingsOverride = async (md: MarkdownIt) => {
10811098

10821099
return md
10831100
}
1101+
1102+
export default markdownItSettingsOverride
10841103
```
10851104

10861105
```typescript
10871106
import markdownIt, { MarkdownIt } from 'markdown-it'
10881107
import myCustomPlugin from './my-custom-plugin'
10891108

1090-
export default const markdownItSettingsOverride = async (md: MarkdownIt) => {
1109+
const markdownItSettingsOverride = async (md: MarkdownIt) => {
10911110
// Create a new instance with different settings
10921111
const newMd = markdownIt({
10931112
html: false, // Disable HTML tags in source
@@ -1101,7 +1120,7 @@ export default const markdownItSettingsOverride = async (md: MarkdownIt) => {
11011120
return newMd
11021121
}
11031122

1104-
markdownItSettingsOverride
1123+
export default markdownItSettingsOverride
11051124
```
11061125

11071126
By default, DOMStack ships with the following markdown-it plugins enabled:
@@ -1436,9 +1455,48 @@ Variable Resolution Layers:
14361455
- **JS pages**: exported vars → page.vars.js
14371456
- **Global data** - Derived variables from `global.data.js`, stamped onto every page after all pages initialize (resolved once, after page init)
14381457

1458+
### Watch Mode
1459+
1460+
When you run `domstack --watch` (or `domstack -w`), domstack performs an initial build and then watches for changes, rebuilding only what's necessary. Watch mode uses two independent watch loops:
1461+
1462+
**esbuild watch** — JS and CSS bundles are handled by esbuild's native `context.watch()`. In watch mode, output filenames are stable (no content hashes), so bundle changes never require a page HTML rebuild. Browser-sync detects the updated files on disk and reloads the browser directly.
1463+
1464+
**chokidar watch** — Page files, layouts, templates, and config files are watched by chokidar. When a file changes, domstack determines the minimal set of pages to rebuild using dependency tracking maps built at startup.
1465+
1466+
#### What triggers what
1467+
1468+
| Change | Rebuild scope |
1469+
|---|---|
1470+
| `page.js`, `page.ts`, `page.html`, `page.md`, or `page.vars.*` | Only that page |
1471+
| A file imported by a `page.js` or `page.vars.*` | Only the pages that import it (transitively) |
1472+
| A layout file (`*.layout.js`) | Only the pages using that layout |
1473+
| A file imported by a layout | Only the pages using the affected layout(s) |
1474+
| A template file (`*.template.js`) | Only that template |
1475+
| A file imported by a template | Only the affected template(s) |
1476+
| `markdown-it.settings.*` | All `.md` pages |
1477+
| `global.data.*` | All pages and templates |
1478+
| `global.vars.*` or `esbuild.settings.*` | Full rebuild (esbuild restart + all pages) |
1479+
| `client.js`, `style.css`, `*.layout.css`, `*.layout.client.*`, `global.client.*`, `global.css`, `*.worker.*` | esbuild handles it — no page rebuild |
1480+
| Adding or removing an esbuild entry point (e.g. creating a new `client.js`) | esbuild restart + only the affected page(s) |
1481+
| Adding or removing any other file | Full rebuild |
1482+
1483+
#### Dependency tracking
1484+
1485+
domstack uses [`@11ty/dependency-tree-typescript`](https://github.com/11ty/dependency-tree-typescript) to statically analyze ESM imports in page files, layout files, and template files. This means if your `page.js` imports a shared utility module, changing that module will only rebuild the pages that depend on it — not the entire site.
1486+
1487+
esbuild tracks its own entry point dependencies independently. Changing a file imported by `client.js` will trigger an esbuild rebundle but will not trigger a page rebuild, since watch mode uses stable filenames.
1488+
1489+
#### Stable filenames
1490+
1491+
In watch mode, esbuild uses `[dir]/[name]` output patterns instead of `[dir]/[name]-[hash]`. This means the `<script>` and `<link>` tags in page HTML always point to the same filenames. When esbuild rebundles, the file contents change but the filenames don't, so page HTML never needs to be re-rendered just because a bundle changed.
1492+
1493+
#### Build serialization
1494+
1495+
Chokidar events are serialized through a promise chain, so rapid saves don't cause overlapping rebuilds. Each rebuild completes before the next one starts.
1496+
14391497
## Roadmap
14401498

1441-
`domstack` works and has a rudimentary watch command, but hasn't been battle tested yet.
1499+
`domstack` works and has a watch command with progressive rebuilds.
14421500
If you end up trying it out, please open any issues or ideas that you have, and feel free to share what you build.
14431501

14441502
Some notable features are included below, see the [roadmap](https://github.com/users/bcomnes/projects/3/) for a more in depth view of whats planned.
@@ -1480,6 +1538,9 @@ Some notable features are included below, see the [roadmap](https://github.com/u
14801538
- [x] markdown-it.settings.ts support
14811539
- [x] page-worker.worker.ts page worker support
14821540
- [x] `page.md` page support
1541+
- [x] Remove `postVars` and implement `global.data.{j,t}s`
1542+
- [x] Harden behavior around conflicting `browserVars` and esbuild settings
1543+
- [x] Progressive watch rebuilds with dependency tracking
14831544
- ...[See roadmap](https://github.com/users/bcomnes/projects/3/)
14841545

14851546
## History

0 commit comments

Comments
 (0)