Migration Guide: domstack v12

This guide covers breaking and notable changes when moving from domstack v11 to v12.

If you are migrating from top-bun, first follow the historical v11 guide at v11-migration.md. Then apply the v12 changes below.

Table of Contents

  1. Type exports moved to @domstack/static/types.js
  2. Development Server Uses @domstack/sync
  3. Default Layout Uses fragtml
  4. Layout Modules Can Export Vars
  5. Keep Layout Dependencies Explicit
  6. JSX Runtime Is Opt-In
  7. mine.css v11 and CSS Cascade Layers
  8. CLI --target moved to esbuild.settings.*
  9. Static Cache Manifest and Service Worker Preview
  10. Migration Checklist

1. Type exports moved to @domstack/static/types.js

In v12, runtime values remain available from @domstack/static. Public types have moved to a dedicated type-only entry.

// Before v12
import type { LayoutFunction, PageFunction, DomStackOpts } from '@domstack/static'

// v12+
import type { LayoutFunction, PageFunction, DomStackOpts } from '@domstack/static/types.js'

2. Development Server Uses @domstack/sync

Watch/serve mode now uses @domstack/sync for the local development server.

This provides live reload, CSS injection, ghost mode, and the UI panel. If you were relying on BrowserSync-specific behavior or output, update your expectations around logs, access URLs, and reload handling.


3. Default Layout Uses fragtml

The bundled default root.layout.js now uses fragtml for server-side HTML rendering.

If you rely on the bundled default layout, make sure your pages and child layouts return compatible values. The v12 default layout accepts HTML strings and fragtml template results. It does not render Preact or HTM VNodes.

If you already ejected or provide your own root layout, you do not have to change that layout for v12. Keep the dependencies that your layout imports in your own package.json.

If you want to update to v12 while keeping the v11 Preact default layout, run domstack --eject on v11 before upgrading. Then keep htm, preact, and preact-render-to-string installed after the upgrade.

If you want to migrate an ejected Preact/HTM layout to the v12 default style, update your layout imports and rendering code from Preact/HTM to fragtml.

// Before
import { html } from 'htm/preact'
import { render } from 'preact-render-to-string'
// After
import { html, raw, render } from 'fragtml'

Use raw(htmlString) when intentionally inserting already-rendered HTML. Markdown output passed to a layout as children is one example.


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.

// src/layouts/article.layout.ts
export const vars = {
  showSidebar: true,
  pageType: 'article',
}

Precedence is:

page/frontmatter vars > page.vars.* > layout vars > global.data/global.vars > domstack defaults

Layout vars were added so shared defaults—especially manifest and service-worker policy defaults—can participate in the normal variable cascade instead of being mixed into rendered output by each layout. If an existing layout manually merges defaults into the values it passes to a child layout or template, move those defaults to the layout’s exported vars so page and frontmatter vars can override them consistently.

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.

If your ejected layout or server-side pages import htm/preact, preact, or preact-render-to-string, keep those packages in your own package.json. If you migrate those server-side templates to fragtml, replace those dependencies with fragtml.


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.

If your browser client code uses JSX or TSX, install the runtime you want and configure it with esbuild.settings.

For Preact:

npm install preact
// src/esbuild.settings.js
export default async function esbuildSettingsOverride (esbuildSettings) {
  esbuildSettings.jsx = 'automatic'
  esbuildSettings.jsxImportSource = 'preact'

  return esbuildSettings
}

For React:

npm install react react-dom
// src/esbuild.settings.js
export default async function esbuildSettingsOverride (esbuildSettings) {
  esbuildSettings.jsx = 'automatic'
  esbuildSettings.jsxImportSource = 'react'

  return esbuildSettings
}

7. mine.css v11 and Optional CSS Layers

DOMStack v12 updates its bundled default stylesheet from mine.css v10 to v11. Sites that use the bundled default layout and stylesheet receive the update without changing their source. The migration steps below apply only when a project imports mine.css directly, has ejected DOMStack’s defaults, or has customized behavior removed by mine.css v11. Review the complete mine.css v11 migration guide in those cases.

mine.css v11 is CSS-only. Its package root now resolves to the main stylesheet, and its JavaScript theme switcher is no longer published. Direct consumers should replace the old deep import with the package root:

