Build Output Manifest

Status: Implemented unstable preview

Domstack has an implemented build-output manifest preview.

The manifest pipeline is build-time first.

It exists primarily for domstack-manifest.settings.*, hooks.manifestBuilt, deployment metadata, auditing, and optional public domstack-manifest.json output.

Service-worker integrations now prefer injected constants from manifestBuilt hooks instead of runtime-fetching a generated manifest or policy file.

The API is still documented as preview-quality because service-worker and PWA use cases are actively shaping the final ergonomics.

Current implementation

The implemented pipeline is:

builder()
  identifyPages()
  ensureDest()
  Promise.all(
    buildEsbuild()  -> output records for app bundles, excluding final /service-worker.js
    buildStatic()   -> output records
    buildCopy()     -> output records
  )
  buildPages()      -> output records for pages/templates
  reconcileDomstackManifest({ dest, records }) -> manifest + conflict warnings
    when a manifest consumer exists
  run manifestBuilt hooks
  build /service-worker.js with finalized manifest version and hook-defined constants
  optionally write domstack-manifest.json

A manifest consumer exists when either:

domstack-manifest.json is not written by default.

The CLI writes it only when --domstackManifest is passed.

Programmatic builds return results.domstackManifest when a manifest consumer exists.

Service worker relationship

Production service workers are built after the manifest is finalized.

They are intentionally omitted from the manifest entries and version hash.

This avoids a circular dependency where /service-worker.js would depend on manifest.version while also changing manifest.version.

Instead, service workers receive:

When manifest-driven service-worker policy changes, the final /service-worker.js bytes change, and the browser’s normal service-worker update lifecycle runs.

Watch mode does not build a manifest.

Watch mode still builds the site’s /service-worker.js, but sets process.env.DOMSTACK_MANIFEST_ENABLED to "false" and leaves the manifest version empty.

Domstack does not automatically add unregister or cache-cleanup behavior. A site service worker can use the disabled-manifest signal to implement that behavior, as the native offline example does.

Watch mode disables esbuild splitting for the service-worker build so /service-worker.js remains parseable during production-to-watch cleanup, even for older classic-worker registrations.

Manifest built hook API

Status: implemented.

domstack-manifest.settings.* and programmatic domstackManifest.hooks can register manifestBuilt hooks.

Hooks receive the finalized manifest after entry reconciliation, filtering, manifest variables, root policy, and manifest.version are resolved.

type DomstackManifestBuiltHookContext<Policy, ManifestVars> = {
  dest: string
  manifest: DomstackManifest<Policy, ManifestVars>
  defineServiceWorkerConstant: (identifier: string, value: unknown) => void
  writeFile: (outputRelname: string, contents: string | Uint8Array) => Promise<void>
}

defineServiceWorkerConstant() serializes value with JSON.stringify() and passes it to esbuild’s define option for the final service-worker build.

Use it for service-worker policy, Workbox precache data, or any other build-time data that should not require a runtime fetch.

writeFile() writes custom generated artifacts into dest.

Use it for deployment metadata or public files that intentionally need their own URL.

Current manifest shape

Relevant public entry fields include:

type DomstackManifestEntry<ManifestVars = Record<string, unknown>> = {
  outputRelname: string
  kind: DomstackManifestKind
  url: string
  revision: string | null
  bytes: number | null
  sourceRelname?: string
  entryPoint?: string
  pagePath?: string
  pageUrl?: string
  templatePath?: string
  contentType?: string
  integrity?: string
  manifestVars?: ManifestVars
  urlRevisioned?: boolean
  static?: boolean
  role?: string
  page?: {
    path: string
    url: string
  }
}

Relevant root fields include:

type DomstackManifest<Policy, ManifestVars> = {
  $schema: typeof DOMSTACK_MANIFEST_SCHEMA_ID
  version: string
  generatedAt: string
  entries: DomstackManifestEntry<ManifestVars>[]
  policy?: Policy
}

version is a SHA-256 hex digest derived from stable cache-relevant manifest data.

It excludes generatedAt and excludes the final /service-worker.js output.

Manifest variables and policy

manifestVars are selected from the resolved page variable cascade.

The current cascade is:

domstack defaults
-> global vars
-> global data
-> layout vars
-> page vars
-> page builder vars/frontmatter

Later sources override earlier sources.

Layouts can export vars with the same async/sync contract as page/global vars.

manifestVars can be configured as an array of variable names or a transform function.

Root policy is a single freeform object for the whole manifest.

Per-entry policy is represented through selected manifestVars, not a separate per-entry policy object.

Stacked examples validating the preview

Two examples added in the stacked follow-up changes validate the preview against native and Workbox service-worker implementations.

examples/static-mpa-offline demonstrates a domstack-native service worker.

It injects manifest-shaped service-worker policy directly into /service-worker.js with defineServiceWorkerConstant().

The service worker consumes Domstack manifest entries directly and derives cache behavior from:

examples/static-mpa-workbox-offline demonstrates a Workbox service worker.

It injects a Workbox-oriented policy constant into /service-worker.js.

The service worker passes policy.precacheManifest to Workbox and maps app-specific runtime/network-only/fallback policy to Workbox routing and strategies.

Public schema artifact

lib/domstack-manifest/schema.json is currently kept as a public validation/documentation artifact.

Runtime/build behavior does not require loading this file.

It is useful for users who opt into writing domstack-manifest.json and want editor or deployment validation.

If the written manifest is de-emphasized further, this schema file could become optional or be replaced by docs-only schema publication.

Current non-goals

Potential follow-ups