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.

288 changes: 282 additions & 6 deletions README.md

Large diffs are not rendered by default.

89 changes: 63 additions & 26 deletions bin.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node

/**
* @import {DomStackOpts as DomStackOpts} from './lib/builder.js'
* @import {DomStackOpts as DomStackOpts, Results} from './lib/builder.js'
* @import { ArgscloptsParseArgsOptionsConfig } from 'argsclopts'
*/

Expand All @@ -14,6 +14,7 @@ import process from 'process'
// @ts-expect-error
import tree from 'pretty-tree'
import { inspect } from 'util'
import browserSync from 'browser-sync'
import { packageDirectory } from 'package-directory'
import { readPackage } from 'read-pkg'
import { addPackageDependencies } from 'write-package'
Expand Down Expand Up @@ -65,6 +66,14 @@ const options = {
type: 'boolean',
help: 'skip writing the esbuild metafile to disk',
},
domstackManifest: {
type: 'string',
help: 'write the domstack manifest to this filename',
},
noDomstackManifest: {
type: 'boolean',
help: 'skip writing the domstack manifest to disk',
},
eject: {
type: 'boolean',
short: 'e',
Expand All @@ -79,6 +88,10 @@ 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',
},
copy: {
type: 'string',
help: 'path to directories to copy into dist; can be used multiple times',
Expand Down Expand Up @@ -205,6 +218,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['noDomstackManifest']) opts.domstackManifest = false
if (argv['domstackManifest']) {
opts.domstackManifest = {
...(typeof opts.domstackManifest === 'object' ? opts.domstackManifest : {}),
filename: String(argv['domstackManifest']),
}
}
if (argv['drafts']) opts.buildDrafts = true
if (argv['copy']) {
const copyPaths = Array.isArray(argv['copy']) ? argv['copy'] : [argv['copy']]
Expand All @@ -213,6 +233,12 @@ domstack eject actions:
}

const domStack = new DomStack(src, dest, opts)
/** @type {browserSync.BrowserSyncInstance | null} */
let buildServer = null

if (argv['serve'] && (argv['watch'] || argv['watch-only'])) {
throw new Error('--serve cannot be combined with --watch or --watch-only')
}

process.once('SIGINT', quit)
process.once('SIGTERM', quit)
Expand All @@ -223,27 +249,28 @@ domstack eject actions:
console.log(results)
console.log('watching stopped')
}
if (buildServer) {
buildServer.exit()
buildServer = null
console.log('server stopped')
}
console.log('\nquitting cleanly')
process.exit(0)
}

if (!argv['watch'] && !argv['watch-only']) {
try {
const results = await domStack.build()
console.log(tree(generateTreeData(cwd, src, dest, results)))
if (results?.warnings?.length > 0) {
console.log(
'\nThere were build warnings:\n'
)
}
for (const warning of results?.warnings) {
if ('message' in warning) {
console.log(` ${warning.message}`)
} else {
console.warn(warning)
}
}
logBuildResults(cwd, src, dest, results)
console.log('\nBuild Success!\n\n')
if (argv['serve']) {
buildServer = browserSync.create()
buildServer.init({
open: false,
server: dest,
})
console.log(`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) {
Expand All @@ -261,18 +288,28 @@ domstack eject actions:
const initialResults = await domStack.watch({
serve: !argv['watch-only'],
})
console.log(tree(generateTreeData(cwd, src, dest, initialResults)))
if (initialResults?.warnings?.length > 0) {
console.log(
'\nThere were build warnings:\n'
)
}
for (const warning of initialResults?.warnings) {
if ('message' in warning) {
console.log(` ${warning.message}`)
} else {
console.warn(warning)
}
logBuildResults(cwd, src, dest, initialResults)
}
}

/**
* @param {string} cwd
* @param {string} src
* @param {string} dest
* @param {Results} results
*/
function logBuildResults (cwd, src, dest, results) {
console.log(tree(generateTreeData(cwd, src, dest, results)))
if (results?.warnings?.length > 0) {
console.log(
'\nThere were build warnings:\n'
)
}
for (const warning of results?.warnings) {
if ('message' in warning) {
console.log(` ${warning.message}`)
} else {
console.warn(warning)
}
}
}
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.
### `domstack-manifest.settings.js` (and `.ts`, `.mjs`, `.mts`, `.cjs`, `.cts`)

Now treated as the domstack manifest settings file. Its default export can return `exclude`
patterns and an `includeEntry(entry)` filter for `domstack-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`, `domstack-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
55 changes: 55 additions & 0 deletions examples/pwa/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# DOMStack PWA Example

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

It demonstrates:

- A `service-worker.js` source that Domstack bundles to `/service-worker.js`.
- A `domstack-manifest.settings.js` file that filters the generated domstack 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`.
- Verbose console logging in the window runtime and service worker so the lifecycle is easy to inspect while learning or testing.

## Running

```bash
cd examples/pwa
npm install
npm run serve
```

Service workers require a secure origin. `localhost` is allowed by browsers, and `npm run serve`
runs a manifest-enabled build so the PWA path works there. Use `npm run watch` for development
without a sticky service worker cache. 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
domstack-manifest.settings.js # Filters the written domstack manifest
manifest.webmanifest # Web app manifest copied as a static asset
service-worker.js # Site service worker entry
pwa/
cache-policy.js # Shared domstack 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-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:

- `domstack-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 domstack manifest, so use `npm run serve` when testing the offline lifecycle.
22 changes: 22 additions & 0 deletions examples/pwa/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@domstack/pwa-example",
"version": "0.0.0",
"description": "DOMStack PWA offline cache example",
"type": "module",
"scripts": {
"start": "npm run serve",
"build": "npm run clean && domstack",
"clean": "rm -rf public && mkdir -p public",
"serve": "npm run clean && domstack --serve",
"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 domstack 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 domstack 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 `domstack-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/)
Loading