@import 'mine.css';

Direct or ejected consumers should remove JavaScript imports of mine.css or mine.css/dist/theme-switcher.js, calls to toggleTheme(), stored theme state, theme controls, and .light-mode or .dark-mode rules. Custom root layouts that use mine.css should include <meta name="color-scheme" content="light dark"> and use prefers-color-scheme for application-specific dark styles. If an ejected stylesheet uses Highlight.js, load a light theme normally and a dark theme conditionally instead of applying one dark theme in both modes.

The optional mine.css layout remains a separate import. DOMStack’s default stylesheet imports it explicitly, and ejected sites that want the same layout should continue to do so.

Optional cascade-layer pattern

The main mine.css stylesheet places its framework rules in the low-priority mine layer. DOMStack’s default stylesheet imports mine.css normally and places its optional layout and Highlight.js sidecars in domstack.default:

@import 'mine.css';
@import 'mine.css/dist/layout.css' layer(domstack.default);
@import 'highlight.js/styles/github.css' layer(domstack.default);
@import 'highlight.js/styles/github-dark-dimmed.css' layer(domstack.default) (prefers-color-scheme: dark);

Custom stylesheets do not have to use layers. Unlayered author rules override normal declarations in mine and domstack.default.

Projects that want explicit layers can let each DOMStack stylesheet declare only its own scope:

/* global.css */
@layer domstack.global {
  /* Site-wide rules */
}
/* article.layout.css */
@layer domstack.layout {
  /* Layout rules */
}
/* style.css */
@layer domstack.page {
  /* Page rules */
}

No stylesheet needs to enumerate the other scopes. DOMStack loads default, global, layout, and page styles in that order, so the layers are first encountered with the corresponding low-to-high precedence. This is a recommended organizational pattern, not a migration requirement.

mine.css v11 intentionally changes typography, forms, tables, media framing, motion, focus treatment, and the optional .mine-layout width. Direct, ejected, or heavily customized consumers should review those surfaces against the upstream migration guide. The distributed CSS uses native CSS nesting, and mine.css requires Node.js 22 or newer and npm 10 or newer for installation.


8. 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.

// 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 for accepted values.


9. 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.

Add a site service worker

A service worker is the browser entrypoint that installs cache contents, intercepts requests, and applies offline/update behavior. Domstack now discovers one service-worker.* source module anywhere under src and bundles it to the stable public URL /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

Configure the manifest in a settings module

Add one domstack-manifest.settings.* module anywhere under src, using the same supported module extensions as other Domstack settings files. The settings module enables the manifest pipeline and is the primary place to select application vars, define root policy, filter entries, and register build hooks.

Use manifestVars to expose selected page, layout, global, or default vars on manifest entries. Use policy for values shared by the whole application. Use includeEntry(entry) and exclude to filter final public output entries.

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

A settings module returns results.domstackManifest and runs manifest hooks, but does not write public JSON unless writing is explicitly enabled.

Inject finalized policy into the service worker

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.

import type {
  DomstackManifestBuiltHookContext,
} from '@domstack/static/types.js'

export async function injectServiceWorkerPolicy (
  context: DomstackManifestBuiltHookContext
): Promise<void> {
  context.defineServiceWorkerConstant('__APP_CACHE_POLICY__', {
    version: context.manifest.version,
    precacheEntries: context.manifest.entries.map(entry => ({
      url: entry.url,
      revision: entry.urlRevisioned ? null : entry.revision,
    })),
  })
}

Bundling the finalized URL/revision list into the service worker is the standard precache workflow: the installed worker receives policy for exactly the outputs produced by the same build. If you intentionally need a separate custom artifact, write it from the hook with context.writeFile().

Optionally write a public manifest

Most service-worker integrations only need the settings module and manifestBuilt hook. Write /domstack-manifest.json when another runtime or deployment tool needs to fetch the manifest:

domstack --domstackManifest

Programmatic builds can opt in with domstackManifest: true; the object form is available for callers that need to override settings or writing behavior.

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:

domstack --serve

Add --domstackManifest only if you also want to serve the public domstack-manifest.json file while debugging:

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.


10. Migration Checklist