From 43de5996d8758af38e419dd4a9b0a6db0dabe3bb Mon Sep 17 00:00:00 2001
From: Bret Comnes
Date: Sun, 5 Jul 2026 20:32:59 -0700
Subject: [PATCH] Add unstable static client cache preview
Adds the unstable Domstack manifest preview, service-worker build support, documentation, migration notes, tests, and workspace-aware example infrastructure without the PWA example implementations.
Address manifest review feedback
Keep service worker build single file
Revert "Keep service worker build single file"
This reverts commit 3c6beffe25ac6c584cf9c580460b2072646b365e.
Working static PWA cache example
PWA
---
.github/dependabot.yml | 141 +--
.gitignore | 2 +
.npmignore | 8 -
README.md | 481 ++++++++-
agents.md | 2 +
bin.js | 60 +-
docs/v12-migration.md | 228 +++-
eslint.config.js | 1 +
examples/basic/package.json | 13 +-
.../src/js-page/loose-assets/shared-lib.ts | 2 +-
examples/blog/package.json | 11 +-
examples/blog/src/layouts/post.layout.ts | 18 +-
examples/css-modules/package.json | 13 +-
examples/default-layout/package.json | 7 +-
examples/esbuild-settings/package.json | 7 +-
examples/markdown-settings/package.json | 5 +-
.../src/markdown-it.settings.js | 6 +-
examples/nested-dest/package.json | 9 +-
examples/preact-isomorphic/package.json | 17 +-
.../src/isomorphic/client.ts | 74 +-
.../preact-isomorphic/src/isomorphic/page.ts | 8 +-
.../src/layouts/root.layout.ts | 12 +-
examples/react/package.json | 11 +-
.../react/src/globals/esbuild.settings.ts | 2 +-
examples/react/src/globals/global.client.ts | 45 +-
examples/react/src/layouts/root.layout.ts | 2 +-
examples/react/src/react-page/client.tsx | 242 ++---
examples/static-mpa-offline/DESIGN.md | 132 +++
examples/static-mpa-offline/README.md | 175 ++++
examples/static-mpa-offline/package.json | 30 +
.../rescue-service-worker.js | 31 +
.../static-mpa-offline/src/about/client.ts | 3 +
examples/static-mpa-offline/src/about/page.md | 6 +
.../static-mpa-offline/src/about/style.css | 5 +
.../static-mpa-offline/src/admin/client.ts | 3 +
examples/static-mpa-offline/src/admin/page.md | 23 +
.../static-mpa-offline/src/admin/page.vars.ts | 5 +
.../static-mpa-offline/src/admin/style.css | 5 +
.../src/cache-inspector/client.ts | 58 +
.../src/cache-inspector/page.html | 7 +
.../src/cache-inspector/page.vars.ts | 5 +
.../src/cache-inspector/style.css | 5 +
examples/static-mpa-offline/src/client.ts | 3 +
.../domstack-manifest.settings.ts | 21 +
.../globals/domstack-manifest/policy-build.ts | 33 +
.../global-client/connection-status.ts | 172 +++
.../globals/global-client/global.client.ts | 74 ++
.../global-client/service-worker-events.ts | 43 +
.../service-worker-registration.ts | 102 ++
.../global-client/service-worker-reset.ts | 71 ++
.../globals/global-client/status-banner.ts | 73 ++
.../static-mpa-offline/src/globals/global.css | 102 ++
.../src/globals/global.vars.ts | 5 +
.../service-worker/background-events.ts | 111 ++
.../service-worker/cache-inspection.ts | 93 ++
.../src/globals/service-worker/clients.ts | 31 +
.../globals/service-worker/fetch-handlers.ts | 166 +++
.../src/globals/service-worker/lifecycle.ts | 47 +
.../src/globals/service-worker/precache.ts | 206 ++++
.../globals/service-worker/runtime-cache.ts | 86 ++
.../service-worker/service-worker-settings.ts | 53 +
.../globals/service-worker/service-worker.ts | 105 ++
.../src/layouts/admin.layout.ts | 33 +
.../src/layouts/progressive-cache.layout.ts | 33 +
.../src/layouts/render-policy-layout.ts | 51 +
.../src/layouts/root.layout.ts | 33 +
.../static-mpa-offline/src/offline/client.ts | 3 +
.../static-mpa-offline/src/offline/page.md | 4 +
.../static-mpa-offline/src/offline/style.css | 5 +
.../static-mpa-offline/src/page-client.ts | 12 +
.../static-mpa-offline/src/page-style.css | 8 +
examples/static-mpa-offline/src/page.md | 85 ++
.../src/progressive-cache/alpha/client.ts | 3 +
.../src/progressive-cache/alpha/page.md | 15 +
.../src/progressive-cache/alpha/style.css | 5 +
.../src/progressive-cache/assets/client.ts | 3 +
.../assets/details/client.ts | 3 +
.../assets/details/details-badge.svg | 9 +
.../progressive-cache/assets/details/page.md | 10 +
.../assets/details/page.vars.ts | 5 +
.../assets/details/style.css | 5 +
.../src/progressive-cache/assets/page.md | 10 +
.../src/progressive-cache/assets/page.vars.ts | 5 +
.../assets/runtime-badge.svg | 9 +
.../src/progressive-cache/assets/style.css | 5 +
.../src/progressive-cache/beta/client.ts | 3 +
.../src/progressive-cache/beta/page.md | 10 +
.../src/progressive-cache/beta/style.css | 5 +
.../src/progressive-cache/override/client.ts | 3 +
.../src/progressive-cache/override/page.md | 16 +
.../progressive-cache/override/page.vars.ts | 5 +
.../src/progressive-cache/override/style.css | 5 +
examples/static-mpa-offline/src/style.css | 5 +
examples/static-mpa-offline/tsconfig.json | 22 +
examples/static-mpa-workbox-offline/DESIGN.md | 112 ++
examples/static-mpa-workbox-offline/README.md | 108 ++
.../static-mpa-workbox-offline/package.json | 38 +
.../rescue-service-worker.js | 31 +
.../src/about/client.ts | 3 +
.../src/about/page.md | 6 +
.../src/about/style.css | 5 +
.../src/admin/client.ts | 3 +
.../src/admin/page.md | 23 +
.../src/admin/page.vars.ts | 5 +
.../src/admin/style.css | 5 +
.../src/cache-inspector/client.ts | 62 ++
.../src/cache-inspector/page.html | 7 +
.../src/cache-inspector/page.vars.ts | 5 +
.../src/cache-inspector/style.css | 5 +
.../static-mpa-workbox-offline/src/client.ts | 3 +
.../domstack-manifest.settings.ts | 21 +
.../globals/domstack-manifest/policy-build.ts | 121 +++
.../global-client/connection-status.ts | 172 +++
.../globals/global-client/global.client.ts | 72 ++
.../global-client/service-worker-events.ts | 43 +
.../service-worker-registration.ts | 116 ++
.../global-client/service-worker-reset.ts | 71 ++
.../globals/global-client/status-banner.ts | 73 ++
.../src/globals/global.css | 110 ++
.../src/globals/global.vars.ts | 5 +
.../service-worker/background-events.ts | 108 ++
.../service-worker/cache-inspection.ts | 93 ++
.../service-worker/service-worker-settings.ts | 42 +
.../globals/service-worker/service-worker.ts | 128 +++
.../src/layouts/admin.layout.ts | 33 +
.../src/layouts/progressive-cache.layout.ts | 33 +
.../src/layouts/render-policy-layout.ts | 51 +
.../src/layouts/root.layout.ts | 33 +
.../src/offline/client.ts | 3 +
.../src/offline/page.md | 4 +
.../src/offline/style.css | 5 +
.../src/page-client.ts | 12 +
.../src/page-style.css | 8 +
.../static-mpa-workbox-offline/src/page.md | 86 ++
.../src/progressive-cache/alpha/client.ts | 3 +
.../src/progressive-cache/alpha/page.md | 15 +
.../src/progressive-cache/alpha/style.css | 5 +
.../src/progressive-cache/assets/client.ts | 3 +
.../assets/details/client.ts | 3 +
.../assets/details/details-badge.svg | 9 +
.../progressive-cache/assets/details/page.md | 10 +
.../assets/details/page.vars.ts | 5 +
.../assets/details/style.css | 5 +
.../src/progressive-cache/assets/page.md | 10 +
.../src/progressive-cache/assets/page.vars.ts | 5 +
.../assets/runtime-badge.svg | 9 +
.../src/progressive-cache/assets/style.css | 5 +
.../src/progressive-cache/beta/client.ts | 3 +
.../src/progressive-cache/beta/page.md | 10 +
.../src/progressive-cache/beta/style.css | 5 +
.../src/progressive-cache/override/client.ts | 3 +
.../src/progressive-cache/override/page.md | 16 +
.../progressive-cache/override/page.vars.ts | 5 +
.../src/progressive-cache/override/style.css | 5 +
.../static-mpa-workbox-offline/src/style.css | 5 +
.../static-mpa-workbox-offline/tsconfig.json | 22 +
examples/string-layouts/package.json | 5 +-
examples/tailwind/package.json | 11 +-
.../tailwind/src/globals/esbuild.settings.js | 6 +-
examples/tailwind/src/layouts/root.layout.js | 8 +-
examples/type-stripping/package.json | 17 +-
.../type-stripping/src/isomorphic/client.ts | 151 ++-
.../type-stripping/src/tsx-page/client.tsx | 24 +-
examples/uhtml-isomorphic/package.json | 9 +-
.../src/layouts/root.layout.js | 6 +-
examples/worker-example/package.json | 13 +-
index.js | 109 +-
lib/build-copy/index.js | 23 +-
lib/build-esbuild/index.js | 506 +++++++--
lib/build-pages/index.js | 25 +-
lib/build-pages/page-builders/page-writer.js | 56 +-
.../page-builders/template-builder.js | 120 ++-
lib/build-pages/page-data.js | 94 +-
lib/build-pages/page-data.test.js | 85 +-
lib/build-pages/resolve-vars.js | 59 +-
lib/build-static/index.js | 13 +-
lib/builder.js | 80 +-
lib/domstack-manifest/index.js | 990 ++++++++++++++++++
lib/domstack-manifest/schema.json | 172 +++
lib/helpers/cpx2-report.js | 68 ++
lib/helpers/domstack-error.js | 29 +-
lib/helpers/domstack-warning.js | 1 +
lib/helpers/generate-tree-data.js | 156 +--
lib/helpers/path.js | 26 +
lib/helpers/stable-json-stringify.js | 38 +
lib/helpers/type-guards.js | 23 +
lib/identify-pages.js | 59 +-
lib/identify-pages.test.js | 76 +-
package.json | 61 +-
plans/domstack-manifest.md | 205 ++++
plans/global-data.md | 189 ----
plans/progressive-rebuild.md | 323 ------
plans/standard-static-mpa-service-worker.md | 199 ++++
plans/workbox-workflow-integration.md | 227 ++++
scripts/domstack-manifest-schema.js | 13 +
test-cases/general-features/index.test.js | 516 ++++++++-
.../blog/2025/a-blog-post-from-2025/page.ts | 6 +-
.../src/domstack-manifest.settings.js | 1 +
.../src/globals/service-worker.mts | 67 ++
.../general-features/src/js-page/page.vars.js | 2 +
.../src/libs/service-worker-helper.js | 13 +
.../template-output-escape/index.test.js | 28 +
.../template-output-escape/src/README.md | 1 +
.../src/escape.template.js | 6 +
test-cases/type-exports/index.test.ts | 187 +++-
test-cases/watch/index.test.js | 48 +-
types.ts | 20 +-
types/@11ty__dependency-tree-typescript.d.ts | 6 +-
208 files changed, 9740 insertions(+), 1406 deletions(-)
delete mode 100644 .npmignore
create mode 100644 examples/static-mpa-offline/DESIGN.md
create mode 100644 examples/static-mpa-offline/README.md
create mode 100644 examples/static-mpa-offline/package.json
create mode 100644 examples/static-mpa-offline/rescue-service-worker.js
create mode 100644 examples/static-mpa-offline/src/about/client.ts
create mode 100644 examples/static-mpa-offline/src/about/page.md
create mode 100644 examples/static-mpa-offline/src/about/style.css
create mode 100644 examples/static-mpa-offline/src/admin/client.ts
create mode 100644 examples/static-mpa-offline/src/admin/page.md
create mode 100644 examples/static-mpa-offline/src/admin/page.vars.ts
create mode 100644 examples/static-mpa-offline/src/admin/style.css
create mode 100644 examples/static-mpa-offline/src/cache-inspector/client.ts
create mode 100644 examples/static-mpa-offline/src/cache-inspector/page.html
create mode 100644 examples/static-mpa-offline/src/cache-inspector/page.vars.ts
create mode 100644 examples/static-mpa-offline/src/cache-inspector/style.css
create mode 100644 examples/static-mpa-offline/src/client.ts
create mode 100644 examples/static-mpa-offline/src/globals/domstack-manifest/domstack-manifest.settings.ts
create mode 100644 examples/static-mpa-offline/src/globals/domstack-manifest/policy-build.ts
create mode 100644 examples/static-mpa-offline/src/globals/global-client/connection-status.ts
create mode 100644 examples/static-mpa-offline/src/globals/global-client/global.client.ts
create mode 100644 examples/static-mpa-offline/src/globals/global-client/service-worker-events.ts
create mode 100644 examples/static-mpa-offline/src/globals/global-client/service-worker-registration.ts
create mode 100644 examples/static-mpa-offline/src/globals/global-client/service-worker-reset.ts
create mode 100644 examples/static-mpa-offline/src/globals/global-client/status-banner.ts
create mode 100644 examples/static-mpa-offline/src/globals/global.css
create mode 100644 examples/static-mpa-offline/src/globals/global.vars.ts
create mode 100644 examples/static-mpa-offline/src/globals/service-worker/background-events.ts
create mode 100644 examples/static-mpa-offline/src/globals/service-worker/cache-inspection.ts
create mode 100644 examples/static-mpa-offline/src/globals/service-worker/clients.ts
create mode 100644 examples/static-mpa-offline/src/globals/service-worker/fetch-handlers.ts
create mode 100644 examples/static-mpa-offline/src/globals/service-worker/lifecycle.ts
create mode 100644 examples/static-mpa-offline/src/globals/service-worker/precache.ts
create mode 100644 examples/static-mpa-offline/src/globals/service-worker/runtime-cache.ts
create mode 100644 examples/static-mpa-offline/src/globals/service-worker/service-worker-settings.ts
create mode 100644 examples/static-mpa-offline/src/globals/service-worker/service-worker.ts
create mode 100644 examples/static-mpa-offline/src/layouts/admin.layout.ts
create mode 100644 examples/static-mpa-offline/src/layouts/progressive-cache.layout.ts
create mode 100644 examples/static-mpa-offline/src/layouts/render-policy-layout.ts
create mode 100644 examples/static-mpa-offline/src/layouts/root.layout.ts
create mode 100644 examples/static-mpa-offline/src/offline/client.ts
create mode 100644 examples/static-mpa-offline/src/offline/page.md
create mode 100644 examples/static-mpa-offline/src/offline/style.css
create mode 100644 examples/static-mpa-offline/src/page-client.ts
create mode 100644 examples/static-mpa-offline/src/page-style.css
create mode 100644 examples/static-mpa-offline/src/page.md
create mode 100644 examples/static-mpa-offline/src/progressive-cache/alpha/client.ts
create mode 100644 examples/static-mpa-offline/src/progressive-cache/alpha/page.md
create mode 100644 examples/static-mpa-offline/src/progressive-cache/alpha/style.css
create mode 100644 examples/static-mpa-offline/src/progressive-cache/assets/client.ts
create mode 100644 examples/static-mpa-offline/src/progressive-cache/assets/details/client.ts
create mode 100644 examples/static-mpa-offline/src/progressive-cache/assets/details/details-badge.svg
create mode 100644 examples/static-mpa-offline/src/progressive-cache/assets/details/page.md
create mode 100644 examples/static-mpa-offline/src/progressive-cache/assets/details/page.vars.ts
create mode 100644 examples/static-mpa-offline/src/progressive-cache/assets/details/style.css
create mode 100644 examples/static-mpa-offline/src/progressive-cache/assets/page.md
create mode 100644 examples/static-mpa-offline/src/progressive-cache/assets/page.vars.ts
create mode 100644 examples/static-mpa-offline/src/progressive-cache/assets/runtime-badge.svg
create mode 100644 examples/static-mpa-offline/src/progressive-cache/assets/style.css
create mode 100644 examples/static-mpa-offline/src/progressive-cache/beta/client.ts
create mode 100644 examples/static-mpa-offline/src/progressive-cache/beta/page.md
create mode 100644 examples/static-mpa-offline/src/progressive-cache/beta/style.css
create mode 100644 examples/static-mpa-offline/src/progressive-cache/override/client.ts
create mode 100644 examples/static-mpa-offline/src/progressive-cache/override/page.md
create mode 100644 examples/static-mpa-offline/src/progressive-cache/override/page.vars.ts
create mode 100644 examples/static-mpa-offline/src/progressive-cache/override/style.css
create mode 100644 examples/static-mpa-offline/src/style.css
create mode 100644 examples/static-mpa-offline/tsconfig.json
create mode 100644 examples/static-mpa-workbox-offline/DESIGN.md
create mode 100644 examples/static-mpa-workbox-offline/README.md
create mode 100644 examples/static-mpa-workbox-offline/package.json
create mode 100644 examples/static-mpa-workbox-offline/rescue-service-worker.js
create mode 100644 examples/static-mpa-workbox-offline/src/about/client.ts
create mode 100644 examples/static-mpa-workbox-offline/src/about/page.md
create mode 100644 examples/static-mpa-workbox-offline/src/about/style.css
create mode 100644 examples/static-mpa-workbox-offline/src/admin/client.ts
create mode 100644 examples/static-mpa-workbox-offline/src/admin/page.md
create mode 100644 examples/static-mpa-workbox-offline/src/admin/page.vars.ts
create mode 100644 examples/static-mpa-workbox-offline/src/admin/style.css
create mode 100644 examples/static-mpa-workbox-offline/src/cache-inspector/client.ts
create mode 100644 examples/static-mpa-workbox-offline/src/cache-inspector/page.html
create mode 100644 examples/static-mpa-workbox-offline/src/cache-inspector/page.vars.ts
create mode 100644 examples/static-mpa-workbox-offline/src/cache-inspector/style.css
create mode 100644 examples/static-mpa-workbox-offline/src/client.ts
create mode 100644 examples/static-mpa-workbox-offline/src/globals/domstack-manifest/domstack-manifest.settings.ts
create mode 100644 examples/static-mpa-workbox-offline/src/globals/domstack-manifest/policy-build.ts
create mode 100644 examples/static-mpa-workbox-offline/src/globals/global-client/connection-status.ts
create mode 100644 examples/static-mpa-workbox-offline/src/globals/global-client/global.client.ts
create mode 100644 examples/static-mpa-workbox-offline/src/globals/global-client/service-worker-events.ts
create mode 100644 examples/static-mpa-workbox-offline/src/globals/global-client/service-worker-registration.ts
create mode 100644 examples/static-mpa-workbox-offline/src/globals/global-client/service-worker-reset.ts
create mode 100644 examples/static-mpa-workbox-offline/src/globals/global-client/status-banner.ts
create mode 100644 examples/static-mpa-workbox-offline/src/globals/global.css
create mode 100644 examples/static-mpa-workbox-offline/src/globals/global.vars.ts
create mode 100644 examples/static-mpa-workbox-offline/src/globals/service-worker/background-events.ts
create mode 100644 examples/static-mpa-workbox-offline/src/globals/service-worker/cache-inspection.ts
create mode 100644 examples/static-mpa-workbox-offline/src/globals/service-worker/service-worker-settings.ts
create mode 100644 examples/static-mpa-workbox-offline/src/globals/service-worker/service-worker.ts
create mode 100644 examples/static-mpa-workbox-offline/src/layouts/admin.layout.ts
create mode 100644 examples/static-mpa-workbox-offline/src/layouts/progressive-cache.layout.ts
create mode 100644 examples/static-mpa-workbox-offline/src/layouts/render-policy-layout.ts
create mode 100644 examples/static-mpa-workbox-offline/src/layouts/root.layout.ts
create mode 100644 examples/static-mpa-workbox-offline/src/offline/client.ts
create mode 100644 examples/static-mpa-workbox-offline/src/offline/page.md
create mode 100644 examples/static-mpa-workbox-offline/src/offline/style.css
create mode 100644 examples/static-mpa-workbox-offline/src/page-client.ts
create mode 100644 examples/static-mpa-workbox-offline/src/page-style.css
create mode 100644 examples/static-mpa-workbox-offline/src/page.md
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/alpha/client.ts
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/alpha/page.md
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/alpha/style.css
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/assets/client.ts
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/assets/details/client.ts
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/assets/details/details-badge.svg
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/assets/details/page.md
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/assets/details/page.vars.ts
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/assets/details/style.css
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/assets/page.md
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/assets/page.vars.ts
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/assets/runtime-badge.svg
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/assets/style.css
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/beta/client.ts
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/beta/page.md
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/beta/style.css
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/override/client.ts
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/override/page.md
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/override/page.vars.ts
create mode 100644 examples/static-mpa-workbox-offline/src/progressive-cache/override/style.css
create mode 100644 examples/static-mpa-workbox-offline/src/style.css
create mode 100644 examples/static-mpa-workbox-offline/tsconfig.json
create mode 100644 lib/domstack-manifest/index.js
create mode 100644 lib/domstack-manifest/schema.json
create mode 100644 lib/helpers/cpx2-report.js
create mode 100644 lib/helpers/path.js
create mode 100644 lib/helpers/stable-json-stringify.js
create mode 100644 lib/helpers/type-guards.js
create mode 100644 plans/domstack-manifest.md
delete mode 100644 plans/global-data.md
delete mode 100644 plans/progressive-rebuild.md
create mode 100644 plans/standard-static-mpa-service-worker.md
create mode 100644 plans/workbox-workflow-integration.md
create mode 100644 scripts/domstack-manifest-schema.js
create mode 100644 test-cases/general-features/src/domstack-manifest.settings.js
create mode 100644 test-cases/general-features/src/globals/service-worker.mts
create mode 100644 test-cases/general-features/src/libs/service-worker-helper.js
create mode 100644 test-cases/template-output-escape/index.test.js
create mode 100644 test-cases/template-output-escape/src/README.md
create mode 100644 test-cases/template-output-escape/src/escape.template.js
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 152df60e..123842dd 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,10 +1,7 @@
-# Basic dependabot.yml file with
-# minimum configuration for two package managers
-
version: 2
updates:
- # Enable version updates for npm
- package-ecosystem: "npm"
+ directory: "/"
groups:
typescript:
patterns:
@@ -13,150 +10,20 @@ updates:
- "typescript"
preact:
patterns:
+ - "@preact/signals"
- "preact"
- "preact-render-to-string"
- "htm"
- directory: "/"
- schedule:
- interval: "daily"
- - package-ecosystem: "npm"
- directory: "/examples/basic"
- groups:
- typescript:
- patterns:
- - "@voxpelli/tsconfig"
- - "@types/node"
- - "typescript"
- schedule:
- interval: "daily"
- - package-ecosystem: "npm"
- directory: "/examples/css-modules/"
- groups:
- typescript:
- patterns:
- - "@voxpelli/tsconfig"
- - "@types/node"
- - "typescript"
- schedule:
- interval: "daily"
- - package-ecosystem: "npm"
- directory: "/examples/default-layout/"
- groups:
- typescript:
- patterns:
- - "@voxpelli/tsconfig"
- - "@types/node"
- - "typescript"
- schedule:
- interval: "daily"
- - package-ecosystem: "npm"
- directory: "/examples/esbuild-settings"
- groups:
- typescript:
- patterns:
- - "@voxpelli/tsconfig"
- - "@types/node"
- - "typescript"
- schedule:
- interval: "daily"
- - package-ecosystem: "npm"
- directory: "/examples/markdown-settings/"
- groups:
- typescript:
- patterns:
- - "@voxpelli/tsconfig"
- - "@types/node"
- - "typescript"
- schedule:
- interval: "daily"
- - package-ecosystem: "npm"
- directory: "/examples/nested-dest/"
- groups:
- typescript:
- patterns:
- - "@voxpelli/tsconfig"
- - "@types/node"
- - "typescript"
- schedule:
- interval: "daily"
- - package-ecosystem: "npm"
- directory: "/examples/preact-isomorphic/"
- groups:
- typescript:
- patterns:
- - "@voxpelli/tsconfig"
- - "@types/node"
- - "typescript"
- schedule:
- interval: "daily"
- - package-ecosystem: "npm"
- directory: "/examples/react/"
- groups:
- typescript:
- patterns:
- - "@voxpelli/tsconfig"
- - "@types/node"
- - "typescript"
react:
patterns:
- "react"
- "react-dom"
- "@types/react"
- "@types/react-dom"
+
schedule:
interval: "daily"
- - package-ecosystem: "npm"
- directory: "/examples/string-layouts/"
- groups:
- typescript:
- patterns:
- - "@voxpelli/tsconfig"
- - "@types/node"
- - "typescript"
- schedule:
- interval: "daily"
- - package-ecosystem: "npm"
- directory: "/examples/tailwind/"
- groups:
- typescript:
- patterns:
- - "@voxpelli/tsconfig"
- - "@types/node"
- - "typescript"
- schedule:
- interval: "daily"
- - package-ecosystem: "npm"
- directory: "/examples/type-stripping/"
- groups:
- typescript:
- patterns:
- - "@voxpelli/tsconfig"
- - "@types/node"
- - "typescript"
- schedule:
- interval: "daily"
- - package-ecosystem: "npm"
- directory: "/examples/uhtml-isomorphic/"
- groups:
- typescript:
- patterns:
- - "@voxpelli/tsconfig"
- - "@types/node"
- - "typescript"
- schedule:
- interval: "daily"
- - package-ecosystem: "npm"
- directory: "/examples/worker-examples/"
- groups:
- typescript:
- patterns:
- - "@voxpelli/tsconfig"
- - "@types/node"
- - "typescript"
- schedule:
- interval: "daily"
- # Enable version updates for pnpm
- # Enable updates to github actions
+
- package-ecosystem: "github-actions"
directory: "/"
schedule:
diff --git a/.gitignore b/.gitignore
index 02a8f93e..2a021aba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,8 @@ node_modules
sandbox.js
.nyc_output
package-lock.json
+pnpm-lock.yaml
+pnpm-workspace.yaml
public
coverage
lcov.info
diff --git a/.npmignore b/.npmignore
deleted file mode 100644
index ad663373..00000000
--- a/.npmignore
+++ /dev/null
@@ -1,8 +0,0 @@
-node_modules
-sandbox.js
-.nyc_output
-package-lock.json
-public
-coverage
-.tap
-.nova
diff --git a/README.md b/README.md
index 1b04d23d..400af485 100644
--- a/README.md
+++ b/README.md
@@ -41,9 +41,13 @@ Usage: domstack [options]
--dest, -d path to build destination directory (default: "public")
--ignore, -i comma separated gitignore style ignore string
--drafts Build draft pages with the `.draft.{md,js,ts,html}` page suffix.
+ --noEsbuildMeta skip writing the esbuild metafile to disk
+ --domstackManifest write the domstack manifest to disk
--eject, -e eject the DOMStack default layout, style and client into the src flag directory
--watch, -w build, watch and serve the site build
--watch-only watch and build the src folder without serving
+ --serve build once and serve the destination directory without watching
+ --port port for --serve (default: 3000)
--copy path to directories to copy into dist; can be used multiple times
--help, -h show help
--version, -v show version information
@@ -55,6 +59,7 @@ domstack (v12.0.0)
- Running `domstack` will result in a `build` by default.
- Running `domstack --watch` or `domstack -w` will build the site and start an auto-reloading development web-server that watches for changes (provided by [`@domstack/sync`][domstack-sync]).
+- Running `domstack --serve` will run a normal one-shot build and then serve the destination directory without watching or injecting live-reload snippets. This is useful for PWA testing because service-worker hooks and manifest versions run against stable build bytes. Add `--domstackManifest` if you also need to serve `domstack-manifest.json`. Use `domstack --serve --port 3001` to choose a different local port.
- Running `domstack --eject` or `domstack -e` will extract the default layout, global styles, and client-side JavaScript into your source directory and add the necessary dependencies to your package.json.
`domstack` is primarily a unix `bin` written for the [Node.js](https://nodejs.org) runtime that is intended to be installed from `npm` as a `devDependency` inside a `package.json` committed to a `git` repository.
@@ -133,7 +138,9 @@ src % tree
│ ├── global.vars.ts # site wide variables get defined in global.vars.ts
│ ├── global.data.ts # optional file to derive and aggregate data from all pages before rendering
│ ├── markdown-it.settings.ts # You can customize the markdown-it instance used to render markdown
-│ └── esbuild.settings.ts # You can even customize the build settings passed to esbuild
+│ ├── domstack-manifest.settings.ts # You can customize the domstack manifest
+│ ├── esbuild.settings.ts # You can even customize the build settings passed to esbuild
+│ └── service-worker.ts # a site service worker builds to /service-worker.js.
├── page.md # The top level page can also be a page.md (or README.md) file.
├── client.ts # the top level page can define a page scoped js client.
├── style.css # the top level page can define a page scoped css style.
@@ -153,15 +160,14 @@ To run examples:
```bash
$ git clone git@github.com:bcomnes/domstack.git
$ cd domstack
-# install the top level deps
+# install the root package and all example workspaces
$ npm i
-$ cd example:{example-name}
-# install the example deps
-$ npm i
-# start the example
-$ npm start
+# build one example workspace
+$ npm --workspace @domstack/basic-example run build
```
+Each example is an npm workspace under `examples/*`, so a root `npm install` links the local `@domstack/static` package into example builds. Example packages keep `@domstack/static` declared as `file:../../.` so the local dependency target remains explicit.
+
### Additional examples
Here are some additional external examples of larger domstack projects.
@@ -533,13 +539,30 @@ Thanks for reading my article
A page referencing a layout name that doesn't have a matching layout file will result in a build error.
+Layouts may also export optional default variables for pages using that layout. Like page/global vars, layout vars may be an object, a sync function, or an async function:
+
+```ts
+export const vars = {
+ showSidebar: true,
+ pageType: 'article',
+}
+```
+
+Layout vars are merged into the same resolved page variable cascade that pages, layouts, templates, and domstack manifest settings receive. Precedence is:
+
+```txt
+page/frontmatter vars > page.vars.* > layout vars > global.data/global.vars > domstack defaults
+```
+
+This makes layout vars useful for section-wide defaults while still letting individual pages override them.
+
### The default `root.layout.js`
A layout is a `ts`/`js` file that `export default`'s an async or sync function that implements an outer-wrapper html template that will house the inner content from the page (`children`) being rendered. Think of the frame around a picture. That's a layout. 🖼️
It is always passed a single object argument with the following entries:
-- `vars`: An object of global, page folder, and page variables merged together. Pages can customize layouts by providing or overriding global defaults.
+- `vars`: The resolved page variable cascade, including domstack defaults, global vars/data, layout vars, page vars, and page builder vars/frontmatter. Pages can customize layouts by overriding global or layout defaults.
- `scripts`: array of paths that should be included onto the page in a script tag src with type `module`.
- `styles`: array of paths that should be included onto the page in a `link rel="stylesheet"` tag with the `href` pointing to the paths in the array.
- `children`: A string of the inner content of the page, or whatever type your js page functions returns. `md` and `html` page types always return strings.
@@ -562,6 +585,10 @@ type RootLayoutVars = {
basePath?: string
}
+export const vars = {
+ defaultStyle: true,
+}
+
const defaultRootLayout: LayoutFunction = ({
vars: {
title,
@@ -1069,6 +1096,366 @@ await pMap(allPosts, async (page) => {
const html = renderCache.get(page.pageInfo.path) ?? ''
```
+## Domstack Manifest
+
+> [!WARNING]
+> The domstack manifest, `domstack-manifest.settings.*`, first-class `service-worker.*` builds, manifest hooks, and related browser `process.env.DOMSTACK_*` defines are an unstable preview feature.
+> Their names, option shapes, manifest schema, generated output, and runtime semantics may change outside of a major version while the API is validated with real PWA use cases.
+> Avoid depending on this preview contract for long-lived integrations without pinning `@domstack/static` to an exact version.
+
+Programmatic one-shot builds return a `domstackManifest` object when a manifest consumer exists.
+A consumer is either a `domstack-manifest.settings.*` file or explicit `domstackManifest` configuration.
+`hooks.manifestBuilt` receives that manifest before the final service-worker bundle is emitted.
+The CLI does not write `domstack-manifest.json` by default; writing the JSON file is opt-in for deployment metadata or integrations that need a runtime/public manifest artifact.
+
+The manifest is a normalized list of files that domstack emitted:
+
+```js
+/**
+ * @import { FromSchema } from 'json-schema-to-ts'
+ */
+import { DOMSTACK_MANIFEST_SCHEMA_ID, domstackManifestSchema } from '@domstack/static'
+
+/**
+ * @typedef {FromSchema} DomstackManifest
+ */
+```
+
+Equivalent shape:
+
+```ts
+type DomstackManifest, ManifestVars = Record> = {
+ $schema: typeof DOMSTACK_MANIFEST_SCHEMA_ID
+ version: string
+ generatedAt: string
+ entries: DomstackManifestEntry[]
+ policy?: Policy
+}
+
+type DomstackManifestEntry> = {
+ url: string
+ outputRelname: string
+ kind: 'page' | 'template' | 'script' | 'style' | 'chunk' |
+ 'service-worker' | 'worker' | 'worker-manifest' | 'static' |
+ 'copy' | 'sourcemap' | 'metadata'
+ revision: string | null
+ bytes: number | null
+ contentType?: string
+ integrity?: string
+ manifestVars?: ManifestVars
+ urlRevisioned?: boolean
+ static?: boolean
+ role?: string
+ sourceRelname?: string
+ entryPoint?: string
+ pagePath?: string
+ pageUrl?: string
+ templatePath?: string
+ page?: {
+ path: string
+ url: string
+ vars?: {
+ precache?: unknown
+ offline?: unknown
+ }
+ }
+}
+```
+
+domstack exports `DOMSTACK_MANIFEST_SCHEMA_ID`, `DOMSTACK_MANIFEST_SCHEMA_PATH`,
+`getDomstackManifestSchemaId(version)`, `domstackManifestSchema`, `domstackManifestEntrySchema`,
+`domstackManifestEntryPageMetaSchema`, and `domstackManifestKindSchema` for tools that want the JSON Schema
+contract directly. The public `DomstackManifest`, `DomstackManifestEntry`, `DomstackManifestEntryPageMeta`,
+`DomstackManifestKind`, `DomstackManifestOptions`, `DomstackManifestTransformContext`,
+`DomstackManifestTransform`, `DomstackManifestPolicyTransformContext`, and
+`DomstackManifestPolicyTransform` types are derived from or aligned with those schemas.
+
+`version` is a sha256 hash of each sorted entry's cache-relevant fields: `url`, `revision`, `kind`,
+`contentType`, `integrity`, `manifestVars`, `urlRevisioned`, `static`, `role`, page-level
+`precache` / `offline` vars, and root `policy`. It intentionally does not depend on `generatedAt`, source
+metadata such as `sourceRelname`, or the final `/service-worker.js` build output, so identical cache
+inputs keep the same version and that version can be safely embedded into the service worker.
+
+Write the standard public manifest from the CLI with `--domstackManifest`:
+
+```console
+domstack --domstackManifest
+```
+
+Use the object form when you also need programmatic manifest options:
+
+```js
+const site = new DomStack('src', 'public', {
+ domstackManifest: {
+ write: true,
+ exclude: ['blog/**', '**/*.map'],
+ manifestVars: ['offline', 'precache'],
+ policy: {
+ offlineFallbackUrl: '/offline/',
+ },
+ },
+})
+
+const results = await site.build()
+```
+
+Use `domstackManifest: true` when you only want to write the standard `domstack-manifest.json` file.
+Use the object form when you need programmatic `exclude`, `manifestVars`, `policy`, or `hooks` settings.
+Add `write: true` to the object form only when you also want Domstack to write `domstack-manifest.json`.
+Leaving `domstackManifest` unset skips the manifest pipeline unless a `domstack-manifest.settings.*` file exists.
+A settings file enables the pipeline, returns `results.domstackManifest`, and runs manifest hooks, but it does not write a public JSON file unless writing is explicitly enabled.
+The manifest file itself is never included in its own `entries`.
+Site service workers are also omitted from manifest entries so `manifest.version` can be embedded into `/service-worker.js` without a circular hash dependency.
+
+You can also add a `domstack-manifest.settings.js` file anywhere under `src`:
+
+```js
+export default {
+ exclude: ['admin/**'],
+ manifestVars: ['offline', 'precache'],
+ policy: {
+ offlineFallbackUrl: '/offline/',
+ },
+ includeEntry (entry) {
+ if (entry.kind === 'sourcemap') return false
+ if (entry.kind === 'metadata') return false
+ if (entry.manifestVars?.offline === false) return false
+ if (entry.manifestVars?.precache === false) return false
+ return true
+ },
+}
+```
+
+`domstack-manifest.settings.*` supports `.js`, `.mjs`, `.cjs`, `.ts`, `.mts`, and `.cts` when Node's
+TypeScript support is available. It can default export an object or a sync/async function that
+returns an object:
+
+```js
+export default async function domstackManifestSettings () {
+ return {
+ exclude: process.env.INCLUDE_BLOG_OFFLINE === '1'
+ ? ['**/*.map']
+ : ['blog/**', '**/*.map'],
+ }
+}
+```
+
+`domstackManifest.exclude` from programmatic options and `domstack-manifest.settings.*` `exclude` values are combined.
+Exclude patterns are ignore-style patterns checked against both `entry.url` and `entry.outputRelname`.
+Excludes run before `includeEntry(entry)`.
+The `includeEntry(entry)` hook receives the public manifest entry shape, not local filesystem paths.
+Custom public manifest-like files should be written from a `manifestBuilt` hook with `context.writeFile()`.
+
+Each entry also includes best-known service-worker/deployment metadata when domstack can derive it:
+
+- `contentType`: best-known build-time MIME type. This is not a guarantee that every deployment will serve the exact same HTTP `Content-Type` header.
+- `integrity`: SRI-formatted SHA-256 digest derived from the same file hash as `revision`.
+- `urlRevisioned`: whether the public URL already contains a content hash or equivalent revision token.
+- `static`: whether the entry is domstack static browser-loadable output. In domstack/static this includes pages as well as subresources.
+- `role`: normalized runtime purpose such as `navigation`, `subresource`, `worker`, or `metadata`.
+
+For page entries, an explicit string `manifestRole` page var overrides the domstack-derived `role`:
+
+```js
+export default {
+ manifestRole: 'offline-fallback',
+}
+```
+
+For custom manifest data, use `manifestVars` to explicitly expose selected page/app vars on entries for tools that need to inspect them, and use root `policy` for normalized service-worker or integration decisions. `DomstackManifestOptions` lets TypeScript users coordinate the emitted policy shape, emitted entry var shape, and source variable-cascade shape:
+
+```js
+export default {
+ manifestVars: ['offline', 'precache'],
+ policy: {
+ offlineFallbackUrl: '/offline/',
+ },
+}
+```
+
+`manifestVars` can also be a per-entry transform, and `policy` can be a whole-manifest transform that receives final entries:
+
+```js
+export default {
+ manifestVars ({ vars }) {
+ return typeof vars.analyticsLabel === 'string'
+ ? { analyticsLabel: vars.analyticsLabel }
+ : undefined
+ },
+ policy ({ entries }) {
+ return {
+ offlineUrls: entries
+ .filter(entry => entry.manifestVars?.offline === true)
+ .map(entry => entry.url),
+ }
+ },
+}
+```
+
+Only values selected by `manifestVars` are copied into public manifest entries. Root `policy` is emitted once on the manifest. This avoids leaking arbitrary page vars while still letting service workers, Workbox hooks, and deployment tools consume a stable manifest-level policy shape.
+
+### Manifest built hooks
+
+`hooks.manifestBuilt` runs after domstack has finalized `manifest.entries`, `manifest.policy`, and
+`manifest.version`, but before `/service-worker.js` is bundled and before the manifest file is written.
+Hooks can write additional generated output files, or define constants that are available only to the
+final service-worker bundle:
+
+```js
+export default {
+ hooks: {
+ manifestBuilt: [context => {
+ const urls = context.manifest.entries
+ .filter(entry => entry.role === 'navigation')
+ .map(entry => entry.url)
+
+ context.defineServiceWorkerConstant('__APP_NAVIGATION_URLS__', urls)
+ }],
+ },
+}
+```
+
+Then the service worker can declare and use the injected constant:
+
+```js
+/** @type {string[]} */
+const navigationUrls = __APP_NAVIGATION_URLS__
+```
+
+Hook context shape:
+
+```ts
+type DomstackManifestBuiltHookContext = {
+ dest: string
+ manifest: DomstackManifest
+ defineServiceWorkerConstant: (identifier: string, value: unknown) => void
+ writeFile: (outputRelname: string, contents: string | Uint8Array) => Promise
+}
+```
+
+`defineServiceWorkerConstant()` serializes `value` with `JSON.stringify()` and passes it to esbuild's
+`define` option for the final service-worker build. Use it for precomputed service-worker policy, such
+as a vanilla precache list or Workbox-shaped `precacheManifest`, when you do not want a runtime fetch or generated global script.
+
+### Service workers
+
+Put one site service worker source file anywhere under `src` and domstack will build it to a stable
+root `/service-worker.js` output:
+
+```txt
+src/
+ globals/
+ service-worker.js
+```
+
+When Node's TypeScript support is available, the same convention also supports
+`service-worker.ts`, `service-worker.mts`, and `service-worker.cts`. JavaScript projects can use
+`service-worker.js`, `service-worker.mjs`, or `service-worker.cjs`.
+
+Only one site service worker source is allowed. If multiple `service-worker.*` sources are present,
+domstack fails with `DOM_STACK_ERROR_DUPLICATE_SERVICE_WORKER`. Service workers are bundled by
+esbuild, so imports work the same way they do for client bundles and page-scoped web workers. The
+entry filename is intentionally not content-hashed because browser service-worker update checks need
+a stable URL.
+
+Domstack provides build facts to browser-side bundles through esbuild `define` values. The service
+worker is built after the manifest is finalized, so it additionally receives the finalized manifest
+version:
+
+| Define | Value |
+| --- | --- |
+| `process.env.DOMSTACK_MANIFEST_URL` | Standard public URL for the built-in domstack manifest, `/domstack-manifest.json` |
+| `process.env.DOMSTACK_MANIFEST_VERSION` | Finalized manifest version in `/service-worker.js`; `""` in other bundles |
+| `process.env.DOMSTACK_MANIFEST_ENABLED` | `"true"` for one-shot builds with an enabled manifest pipeline, `"false"` when disabled or in watch mode |
+| `process.env.DOMSTACK_SERVICE_WORKER_URL` | Public URL of the site service worker, usually `/service-worker.js`, or `""` when no service worker is present |
+| `process.env.DOMSTACK_SERVICE_WORKER_SCOPE` | Registration scope for the site service worker, usually `/`, or `""` when no service worker is present |
+
+Prefer injecting finalized service-worker policy from `hooks.manifestBuilt` instead of fetching the public manifest at runtime:
+
+```js
+// domstack-manifest.settings.js
+export default {
+ manifestVars: ['offline', 'precache'],
+ hooks: {
+ manifestBuilt: [context => {
+ context.defineServiceWorkerConstant('__APP_CACHE_POLICY__', {
+ version: context.manifest.version,
+ precacheEntries: context.manifest.entries
+ .filter(entry => entry.static === true)
+ .filter(entry => entry.revision)
+ .map(entry => ({
+ url: entry.url,
+ revision: entry.urlRevisioned ? null : entry.revision,
+ integrity: entry.integrity,
+ })),
+ })
+ }],
+ },
+}
+```
+
+Then consume that policy from the service worker:
+
+```js
+// service-worker.js
+const CACHE_PREFIX = 'domstack-precache-'
+const manifestEnabled = process.env.DOMSTACK_MANIFEST_ENABLED === 'true'
+
+self.addEventListener('install', event => {
+ if (!manifestEnabled) return
+ event.waitUntil(precache())
+})
+
+self.addEventListener('fetch', event => {
+ if (event.request.method !== 'GET') return
+ event.respondWith(cacheFirst(event.request))
+})
+
+async function precache () {
+ const policy = __APP_CACHE_POLICY__
+ const cache = await caches.open(CACHE_PREFIX + policy.version)
+ await cache.addAll(policy.precacheEntries.map(entry => new Request(entry.url, {
+ cache: 'reload',
+ credentials: 'same-origin',
+ ...(entry.integrity ? { integrity: entry.integrity } : {}),
+ })))
+}
+
+async function cacheFirst (request) {
+ const cached = await caches.match(request)
+ return cached || fetch(request)
+}
+```
+
+Use `context.writeFile()` from a manifest hook only when you intentionally need a public runtime artifact.
+
+Register the built service worker from your site client code, usually `global.client.js`:
+
+```js
+const serviceWorkerUrl = process.env.DOMSTACK_SERVICE_WORKER_URL
+const serviceWorkerScope = process.env.DOMSTACK_SERVICE_WORKER_SCOPE
+
+if (serviceWorkerUrl && serviceWorkerScope && 'serviceWorker' in navigator) {
+ navigator.serviceWorker.register(serviceWorkerUrl, { scope: serviceWorkerScope })
+}
+```
+
+domstack does not inject this into the default layout. Registration timing, update prompts,
+development opt-outs, and recovery behavior are application policy, so keep that logic in your
+global client or an imported client module.
+
+This keeps domstack's build pipeline to one page/template pass and one manifest reconciliation. Use
+`domstack-manifest.settings.*` `exclude` or `includeEntry(entry)` to keep entries such as source maps, admin
+routes, or blog pages out of the written manifest before the service worker sees it.
+
+Watch mode builds and rebundles site service-worker entries, but it does not write
+`domstack-manifest.json` or return `results.domstackManifest`. Use one-shot builds when testing
+service-worker and PWA cache behavior.
+
+Domstack does not clean the destination directory before a build. During this preview, run clean
+builds for service-worker lifecycle testing and deployments so removed or renamed preview outputs
+(such as `/service-worker.js` or a custom manifest filename) cannot remain as stale files.
+
## Global Assets
There are a few important (and optional) global assets that live anywhere in the `src` directory. If duplicate named files that match the global asset file name pattern are found, a build error will occur until the duplicate file error is resolved.
@@ -1097,6 +1484,9 @@ export const browser = {
```
The exported object is passed to esbuild's [`define`](https://esbuild.github.io/api/#define) options and is available to every js bundle.
+Domstack also reserves `process.env.DOMSTACK_MANIFEST_URL`,
+`process.env.DOMSTACK_MANIFEST_VERSION`, `process.env.DOMSTACK_MANIFEST_ENABLED`,
+`process.env.DOMSTACK_SERVICE_WORKER_URL`, and `process.env.DOMSTACK_SERVICE_WORKER_SCOPE` for generated build facts.
> [!WARNING]
> Setting `define` in [`esbuild.settings.ts`](#esbuild-settingsts) while also using the `browser` export will throw an error. Use one or the other.
@@ -1180,6 +1570,43 @@ Use `GlobalDataFunction` or `AsyncGlobalDataFunction` to type the function
**`renderInnerPage()` is available.** `global.data.js` runs after page initialization has been attempted, and receives `PageData` instances (some may be uninitialized if they failed to initialize), so you can call `renderInnerPage()` here with the same care described above for `page.vars` and other page-dependent access. For examples and performance guidance, see [Accessing rendered page content](#accessing-rendered-page-content).
+### `domstack-manifest.settings.ts`
+
+This is an optional file you can create anywhere.
+It should export a default object or a default sync/async function that returns an object.
+Use this to filter the domstack manifest before hooks receive it, before domstack optionally writes
+`domstack-manifest.json`, and before `results.domstackManifest` is returned.
+
+```js
+/**
+ * @import { DomstackManifestEntry } from '@domstack/static'
+ */
+
+export default {
+ exclude: [
+ 'admin/**',
+ '**/*.map',
+ ],
+ includeEntry,
+}
+
+/**
+ * @param {DomstackManifestEntry} entry
+ */
+function includeEntry (entry) {
+ return entry.kind !== 'metadata'
+}
+```
+
+The supported settings are:
+
+- `exclude` - ignore-style patterns matched against `entry.url` and `entry.outputRelname`.
+- `includeEntry(entry)` - a sync or async function that receives a public `DomstackManifestEntry` and returns `true` to keep it.
+
+The `domstackManifest.exclude` option and `domstack-manifest.settings.*` `exclude` values are combined.
+Excludes run before `includeEntry(entry)`.
+Watch mode builds and rebundles service workers, but it does not write or return the domstack manifest, so `domstack-manifest.settings.*` is only applied during one-shot builds.
+
### `esbuild.settings.ts`
This is an optional file you can create anywhere.
@@ -1643,7 +2070,7 @@ The following diagram illustrates the DomStack build process:
│ │ │ │ │ │
│ • Bundle JS/CSS │ │ • Copy static │ │ • Copy extra │
│ • Generate │ │ files │ │ directories │
-│ metafile │ │ (if enabled) │ │ from opts │
+│ records │ │ • Record files │ │ • Record files │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└───────────────────┼───────────────────┘
@@ -1656,6 +2083,13 @@ The following diagram illustrates the DomStack build process:
│ • Process MD │
│ • Process JS │
│ • Apply layouts │
+ │ • Record outputs │
+ └────────┬─────────┘
+ │
+ ▼
+ ┌──────────────────┐
+ │ Reconcile │
+ │ Output Manifest │
└────────┬─────────┘
│
▼
@@ -1667,6 +2101,7 @@ The following diagram illustrates the DomStack build process:
│ • staticResults │
│ • copyResults │
│ • pageResults │
+ │ • domstackManifest │
│ • warnings │
└──────────────────┘
```
@@ -1675,11 +2110,13 @@ The build process follows these key steps:
1. **Page identification** - Scans the source directory to identify all pages, layouts, templates, and global assets
2. **Destination preparation** - Ensures the destination directory is ready for the build output
-3. **Parallel asset processing** - Three operations run concurrently:
+3. **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 `--copy` options)
-4. **Page building** - Processes all pages, applying layouts and generating final HTML
+4. **Page building** - Processes pages and normal templates, applying layouts and recording outputs
+5. **Manifest reconciliation** - Normalizes recorded outputs, hashes file contents, filters entries, and computes a stable manifest version
+6. **Return results** - Writes the manifest when enabled and returns all build results
This architecture allows for efficient parallel processing of independent tasks while maintaining the correct build order dependencies.
@@ -1741,14 +2178,15 @@ The `buildPages()` step processes pages in parallel with a concurrency limit:
└───────────────────────────────┘
```
-Variable Resolution Layers:
-- **Global vars** - Site-wide variables from `global.vars.js` (resolved once)
-- **Layout vars** - Layout-specific variables from layout functions (resolved once)
+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).
+- **Global data** - Derived variables from `global.data.js`, stamped onto every page after all pages initialize.
+- **Layout vars** - Optional `export const vars` from the selected layout module.
- **Page-specific vars** vary by type:
- - **MD pages**: page.vars.js + builder vars (from frontmatter)
- - **HTML pages**: page.vars.js
- - **JS pages**: exported vars → page.vars.js
-- **Global data** - Derived variables from `global.data.js`, stamped onto every page after all pages initialize (resolved once, after page init)
+ - **MD pages**: `page.vars.js` plus builder vars from frontmatter.
+ - **HTML pages**: `page.vars.js`.
+ - **JS pages**: exported `vars` plus `page.vars.js`.
### Watch Mode
@@ -1758,6 +2196,10 @@ When you run `domstack --watch` (or `domstack -w`), domstack performs an initial
**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.
+Domstack manifests are build-only artifacts. Watch mode builds and rebundles site service-worker
+entries, but it does not write `domstack-manifest.json` or return `results.domstackManifest`.
+Use `domstack --serve` when testing PWA cache lifecycle behavior locally: it runs a normal manifest-enabled build and serves the result without watch-mode filenames or live-reload HTML injection, so service-worker policy matches emitted build bytes. Add `--domstackManifest` if the worker or test needs the public JSON manifest file.
+
#### What triggers what
| Change | Rebuild scope |
@@ -1771,7 +2213,8 @@ When you run `domstack --watch` (or `domstack -w`), domstack performs an initial
| `markdown-it.settings.*` | All `.md` pages |
| `global.data.*` | All pages and templates |
| `global.vars.*` or `esbuild.settings.*` | Full rebuild (esbuild restart + all pages) |
-| `client.js`, `style.css`, `*.layout.css`, `*.layout.client.*`, `global.client.*`, `global.css`, `*.worker.*` | esbuild handles it — no page rebuild |
+| `domstack-manifest.settings.*` | No rebuild in watch mode; domstack manifests are only generated in one-shot builds |
+| `client.js`, `style.css`, `*.layout.css`, `*.layout.client.*`, `global.client.*`, `global.css`, `*.worker.*`, `service-worker.*` | esbuild handles it — no page rebuild |
| Adding or removing an esbuild entry point (e.g. creating a new `client.js`) | esbuild restart + only the affected page(s) |
| Adding or removing any other file | Full rebuild |
diff --git a/agents.md b/agents.md
index 9e03d4f4..e4c499d5 100644
--- a/agents.md
+++ b/agents.md
@@ -3,8 +3,10 @@
- Write Markdown prose with one sentence per line so git diffs stay focused and readable.
- Never use inline type imports.
- Always favor `@import` syntax at the top of JavaScript files for JSDoc types.
+- Add explicit TypeScript lib reference headers when a standalone example file relies on browser or service-worker globals, such as `/// ` for client files or `/// ` for service-worker files.
- This repo does not require TypeScript declaration builds during normal development.
- Type builds are only needed during publish time or when debugging types.
- After running a type build, clean up the generated build files and do not leave them sitting around.
- Use the cleanup scripts in `package.json` for generated type build files.
+- For formatting-only ESLint failures, use `npx eslint --fix` for a quick targeted fix before rerunning lint.
- When handling PR review comments, validate that each comment is correct before making changes; maintainer comments are almost always valid, but review bot comments may be wrong, and after addressing a comment, always reply with what was done.
diff --git a/bin.js b/bin.js
index e5cdbcfa..7fd8d680 100755
--- a/bin.js
+++ b/bin.js
@@ -4,10 +4,11 @@
* @import { BuildStepWarnings, DomStackOpts as DomStackOpts } from './lib/builder.js'
* @import { ArgscloptsParseArgsOptionsConfig } from 'argsclopts'
* @import { Logger as PinoLogger } from 'pino'
+ * @import { BsInstance } from '@domstack/sync'
*/
import { readFile } from 'node:fs/promises'
-import { resolve, join, relative } from 'node:path'
+import { basename, resolve, join, relative } from 'node:path'
import { parseArgs } from 'node:util'
import { printHelpText } from 'argsclopts'
import readline from 'node:readline'
@@ -15,6 +16,7 @@ import process from 'process'
// @ts-expect-error
import tree from 'pretty-tree'
import { inspect } from 'util'
+import { createServer } from '@domstack/sync'
import { packageDirectory } from 'package-directory'
import { readPackage } from 'read-pkg'
import { addPackageDependencies } from 'write-package'
@@ -58,15 +60,15 @@ const options = {
help: 'Build draft pages with the `.draft.{md,js,html}` page suffix.',
default: false
},
- target: {
- type: 'string',
- short: 't',
- help: 'comma separated target strings for esbuild',
- },
noEsbuildMeta: {
type: 'boolean',
help: 'skip writing the esbuild metafile to disk',
},
+ domstackManifest: {
+ type: 'boolean',
+ help: 'write the domstack manifest to disk',
+ },
+
eject: {
type: 'boolean',
short: 'e',
@@ -81,6 +83,14 @@ const options = {
type: 'boolean',
help: 'watch and build the src folder without serving',
},
+ serve: {
+ type: 'boolean',
+ help: 'build once and serve the destination directory without watching',
+ },
+ port: {
+ type: 'string',
+ help: 'port for --serve (default: 3000)',
+ },
copy: {
type: 'string',
help: 'path to directories to copy into dist; can be used multiple times',
@@ -205,8 +215,8 @@ domstack eject actions:
const opts = {}
if (argv['ignore']) opts.ignore = String(argv['ignore']).split(',')
- if (argv['target']) opts.target = String(argv['target']).split(',')
if (argv['noEsbuildMeta']) opts.metafile = false
+ if (argv['domstackManifest']) opts.domstackManifest = true
if (argv['drafts']) opts.buildDrafts = true
if (argv['copy']) {
const copyPaths = Array.isArray(argv['copy']) ? argv['copy'] : [argv['copy']]
@@ -217,6 +227,16 @@ domstack eject actions:
const logger = createDomStackLogger()
opts.logger = logger
const domStack = new DomStack(src, dest, opts)
+ /** @type {BsInstance | null} */
+ let buildServer = null
+
+ if (argv['serve'] && (argv['watch'] || argv['watch-only'])) {
+ throw new Error('--serve cannot be combined with --watch or --watch-only')
+ }
+ if (argv['port'] && !argv['serve']) {
+ throw new Error('--port can only be combined with --serve')
+ }
+ const servePort = argv['port'] ? parsePort(String(argv['port'])) : undefined
process.once('SIGINT', quit)
process.once('SIGTERM', quit)
@@ -226,6 +246,11 @@ domstack eject actions:
await domStack.stopWatching()
logger.info('Watching stopped')
}
+ if (buildServer) {
+ await buildServer.exit()
+ buildServer = null
+ logger.info('Server stopped')
+ }
logger.info('Quitting cleanly')
process.exit(0)
}
@@ -236,6 +261,16 @@ domstack eject actions:
logger.info(tree(generateTreeData(cwd, src, dest, results)))
logWarnings(logger, results?.warnings)
logger.info('\nBuild Success!\n\n')
+ if (argv['serve']) {
+ buildServer = await createServer({
+ server: dest,
+ files: basename(dest),
+ logger: logger.child({ component: 'sync', logPrefix: '[domstack-sync]' }),
+ ...(servePort ? { port: servePort } : {}),
+ snippet: false,
+ })
+ logger.info(`Serving ${relative(cwd, dest)} without watching. Press Ctrl-C to stop.`)
+ }
} catch (err) {
if (!(err instanceof Error || err instanceof AggregateError)) throw new Error('Non-error thrown', { cause: err })
if (err instanceof DomStackAggregateError) {
@@ -259,6 +294,17 @@ domstack eject actions:
}
}
+/**
+ * @param {string} value
+ */
+function parsePort (value) {
+ const port = Number(value)
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
+ throw new Error('--port must be an integer between 1 and 65535')
+ }
+ return port
+}
+
/**
* @param {PinoLogger} logger
* @param {BuildStepWarnings | undefined} warnings
diff --git a/docs/v12-migration.md b/docs/v12-migration.md
index 89f1f6aa..664c91c2 100644
--- a/docs/v12-migration.md
+++ b/docs/v12-migration.md
@@ -10,9 +10,12 @@ Then apply the v12 changes below.
1. [Type exports moved to `@domstack/static/types.js`](#1-type-exports-moved-to-domstackstatictypesjs)
2. [Development Server Uses @domstack/sync](#2-development-server-uses-domstacksync)
3. [Default Layout Uses fragtml](#3-default-layout-uses-fragtml)
-4. [Keep Layout Dependencies Explicit](#4-keep-layout-dependencies-explicit)
-5. [JSX Runtime Is Opt-In](#5-jsx-runtime-is-opt-in)
-6. [Migration Checklist](#6-migration-checklist)
+4. [Layout Modules Can Export Vars](#4-layout-modules-can-export-vars)
+5. [Keep Layout Dependencies Explicit](#5-keep-layout-dependencies-explicit)
+6. [JSX Runtime Is Opt-In](#6-jsx-runtime-is-opt-in)
+7. [CLI `--target` moved to `esbuild.settings.*`](#7-cli---target-moved-to-esbuildsettings)
+8. [Static Cache Manifest and Service Worker Preview](#8-static-cache-manifest-and-service-worker-preview)
+9. [Migration Checklist](#9-migration-checklist)
---
@@ -72,7 +75,29 @@ Markdown output passed to a layout as `children` is one example.
---
-## 4. Keep Layout Dependencies Explicit
+## 4. Layout Modules Can Export Vars
+
+Layouts can now export optional `vars` that are merged into the resolved page variable cascade for pages using that layout. Like page/global vars, layout vars may be an object, a sync function, or an async function.
+
+```ts
+// src/layouts/article.layout.ts
+export const vars = {
+ showSidebar: true,
+ pageType: 'article',
+}
+```
+
+Precedence is:
+
+```txt
+page/frontmatter vars > page.vars.* > layout vars > global.data/global.vars > domstack defaults
+```
+
+This is additive for most sites. If a layout module already exported a named `vars` value for another purpose, that value will now participate in page variable resolution. Rename that export if it was not intended as layout defaults.
+
+---
+
+## 5. Keep Layout Dependencies Explicit
DOMStack only installs dependencies for its bundled defaults.
Your project is responsible for any packages imported by pages, layouts, globals, or browser clients.
@@ -82,7 +107,7 @@ If you migrate those server-side templates to `fragtml`, replace those dependenc
---
-## 5. JSX Runtime Is Opt-In
+## 6. JSX Runtime Is Opt-In
Client `.jsx` and `.tsx` bundles are still supported through esbuild.
Domstack no longer configures Preact as the default JSX runtime.
@@ -123,13 +148,204 @@ export default async function esbuildSettingsOverride (esbuildSettings) {
---
-## 6. Migration Checklist
+## 7. CLI `--target` moved to `esbuild.settings.*`
+
+The `domstack --target` / `domstack -t` CLI flag has been removed in v12. Configure esbuild targets in
+`esbuild.settings.*` instead.
+
+```js
+// src/esbuild.settings.js
+export default function esbuildSettings (opts) {
+ return {
+ ...opts,
+ target: ['es2022', 'chrome120', 'firefox121', 'safari17'],
+ }
+}
+```
+
+Domstack does not set a rolling “modern browser” target by default. If your project needs specific
+syntax lowering, set explicit esbuild targets in this settings file. See
+[esbuild's target docs](https://esbuild.github.io/api/#target) for accepted values.
+
+---
+
+## 8. Static Cache Manifest and Service Worker Preview
+
+v12 adds an unstable-preview manifest pipeline for static caching and first-class site service-worker builds.
+
+These APIs are preview APIs.
+Their names, option shapes, manifest schema, generated output, and runtime semantics may change outside of a major version while they are validated with real PWA use cases.
+Pin `@domstack/static` to an exact version if you rely on this contract.
+
+### Public manifest writing is opt-in
+
+The manifest pipeline can run without writing a public `domstack-manifest.json` file.
+This is useful when your app only needs the finalized manifest in a build hook to inject service-worker policy.
+
+Write the standard public manifest from the CLI with:
+
+```sh
+domstack --domstackManifest
+```
+
+Or write it from programmatic builds with:
+
+```js
+const site = new DomStack('src', 'public', {
+ domstackManifest: true,
+})
+```
+
+Use the object form when you also need filters, manifest vars, root policy, or hooks:
+
+```js
+const site = new DomStack('src', 'public', {
+ domstackManifest: {
+ write: true,
+ exclude: ['blog/**', '**/*.map'],
+ manifestVars: ['offline', 'precache'],
+ policy: {
+ offlineFallbackUrl: '/offline/',
+ },
+ },
+})
+```
+
+If `domstackManifest` is unset, Domstack skips the manifest pipeline unless a `domstack-manifest.settings.*` file exists.
+A settings file enables the pipeline, returns `results.domstackManifest`, and runs manifest hooks, but does not write a public JSON file unless writing is explicitly enabled.
+
+### Add manifest settings when you need cache policy
+
+Apps can add one `domstack-manifest.settings.*` file anywhere under `src`:
+
+```txt
+domstack-manifest.settings.js
+domstack-manifest.settings.mjs
+domstack-manifest.settings.cjs
+domstack-manifest.settings.ts
+domstack-manifest.settings.mts
+domstack-manifest.settings.cts
+```
+
+Use `manifestVars` to explicitly expose selected page, layout, global, or default vars on manifest entries.
+Use `policy` for root-level application policy.
+Use `includeEntry(entry)` and `exclude` to filter final public output entries.
+
+```ts
+import type { DomstackManifestOptions } from '@domstack/static/types.js'
+
+const settings = {
+ manifestVars: ['offline', 'precache'],
+ policy: {
+ offlineFallbackUrl: '/offline/',
+ },
+ exclude: ['admin/**', 'blog/**', '**/*.map'],
+ includeEntry (entry) {
+ if (entry.kind === 'metadata') return false
+ if (entry.kind === 'sourcemap') return false
+ if (entry.manifestVars?.offline === false) return false
+ if (entry.manifestVars?.precache === false) return false
+ return true
+ },
+} satisfies DomstackManifestOptions
+
+export default settings
+```
+
+### Prefer manifest hooks over runtime manifest fetches
+
+The primary service-worker integration point is `hooks.manifestBuilt`.
+This hook receives the finalized manifest before the final `/service-worker.js` bundle is emitted.
+Use `context.defineServiceWorkerConstant()` to inject JSON-serializable policy directly into the service-worker bundle.
+
+```ts
+import type {
+ DomstackManifestBuiltHookContext,
+} from '@domstack/static/types.js'
+
+export async function injectServiceWorkerPolicy (
+ context: DomstackManifestBuiltHookContext
+): Promise {
+ context.defineServiceWorkerConstant('__APP_CACHE_POLICY__', {
+ version: context.manifest.version,
+ precacheEntries: context.manifest.entries.map(entry => ({
+ url: entry.url,
+ revision: entry.urlRevisioned ? null : entry.revision,
+ })),
+ })
+}
+```
+
+This avoids shipping a runtime manifest file just so a service worker can discover the build output list.
+If you intentionally need a public custom artifact, write it from the hook with `context.writeFile()`.
+
+### Add one site service worker when needed
+
+Domstack reserves one site service-worker source filename:
+
+```txt
+service-worker.js
+service-worker.mjs
+service-worker.cjs
+service-worker.ts
+service-worker.mts
+service-worker.cts
+```
+
+The source may live anywhere under `src`, but only one is allowed.
+Domstack bundles it to a stable root output path:
+
+```txt
+/service-worker.js
+```
+
+Domstack does not register the service worker for you.
+Your app owns registration timing, update prompts, local-development opt-outs, reset/recovery behavior, route filtering, offline fallback behavior, and runtime cache policy.
+
+The service worker can read Domstack's build-time browser defines:
+
+| Define | Value |
+| --- | --- |
+| `process.env.DOMSTACK_MANIFEST_URL` | Standard public URL for the built-in manifest, `/domstack-manifest.json` |
+| `process.env.DOMSTACK_MANIFEST_VERSION` | Finalized manifest version in `/service-worker.js`; `""` in other bundles |
+| `process.env.DOMSTACK_MANIFEST_ENABLED` | `"true"` for one-shot builds with an enabled manifest pipeline, `"false"` when disabled or in watch mode |
+| `process.env.DOMSTACK_SERVICE_WORKER_URL` | Public URL of the site service worker, usually `/service-worker.js`, or `""` when no service worker is present |
+| `process.env.DOMSTACK_SERVICE_WORKER_SCOPE` | Registration scope for the site service worker, usually `/`, or `""` when no service worker is present |
+
+### Use `--serve` for PWA testing
+
+Watch mode intentionally does not write or return the domstack manifest.
+It still rebuilds the site service worker, but watch-mode output is not representative of production cache invalidation.
+
+Use `--serve` when testing PWA install/update/offline lifecycle behavior:
+
+```sh
+domstack --serve
+```
+
+Add `--domstackManifest` only if you also want to serve the public `domstack-manifest.json` file while debugging:
+
+```sh
+domstack --serve --domstackManifest
+```
+
+### Avoid circular manifest dependencies
+
+The built-in manifest file is never included in its own `entries`.
+Site service workers are also omitted from manifest entries.
+This lets Domstack inject the finalized `manifest.version` into `/service-worker.js` without making the manifest hash depend on the service-worker hash.
+
+---
+
+## 9. Migration Checklist
- [ ] If you import public types from `@domstack/static`, update those imports to `@domstack/static/types.js`.
- [ ] If you rely on BrowserSync-specific dev-server behavior, test watch mode with `@domstack/sync`.
- [ ] If you rely on the bundled default layout, make sure pages and child layouts return HTML strings or `fragtml` template results, not Preact or HTM VNodes.
+- [ ] If any layout module already exports a named `vars` value, confirm it should now act as layout defaults.
- [ ] If you want to keep the v11 Preact default layout, eject on v11 before upgrading to v12.
- [ ] If your ejected layout or server-side pages still import `htm/preact`, `preact`, or `preact-render-to-string`, keep those dependencies in your own `package.json`.
- [ ] If you want your ejected server-side layout to match the v12 default, migrate its templates to `fragtml` and install `fragtml`.
- [ ] If you use `.jsx` or `.tsx` browser clients, add an `esbuild.settings` file that configures your JSX runtime.
+- [ ] If you use `domstack --target` or `domstack -t`, move that target list to `esbuild.settings.*`.
- [ ] If you use Preact browser clients, keep `preact` in your project dependencies.
diff --git a/eslint.config.js b/eslint.config.js
index caf35f01..cd55418b 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -2,6 +2,7 @@ import neostandard, { resolveIgnoresFromGitignore } from 'neostandard'
export default neostandard({
env: ['browser'],
+ ts: true,
ignores: [
...resolveIgnoresFromGitignore(),
'test-cases/build-errors/src/**/*.js',
diff --git a/examples/basic/package.json b/examples/basic/package.json
index 0c33dec8..c6e3ca68 100644
--- a/examples/basic/package.json
+++ b/examples/basic/package.json
@@ -1,5 +1,5 @@
{
- "name": "basic",
+ "name": "@domstack/basic-example",
"version": "0.0.0",
"description": "",
"type": "module",
@@ -14,14 +14,17 @@
"author": "Bret Comnes (https://bret.io/)",
"license": "MIT",
"devDependencies": {
- "npm-run-all2": "^6.0.0",
- "typescript": "~5.8.2",
- "@voxpelli/tsconfig": "^15.0.0"
+ "npm-run-all2": "^9.0.2",
+ "typescript": "~6.0.3",
+ "@voxpelli/tsconfig": "^16.2.1"
},
"dependencies": {
"@domstack/static": "file:../../.",
"fragtml": "^0.0.9",
- "mine.css": "^9.0.1",
+ "mine.css": "^10.0.3",
"highlight.js": "^11.9.0"
+ },
+ "engines": {
+ "node": "^22.0.0 || >=24.0.0"
}
}
diff --git a/examples/basic/src/js-page/loose-assets/shared-lib.ts b/examples/basic/src/js-page/loose-assets/shared-lib.ts
index f8cf09d1..571df0c5 100644
--- a/examples/basic/src/js-page/loose-assets/shared-lib.ts
+++ b/examples/basic/src/js-page/loose-assets/shared-lib.ts
@@ -9,4 +9,4 @@ const sharedLib: SharedData = {
shared: 'data',
}
-export default sharedLib;
+export default sharedLib
diff --git a/examples/blog/package.json b/examples/blog/package.json
index 468ec337..a9017d14 100644
--- a/examples/blog/package.json
+++ b/examples/blog/package.json
@@ -1,5 +1,5 @@
{
- "name": "blog",
+ "name": "@domstack/blog-example",
"version": "0.0.0",
"description": "A blog example for domstack demonstrating global.data.ts, nested layouts, and feeds.",
"type": "module",
@@ -14,12 +14,15 @@
"author": "Bret Comnes (https://bret.io/)",
"license": "MIT",
"devDependencies": {
- "@voxpelli/tsconfig": "^16.0.0",
- "typescript": "~5.9.2"
+ "@voxpelli/tsconfig": "^16.2.1",
+ "typescript": "~6.0.3"
},
"dependencies": {
"@domstack/static": "file:../../.",
"fragtml": "^0.0.9",
- "mine.css": "^10.0.0"
+ "mine.css": "^10.0.3"
+ },
+ "engines": {
+ "node": "^22.0.0 || >=24.0.0"
}
}
diff --git a/examples/blog/src/layouts/post.layout.ts b/examples/blog/src/layouts/post.layout.ts
index cd1924c7..703f573b 100644
--- a/examples/blog/src/layouts/post.layout.ts
+++ b/examples/blog/src/layouts/post.layout.ts
@@ -43,29 +43,35 @@ const postLayout: LayoutFunction = (args)
${/* Publish date */''}
- ${publishDate ? html`
+ ${publishDate
+? html`
- ` : null}
+ `
+: null}
${/* Updated date */''}
- ${updatedDate ? html`
+ ${updatedDate
+? html`
- ` : null}
+ `
+: null}
${/* Tags */''}
- ${vars.tags && vars.tags.length > 0 ? html`
+ ${vars.tags && vars.tags.length > 0
+? html`