Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ lcov.info
*.d.ts.map
!types/**/*.d.ts
!types/**/*.d.ts.map
pnpm-workspace.yaml
pnpm-lock.yaml
8 changes: 0 additions & 8 deletions .npmignore

This file was deleted.

272 changes: 266 additions & 6 deletions README.md

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ const options = {
type: 'boolean',
help: 'skip writing the esbuild metafile to disk',
},
outputManifest: {
type: 'string',
help: 'write the domstack output manifest to this filename',
},
noOutputManifest: {
type: 'boolean',
help: 'skip writing the domstack output manifest to disk',
},
eject: {
type: 'boolean',
short: 'e',
Expand Down Expand Up @@ -205,6 +213,13 @@ domstack eject actions:
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['noOutputManifest']) opts.outputManifest = false
if (argv['outputManifest']) {
opts.outputManifest = {
...(typeof opts.outputManifest === 'object' ? opts.outputManifest : {}),
filename: String(argv['outputManifest']),
}
}
if (argv['drafts']) opts.buildDrafts = true
if (argv['copy']) {
const copyPaths = Array.isArray(argv['copy']) ? argv['copy'] : [argv['copy']]
Expand Down
11 changes: 8 additions & 3 deletions docs/v11-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ Key differences:

## 8. New Reserved Filenames

Two new filenames are now recognized and processed by domstack. If you have existing files with these names used for other purposes, they will now be treated as special files:
New filenames are now recognized and processed by domstack. If you have existing files with these names used for other purposes, they will now be treated as special files:

### `global.data.js` (and `.ts`, `.mjs`, `.mts`, `.cjs`, `.cts`)

Expand All @@ -218,7 +218,12 @@ export default function (md) {
}
```

If you have a file with either of these names that was serving another purpose, rename it.
### `manifest.settings.js` (and `.ts`, `.mjs`, `.mts`, `.cjs`, `.cts`)

Now treated as the build output manifest settings file. Its default export can return `exclude`
patterns and an `includeOutput(entry)` filter for `domstack-output-manifest.json`.

If you have a file with any of these names that was serving another purpose, rename it.

---

Expand Down Expand Up @@ -370,7 +375,7 @@ const page: PageFunction<MyVars, string> = async ({ vars }) => { ... }
- [ ] Replace `TopBunAggregateError`/`TopBunDuplicatePageError` with `DomStack*` equivalents
- [ ] Replace `TOP_BUN_*` error/warning codes with `DOM_STACK_*`
- [ ] Migrate `postVars` exports from `page.vars.js` to a `global.data.js` default export
- [ ] Rename any files accidentally named `global.data.js`, `markdown-it.settings.js`, `page.md`, or `*.worker.js` that weren't intended for those purposes
- [ ] Rename any files accidentally named `global.data.js`, `manifest.settings.js`, `markdown-it.settings.js`, `page.md`, or `*.worker.js` that weren't intended for those purposes
- [ ] If using both `browser` in `global.vars.js` and `define` in `esbuild.settings.js`, consolidate to one
- [ ] If importing `uhtml-isomorphic` from layouts without it in your own `package.json`, add it explicitly
- [ ] Update any CI/scripts referencing `top-bun-esbuild-meta.json` → `domstack-esbuild-meta.json`
Expand Down
6 changes: 4 additions & 2 deletions examples/markdown-settings/src/markdown-it.settings.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/**
* @import MarkdownIt from 'markdown-it'
*
* Custom Markdown-it Configuration
*
* This file demonstrates how to extend DOMStack's markdown rendering
Expand Down Expand Up @@ -44,8 +46,8 @@ function createContainer (name, defaultTitle, cssClass) {
/**
* Customize the markdown-it instance with additional plugins and renderers
*
* @param {import('markdown-it')} md - The markdown-it instance
* @returns {import('markdown-it')} - The modified markdown-it instance
* @param {MarkdownIt} md - The markdown-it instance
* @returns {Promise<MarkdownIt>} - The modified markdown-it instance
*/
export default async function markdownItSettingsOverride (md) {
// =====================================================
Expand Down
59 changes: 59 additions & 0 deletions examples/pwa/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# DOMStack PWA Example

This example shows a production-style static PWA using Domstack's output manifest and first-class service-worker build support.

It demonstrates:

- A `service-worker.js` source that Domstack bundles to `/service-worker.js`.
- A `manifest.settings.js` file that filters the generated output manifest.
- A global client runtime that registers the worker, handles update prompts, and disables sticky caches during local watch development.
- Offline precaching for app, docs, legal-style pages, static assets, shared chunks, and the web app manifest.
- Excluding `/blog/**`, `/admin/**`, source maps, metadata files, and pages with `precache: false` or `offline: false`.

## Running

```bash
cd examples/pwa
npm install
npm run build
npx browser-sync start --server public --files public
```

Service workers require a secure origin. `localhost` is allowed by browsers, but this example disables PWA behavior on local origins by default so watch-mode builds cannot leave stale caches behind. To intentionally test the production worker locally:

```js
localStorage.setItem('domstack:pwa-dev', '1')
```

Reload after setting that flag. To clear all example workers and caches:

```txt
/?reset-sw=1
```

## Files

```txt
src/
global.client.js # Registers the service worker through pwa/runtime.js
global.css # Site styles
global.vars.js # Shared page variables
manifest.settings.js # Filters the written output manifest
manifest.webmanifest # Web app manifest copied as a static asset
service-worker.js # Site service worker entry
pwa/
cache-policy.js # Shared output-manifest filtering and constants
runtime.js # Browser registration/update/recovery behavior
sw/
*.js # Service-worker install, update, cache, and fetch helpers
```

## Production Pattern

The worker fetches `/domstack-output-manifest.json` during installation and uses the revisioned URLs in that file as its cache plan. The manifest is generated after Domstack reconciles pages, bundles, chunks, worker output, copied static assets, and templates. Application policy stays in app code:

- `manifest.settings.js` decides what can ever enter the manifest.
- `service-worker.js` decides how to install, activate, update, and serve cached responses.
- `global.client.js` decides when to register, prompt, apply updates, or recover from a bad cache.

Watch mode does not write the output manifest, so use `npm run build` when testing the offline lifecycle.
21 changes: 21 additions & 0 deletions examples/pwa/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@domstack/pwa-example",
"version": "0.0.0",
"description": "DOMStack PWA offline cache example",
"type": "module",
"scripts": {
"start": "npm run watch",
"build": "npm run clean && domstack",
"clean": "rm -rf public && mkdir -p public",
"watch": "npm run clean && dom --watch"
},
"keywords": ["domstack", "pwa", "service-worker", "offline"],
"author": "",
"license": "MIT",
"dependencies": {
"@domstack/static": "file:../../."
},
"devDependencies": {
"npm-run-all2": "^9.0.0"
}
}
77 changes: 77 additions & 0 deletions examples/pwa/src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
title: App Shell
---

<section class="app-layout">
<div class="app-summary">
<div>
<h1>Static PWA shell</h1>
<p>This page is built as ordinary static HTML, then the service worker uses Domstack's output manifest to cache the static shell for offline launches.</p>
<div class="pill-row" aria-label="Cache policy">
<span class="pill ok">Pages</span>
<span class="pill ok">Bundles</span>
<span class="pill ok">Static assets</span>
<span class="pill warn">No API cache</span>
</div>
</div>
<div class="status-list" aria-live="polite">
<div class="status-row">
<span>Worker</span>
<strong data-pwa-status>Not registered</strong>
</div>
<div class="status-row">
<span>Cache version</span>
<strong data-pwa-version>Waiting for build manifest</strong>
</div>
<div class="status-row">
<span>Network</span>
<strong data-online-state>Checking</strong>
</div>
</div>
</div>

<ul class="route-grid" aria-label="Example routes">
<li class="route-card">
<h2>Docs</h2>
<p>Docs are part of the first offline bundle.</p>
<a class="button" href="/docs/">Open docs</a>
<div class="pill-row"><span class="pill ok">Precached</span></div>
</li>
<li class="route-card">
<h2>Legal</h2>
<p>Legal-style static pages use the same offline policy as docs.</p>
<a class="button" href="/legal/">Open legal</a>
<div class="pill-row"><span class="pill ok">Precached</span></div>
</li>
<li class="route-card">
<h2>Login</h2>
<p>Auth shells can load offline while submissions remain network-only.</p>
<a class="button" href="/login/">Open login</a>
<div class="pill-row"><span class="pill ok">Precached</span></div>
</li>
<li class="route-card">
<h2>Offline fallback</h2>
<p>Excluded navigations fall back to a small static page.</p>
<a class="button" href="/offline/">Open fallback</a>
<div class="pill-row"><span class="pill ok">Precached</span></div>
</li>
<li class="route-card">
<h2>Blog</h2>
<p>Blog pages are intentionally left out to reduce first install cost.</p>
<a class="button secondary" href="/blog/">Open blog</a>
<div class="pill-row"><span class="pill warn">Excluded</span></div>
</li>
<li class="route-card">
<h2>Admin</h2>
<p>Protected routes should stay network-only.</p>
<a class="button secondary" href="/admin/">Open admin</a>
<div class="pill-row"><span class="pill warn">Excluded</span></div>
</li>
<li class="route-card">
<h2>Opted-out</h2>
<p>Page vars can opt a static route out of precaching.</p>
<a class="button secondary" href="/private/">Open page</a>
<div class="pill-row"><span class="pill warn">Excluded</span></div>
</li>
</ul>
</section>
9 changes: 9 additions & 0 deletions examples/pwa/src/admin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: Admin
precache: false
offline: false
---

# Admin

This static route stands in for server-protected admin pages. It is excluded from the PWA manifest and the service worker treats `/admin/**` as network-only.
13 changes: 13 additions & 0 deletions examples/pwa/src/blog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: Blog
precache: false
offline: false
---

# Blog

This route is present in the static site but filtered out of the first PWA cache by `manifest.settings.js`.

Its page vars also set `precache: false` and `offline: false`, which lets the cache policy reject it without relying only on path names.

- [First post](/blog/first-post/)
9 changes: 9 additions & 0 deletions examples/pwa/src/blog/first-post/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: First Post
precache: false
offline: false
---

# First Post

Blog content is excluded in this example so the first offline install stays small and stable.
23 changes: 23 additions & 0 deletions examples/pwa/src/docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
title: Docs
---

# Docs

This route is intentionally included in the PWA cache. It represents static documentation, legal pages, help pages, or other public content that should remain available after the first online visit.

The service worker gets this page from the generated output manifest instead of a handwritten list.

## Cache Inputs

- Page HTML, including `/docs/`
- Global CSS and JavaScript bundles
- Shared chunks
- Static icons and the web app manifest

## Cache Exclusions

- `/api/**` requests
- `/admin/**` routes
- `/blog/**` routes
- Source maps and Domstack metadata
5 changes: 5 additions & 0 deletions examples/pwa/src/global.client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { initializePwa } from './pwa/runtime.js'

initializePwa().catch(err => {
console.error('Unable to initialize the PWA runtime', err)
})
Loading