@takazudo/zfb 2.10.1 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -198,6 +198,32 @@ schedules before a swap, and `unmountIslands(root, incomingBody)` runs
198
198
  framework cleanup for discarded islands while preserving matching
199
199
  `data-zfb-transition-persist` islands.
200
200
 
201
+ ### Post-mount marker
202
+
203
+ The runtime writes `data-zfb-island-mounted` after the generated `mount()` function
204
+ returns. It applies to both hydrated `data-zfb-island` markers and SSR-skip
205
+ `data-zfb-island-skip-ssr` markers. Consumers can gate pre-hydration styling before the
206
+ runtime's mount function returns, with the practical CSS selector:
207
+
208
+ ```css
209
+ [data-zfb-island]:not([data-zfb-island-mounted]) {
210
+ opacity: 0.7;
211
+ }
212
+ ```
213
+
214
+ This means **"the runtime called the mount function and it returned"**, not **"the
215
+ component is interactive"**. React's `hydrateRoot` is internally concurrent, and the
216
+ generated `mount()` can silently no-op, so the attribute is a lifecycle signal rather
217
+ than proof that interaction is ready.
218
+
219
+ The marker is removed when an island is unmounted. During a body swap, an unchanged
220
+ `data-zfb-transition-persist` island keeps its mounted instance and marker when the
221
+ same id exists in the incoming body; a discarded island has its marker cleared and is
222
+ mounted again from the incoming markup. A props-changed persisted island loses the
223
+ marker while its old instance is torn down and receives it again after the forced
224
+ remount returns. A fresh runtime module strips stale markers from elements it has not
225
+ mounted before, then writes its own marker after mounting.
226
+
201
227
  ## Markdown / GFM config
202
228
 
203
229
  `ZfbConfig.markdown.gfm` controls which GitHub-Flavored-Markdown
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import "./wasm.js";
2
2
  export { ANONYMOUS_COMPONENT_NAME, HYDRATE_MARKER_ATTR, Island, SKIP_SSR_MARKER_ATTR, resolveWhen, type IslandElement, type IslandProps, } from "./island.js";
3
- export { scheduleHydrate, mountIslands, mountNewIslands, cancelPendingIslands, unmountIslands, } from "./runtime.js";
3
+ export { scheduleHydrate, mountIslands, mountNewIslands, cancelPendingIslands, unmountIslands, ISLAND_MOUNTED_ATTR, } from "./runtime.js";
4
4
  export type { IslandManifest, IslandManifestValue } from "./runtime.js";
5
5
  export type { VNode, VNodeArray, VNodeObject } from "./jsx-types.js";
6
6
  export { DEFAULT_WHEN, isWhen, WHEN_VALUES, type When } from "./types.js";
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // root declaration. The compiled module is intentionally empty at runtime.
9
9
  import "./wasm.js";
10
10
  export { ANONYMOUS_COMPONENT_NAME, HYDRATE_MARKER_ATTR, Island, SKIP_SSR_MARKER_ATTR, resolveWhen, } from "./island.js";
11
- export { scheduleHydrate, mountIslands, mountNewIslands, cancelPendingIslands, unmountIslands, } from "./runtime.js";
11
+ export { scheduleHydrate, mountIslands, mountNewIslands, cancelPendingIslands, unmountIslands, ISLAND_MOUNTED_ATTR, } from "./runtime.js";
12
12
  export { DEFAULT_WHEN, isWhen, WHEN_VALUES } from "./types.js";
13
13
  // `defaultComponents` (htmlOverrides convention) is re-exported from the
14
14
  // root entry point so `import { defaultComponents } from "@takazudo/zfb"` is the
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,iFAAiF;AACjF,uDAAuD;AACvD,gFAAgF;AAChF,uCAAuC;AAEvC,yEAAyE;AACzE,2EAA2E;AAC3E,OAAO,WAAW,CAAC;AAEnB,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,MAAM,EACN,oBAAoB,EACpB,WAAW,GAGZ,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,eAAe,EACf,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,cAAc,GACf,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAa,MAAM,YAAY,CAAC;AAE1E,yEAAyE;AACzE,iFAAiF;AACjF,oEAAoE;AACpE,+FAA+F;AAC/F,qCAAqC;AACrC,kEAAkE;AAClE,sEAAsE;AACtE,wEAAwE;AACxE,YAAY;AACZ,OAAO,EACL,YAAY,GAUb,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAEtD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,SAAS,EACT,SAAS,EACT,SAAS,EACT,WAAW,EACX,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,SAAS,EACT,iBAAiB,EACjB,kBAAkB,GAInB,MAAM,cAAc,CAAC","sourcesContent":["// Public entry point for the \"@takazudo/zfb\" package.\n//\n// User TSX pages reach this module via `import { Island } from \"@takazudo/zfb\"`.\n// The hydration runtime (Sub 3) reaches the helper via\n// `import { scheduleHydrate } from \"@takazudo/zfb/runtime\"` (or by inlining the\n// same logic; coordinated separately).\n\n// Keep the global Wasm declaration reachable from this package's emitted\n// root declaration. The compiled module is intentionally empty at runtime.\nimport \"./wasm.js\";\n\nexport {\n ANONYMOUS_COMPONENT_NAME,\n HYDRATE_MARKER_ATTR,\n Island,\n SKIP_SSR_MARKER_ATTR,\n resolveWhen,\n type IslandElement,\n type IslandProps,\n} from \"./island.js\";\nexport {\n scheduleHydrate,\n mountIslands,\n mountNewIslands,\n cancelPendingIslands,\n unmountIslands,\n} from \"./runtime.js\";\nexport type { IslandManifest, IslandManifestValue } from \"./runtime.js\";\nexport type { VNode, VNodeArray, VNodeObject } from \"./jsx-types.js\";\nexport { DEFAULT_WHEN, isWhen, WHEN_VALUES, type When } from \"./types.js\";\n\n// `defaultComponents` (htmlOverrides convention) is re-exported from the\n// root entry point so `import { defaultComponents } from \"@takazudo/zfb\"` is the\n// canonical access path. Each named override is also re-exported so\n// consumers can tree-shake-import a single one (`import { ContentLink } from \"@takazudo/zfb\"`)\n// without dragging in the whole map.\n// Plugin lifecycle types + `definePlugin` identity helper. Plugin\n// authors typically import these from \"@takazudo/zfb/plugins\" but the\n// root entry re-exports them so simple plugins can pull everything from\n// one path.\nexport {\n definePlugin,\n type ZfbBuildHookContext,\n type ZfbDevMiddlewareContext,\n type ZfbDevMiddlewareHandler,\n type ZfbDevMiddlewareRequest,\n type ZfbDevMiddlewareResponse,\n type ZfbPlugin,\n type ZfbPluginLogger,\n type ZfbPreviewMiddlewareContext,\n type ZfbPreviewMiddlewareHandler,\n} from \"./plugins.js\";\n\nexport { slugify, SlugAllocator } from \"./slugify.js\";\n\nexport { clientScript } from \"./client-script.js\";\n\nexport {\n ContentBlockquote,\n ContentCode,\n ContentH2,\n ContentH3,\n ContentH4,\n ContentLink,\n ContentOl,\n ContentParagraph,\n ContentStrong,\n ContentTable,\n ContentUl,\n defaultComponents,\n mergeMdxComponents,\n type ContentComponentElement,\n type ContentComponentProps,\n type MdxComponents,\n} from \"./content.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,iFAAiF;AACjF,uDAAuD;AACvD,gFAAgF;AAChF,uCAAuC;AAEvC,yEAAyE;AACzE,2EAA2E;AAC3E,OAAO,WAAW,CAAC;AAEnB,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,MAAM,EACN,oBAAoB,EACpB,WAAW,GAGZ,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,eAAe,EACf,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,cAAc,EACd,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAa,MAAM,YAAY,CAAC;AAE1E,yEAAyE;AACzE,iFAAiF;AACjF,oEAAoE;AACpE,+FAA+F;AAC/F,qCAAqC;AACrC,kEAAkE;AAClE,sEAAsE;AACtE,wEAAwE;AACxE,YAAY;AACZ,OAAO,EACL,YAAY,GAUb,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAEtD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,SAAS,EACT,SAAS,EACT,SAAS,EACT,WAAW,EACX,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,SAAS,EACT,iBAAiB,EACjB,kBAAkB,GAInB,MAAM,cAAc,CAAC","sourcesContent":["// Public entry point for the \"@takazudo/zfb\" package.\n//\n// User TSX pages reach this module via `import { Island } from \"@takazudo/zfb\"`.\n// The hydration runtime (Sub 3) reaches the helper via\n// `import { scheduleHydrate } from \"@takazudo/zfb/runtime\"` (or by inlining the\n// same logic; coordinated separately).\n\n// Keep the global Wasm declaration reachable from this package's emitted\n// root declaration. The compiled module is intentionally empty at runtime.\nimport \"./wasm.js\";\n\nexport {\n ANONYMOUS_COMPONENT_NAME,\n HYDRATE_MARKER_ATTR,\n Island,\n SKIP_SSR_MARKER_ATTR,\n resolveWhen,\n type IslandElement,\n type IslandProps,\n} from \"./island.js\";\nexport {\n scheduleHydrate,\n mountIslands,\n mountNewIslands,\n cancelPendingIslands,\n unmountIslands,\n ISLAND_MOUNTED_ATTR,\n} from \"./runtime.js\";\nexport type { IslandManifest, IslandManifestValue } from \"./runtime.js\";\nexport type { VNode, VNodeArray, VNodeObject } from \"./jsx-types.js\";\nexport { DEFAULT_WHEN, isWhen, WHEN_VALUES, type When } from \"./types.js\";\n\n// `defaultComponents` (htmlOverrides convention) is re-exported from the\n// root entry point so `import { defaultComponents } from \"@takazudo/zfb\"` is the\n// canonical access path. Each named override is also re-exported so\n// consumers can tree-shake-import a single one (`import { ContentLink } from \"@takazudo/zfb\"`)\n// without dragging in the whole map.\n// Plugin lifecycle types + `definePlugin` identity helper. Plugin\n// authors typically import these from \"@takazudo/zfb/plugins\" but the\n// root entry re-exports them so simple plugins can pull everything from\n// one path.\nexport {\n definePlugin,\n type ZfbBuildHookContext,\n type ZfbDevMiddlewareContext,\n type ZfbDevMiddlewareHandler,\n type ZfbDevMiddlewareRequest,\n type ZfbDevMiddlewareResponse,\n type ZfbPlugin,\n type ZfbPluginLogger,\n type ZfbPreviewMiddlewareContext,\n type ZfbPreviewMiddlewareHandler,\n} from \"./plugins.js\";\n\nexport { slugify, SlugAllocator } from \"./slugify.js\";\n\nexport { clientScript } from \"./client-script.js\";\n\nexport {\n ContentBlockquote,\n ContentCode,\n ContentH2,\n ContentH3,\n ContentH4,\n ContentLink,\n ContentOl,\n ContentParagraph,\n ContentStrong,\n ContentTable,\n ContentUl,\n defaultComponents,\n mergeMdxComponents,\n type ContentComponentElement,\n type ContentComponentProps,\n type MdxComponents,\n} from \"./content.js\";\n"]}
package/dist/plugins.d.ts CHANGED
@@ -297,6 +297,8 @@ export type ZfbSetupContext = {
297
297
  * Register a synthetic / package-owned page route. `pattern` uses the
298
298
  * same grammar as `pages/` filenames (`/blog/[slug]`, `/api/dev/x`,
299
299
  * `/docs/[...rest]`).
300
+ * Patterns below `/__paths__/` are reserved for zfb's internal `paths()`
301
+ * endpoint and raise `ReservedRoutePrefix` with this plugin's name.
300
302
  *
301
303
  * - In **build** (package-owned routes), the route is materialised
302
304
  * into a per-build overlay pages root and **prerendered** through
@@ -1 +1 @@
1
- {"version":3,"file":"plugins.js","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,0EAA0E;AAC1E,wEAAwE;AACxE,wEAAwE;AACxE,oEAAoE;AACpE,oEAAoE;AACpE,EAAE;AACF,uEAAuE;AACvE,sEAAsE;AACtE,qEAAqE;AACrE,sEAAsE;AACtE,sEAAsE;AACtE,6CAA6C;AAC7C,EAAE;AACF,wCAAwC;AACxC,EAAE;AACF,qEAAqE;AACrE,mEAAmE;AACnE,oEAAoE;AACpE,oEAAoE;AACpE,+BAA+B;AAib/B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,YAAY,CAAC,MAAiB;IAC5C,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["// `zfb/plugins` — TypeScript helper for the zfb plugin lifecycle.\n//\n// A plugin is a JS module whose default export is a [`ZfbPlugin`] object.\n// `zfb.config.ts` references plugins by `name` (npm bare specifier or a\n// `./`-relative path); the zfb config loader resolves each `name` to an\n// absolute module specifier and the Rust-side plugin host loads the\n// module via dynamic `import()` and dispatches the lifecycle hooks.\n//\n// Sub 3 / issue #108 — initial drop. Three optional hooks: `preBuild`,\n// `postBuild`, `devMiddleware`. Astro-migration epic #253 / sub-issue\n// #255 adds a fourth: `setup`, which runs once before `preBuild` and\n// lets plugins register virtual modules, import aliases, and dev-only\n// injected routes. None of the hooks see real Node IPC objects across\n// the boundary; everything is JSON-friendly.\n//\n// ## Inline functions are NOT supported\n//\n// `PluginConfig` (in `./config.ts`) carries only data. A user cannot\n// inline a function in `zfb.config.ts` — the config goes through a\n// JSON round-trip and any function value would be silently dropped.\n// Plugins must live in their own module (npm package or local file)\n// and be referenced by `name`.\n\n/**\n * Logger handed to every plugin hook. `info`/`warn`/`error` each render on\n * the `zfb dev`/`zfb build`/`zfb preview` terminal at exactly that level,\n * attributed to the plugin: `zfb <level>: [plugin:<name>] <message>`.\n * `console.*` is redirected the same way, but note it maps onto only two\n * underlying streams (stdout -> info, stderr -> warn/error/trace/assert ->\n * error) — `console.warn` therefore renders as `zfb error:`, not\n * `zfb warn:`. Prefer this logger over `console.*` when the level matters.\n */\nexport type ZfbPluginLogger = {\n info(msg: string): void;\n warn(msg: string): void;\n error(msg: string): void;\n};\n\n/**\n * One emitted route in the `postBuild` route manifest (#262).\n * Present on `ctx.routes.routes` so a `postBuild` plugin can iterate\n * every URL the build produced (e.g. to write a `sitemap.xml`).\n */\nexport type ZfbRouteEntry = {\n /** Emitted URL path, e.g. `/`, `/blog/hello/`, `/sitemap.xml`. */\n url: string;\n /** Path under `outDir`, e.g. `index.html`, `blog/hello/index.html`, `sitemap.xml`. */\n output: string;\n /** File extension: `html`, `xml`, `rss`, `txt`, `json`, … */\n extension: string;\n /** Source page module relative to the project root, e.g. `pages/blog/[slug].tsx`. */\n source: string;\n /**\n * `true` when the page is prerendered to disk (default / SSG); `false`\n * when the page exports `prerender = false` and is served by the\n * runtime adapter (SSR — no on-disk artifact under `outDir`).\n *\n * Indexes that enumerate on-disk URLs (sitemap.xml, search-index.json,\n * etc.) should filter `r.prerender !== false` to avoid surfacing SSR\n * routes that have no static output.\n */\n prerender: boolean;\n /**\n * Bound route parameters. Absent for static routes.\n * Dynamic (`[slug]`) params are string scalars; catchall (`[...rest]`)\n * params are string arrays.\n */\n params?: Record<string, string | string[]>;\n};\n\n/**\n * The route manifest exposed on `ctx.routes` during a `postBuild` callback\n * (#262). Sorted by `url` for byte-stable output across runs.\n */\nexport type ZfbRouteManifest = {\n routes: ZfbRouteEntry[];\n};\n\n/**\n * Context passed to `preBuild` and `postBuild`. `outDir` is the\n * resolved absolute path of the configured `outDir` (default\n * `<projectRoot>/dist`). `projectRoot` is the directory containing\n * `zfb.config.ts`.\n *\n * `routes` is **only present on `postBuild`** calls; it is `undefined`\n * on `preBuild`. This is intentional: the route manifest is not\n * available until the build finishes writing `dist/` (#262).\n */\nexport type ZfbBuildHookContext = {\n /** Project root — the directory containing `zfb.config.ts`. */\n projectRoot: string;\n /** Resolved absolute path of the build output directory. */\n outDir: string;\n /** The full loaded `ZfbConfig` (data-only view). */\n config: import(\"./config.js\").ZfbConfig;\n /** Plugin-specific options block, copied verbatim from the matching `PluginConfig.options`. */\n options: Record<string, unknown>;\n /** Logger that wraps the Rust-side `tracing` subscriber. */\n logger: ZfbPluginLogger;\n /**\n * All routes emitted by this build, sorted by URL (#262).\n * Present only on `postBuild` calls; `undefined` on `preBuild`.\n */\n routes?: ZfbRouteManifest;\n};\n\n/**\n * A request handed to a `devMiddleware` handler. Subset of the Node\n * `http.IncomingMessage` surface intentionally — the dev server is\n * Rust-side `axum`, not Node, so we expose only what survives a JSON\n * envelope hop.\n */\nexport type ZfbDevMiddlewareRequest = {\n method: string;\n url: string;\n /** Lower-cased header names → first value. */\n headers: Record<string, string>;\n /** Raw request body; absent for GET/HEAD. UTF-8 only — binary is out of scope for v1 dev plugins. */\n body?: string;\n};\n\n/**\n * Response returned by a `devMiddleware` handler. All fields optional\n * except `status`. `body` may be a string (UTF-8) or a base64-encoded\n * binary payload (set `bodyEncoding` to `\"base64\"` in that case).\n */\nexport type ZfbDevMiddlewareResponse = {\n status: number;\n headers?: Record<string, string>;\n body?: string;\n bodyEncoding?: \"utf8\" | \"base64\";\n};\n\n/**\n * Handler signature for a `devMiddleware` registration. The `next` callback\n * is reserved for future composition; v1 plugins should produce a response\n * directly. Returning `undefined` from the handler signals \"I did not handle\n * this request\" — the dev server then falls through to its built-in routes\n * (the page cache, /__zfb/livereload.js, etc.).\n */\nexport type ZfbDevMiddlewareHandler = (\n req: ZfbDevMiddlewareRequest,\n) => Promise<ZfbDevMiddlewareResponse | undefined> | ZfbDevMiddlewareResponse | undefined;\n\n/**\n * Context passed to `devMiddleware`. The `register` callback installs\n * one handler per URL path prefix. `path` is matched as an exact prefix\n * — a registration on `/doc-history` matches `/doc-history` and\n * `/doc-history/foo`, but NOT `/doc-historyx`.\n */\nexport type ZfbDevMiddlewareContext = {\n projectRoot: string;\n config: import(\"./config.js\").ZfbConfig;\n options: Record<string, unknown>;\n logger: ZfbPluginLogger;\n /** Register an HTTP handler at `path`. Calling twice on the same path overwrites. */\n register(path: string, handler: ZfbDevMiddlewareHandler): void;\n};\n\n/**\n * Handler signature for a `previewMiddleware` registration (#1542).\n * Deliberately reuses [`ZfbDevMiddlewareRequest`] /\n * [`ZfbDevMiddlewareResponse`] verbatim — the wire shape crossing the\n * Rust↔JS boundary is genuinely the SAME for dev and preview (mirrors\n * the Rust side, which shares `DevRequest`/`DevResponse` between both\n * hooks too), so there is nothing preview-specific to say about the\n * request/response contract itself. `next` is likewise reserved for\n * future composition; returning `undefined` signals \"I did not handle\n * this request\" and the preview server falls through to its built-in\n * routes (static-file serving, or the wrangler-backed adapter in\n * adapter mode).\n */\nexport type ZfbPreviewMiddlewareHandler = (\n req: ZfbDevMiddlewareRequest,\n) => Promise<ZfbDevMiddlewareResponse | undefined> | ZfbDevMiddlewareResponse | undefined;\n\n/**\n * Context passed to `previewMiddleware` (#1542). Structurally identical\n * to [`ZfbDevMiddlewareContext`] today — one handler per URL path\n * prefix, matched the same way — but declared as its own named type\n * (unlike the request/response types above, which are reused verbatim)\n * because the *context* is where a hook-specific capability would land\n * first if one were ever added (e.g. something preview-only that\n * `devMiddleware` has no equivalent for). Keeping it a separate\n * declaration costs nothing today and avoids a breaking rename later.\n */\nexport type ZfbPreviewMiddlewareContext = {\n projectRoot: string;\n config: import(\"./config.js\").ZfbConfig;\n options: Record<string, unknown>;\n logger: ZfbPluginLogger;\n /** Register an HTTP handler at `path`. Calling twice on the same path overwrites. */\n register(path: string, handler: ZfbPreviewMiddlewareHandler): void;\n};\n\n/**\n * Loader signature for a virtual-module registration. Must return the\n * **complete ESM module source text** as a string — the bundler /\n * embedded V8 host feeds the returned string in as the module's\n * source verbatim. The loader runs **eagerly**, not lazily on first\n * import: exactly once per `zfb build` run and once per `zfb dev`\n * host boot, during the setup phase right after every plugin's\n * `setup` hook has returned — even if the registered specifier is\n * never imported by any page or module. The resulting source is\n * memoised; every subsequent import of that specifier reuses it,\n * **unless a forced reload is requested** (#2167) — the plugin-host\n * protocol now supports bypassing the memo and re-invoking the loader,\n * intended for a loader whose registration also declares\n * [`watchFiles`](#watchFiles) and needs a fresh read after one of\n * those files changes on disk. `zfb dev` watches every declared\n * [`watchFiles`](#watchFiles) path and re-invokes the owning loader with\n * its memo bypassed when one of them changes (#2169, #2181); `zfb build`\n * invokes each loader exactly once and never re-invokes it. See the\n * Plugins concept page for the full refresh contract.\n * (Under `zfb preview`, `addVirtualModule` registrations are accepted\n * but inert — see [`ZfbSetupContext.command`](#command) — so the\n * loader never runs there.)\n *\n * Example:\n *\n * ```ts\n * addVirtualModule(\"virtual:my-data\", () =>\n * `export default ${JSON.stringify(myJson)}`,\n * );\n * ```\n */\nexport type ZfbVirtualModuleLoader = () => string | Promise<string>;\n\n/**\n * Optional third argument to `addVirtualModule` (#2167).\n */\nexport type ZfbVirtualModuleOptions = {\n /**\n * Extra absolute filesystem paths a `zfb dev` watcher should track on\n * this loader's behalf — useful when the loader's output depends on\n * files it reads directly (e.g. via `node:fs`) rather than static ESM\n * imports the dev bundler would otherwise notice on its own.\n *\n * Every entry **must be an absolute path**: this mirrors\n * `extraWatchPaths`'s absolute-only rule in `zfb.config.ts`, and for\n * the same reason — `watchFiles` entries are never resolved against\n * the project root, so a relative entry has no defined base directory\n * to resolve against. A relative (or otherwise malformed) entry throws\n * at `setup` time.\n */\n watchFiles?: string[];\n};\n\n/**\n * Context passed to the new `setup` hook (#255). Runs once per host\n * boot, in `Config.plugins` declaration order, **before** `preBuild`.\n *\n * `ctx.command` tells the plugin which lifecycle is active so it can\n * gate per-lifecycle registrations. A dev-only mock route stays gated\n * to `\"dev\"`; a package-owned page route is registered unconditionally\n * (it is prerendered during a build and dev-routed during dev):\n *\n * ```ts\n * setup({ command, injectRoute }) {\n * // package-owned page route (rendered in build and dev)\n * injectRoute(\"/preset-page\", \"./pages/preset-page.tsx\");\n * // dev-only mock endpoint\n * if (command === \"dev\") {\n * injectRoute(\"/api/dev/x\", \"./scripts/dev-x.ts\");\n * }\n * }\n * ```\n *\n * The hook's surface is intentionally **closed**: only `injectRoute`,\n * `addVirtualModule`, `addAlias`, and `addClientEntry`. There is no\n * `addRemarkPlugin` / `addRehypePlugin` / `addMarkdownVisitor` — by\n * design (see the concept doc for the rationale). `addVirtualModule`'s\n * optional `watchFiles` argument (#2167) is a registration OPTION on\n * that existing method, not a new closed-surface method — the closed\n * set of four stays exactly four.\n */\nexport type ZfbSetupContext = {\n /**\n * Active zfb command. `\"build\"` during `zfb build`; `\"dev\"` during\n * `zfb dev`; `\"preview\"` during `zfb preview` (#1542). It can guide\n * lifecycle-specific plugin behavior. `injectRoute` registrations are\n * accepted in both `\"dev\"` and `\"build\"`; user `pages/` routes retain\n * precedence over matching injected routes (see\n * [`injectRoute`](#injectRoute)).\n *\n * Under `\"preview\"`, `setup` still fires (Rust-side via the minimal\n * non-V8 `run_preview_setup` path) so plugin-side state\n * initialisation runs, but `zfb preview` serves an ALREADY-BUILT\n * `dist/` verbatim and never re-enters the scan → bundle → render\n * pipeline. Consequently `injectRoute` / `addVirtualModule` /\n * `addAlias` / `addClientEntry` calls made under `\"preview\"` are\n * accepted (for shape-consistency with `\"build\"`/`\"dev\"`) but are\n * **inert** — nothing downstream ever reads them. Only the hook's\n * side effects and a subsequent `previewMiddleware` registration do\n * anything meaningful under `\"preview\"`.\n */\n command: \"build\" | \"dev\" | \"preview\";\n /** Project root — the directory containing `zfb.config.ts`. */\n projectRoot: string;\n /** The full loaded `ZfbConfig` (data-only view). */\n config: import(\"./config.js\").ZfbConfig;\n /** Plugin-specific options block, copied verbatim from `PluginConfig.options`. */\n options: Record<string, unknown>;\n /** Logger that wraps the Rust-side `tracing` subscriber. */\n logger: ZfbPluginLogger;\n\n /**\n * Register an import alias. **Exact-match-only in v1**:\n * `addAlias(\"@/foo\", \"./src/foo.tsx\")` rewrites `import \"@/foo\"`\n * but does NOT match `import \"@/foo/bar\"`. Prefix-matching is\n * explicitly deferred to v2 — switch to one bare alias per file\n * until then.\n *\n * `to` is resolved relative to the project root. Two plugins\n * registering the same `from` with different `to` raises\n * `AliasConflict` and aborts the build.\n */\n addAlias(from: string, to: string): void;\n\n /**\n * Register a virtual module. `specifier` is a bare import\n * specifier (recommended `virtual:` prefix, not enforced).\n * `loader` returns the complete ESM source text as a string and\n * runs **eagerly, once per build/dev-boot during setup** — not\n * lazily at first import (see [`ZfbVirtualModuleLoader`], including\n * its forced-reload amendment).\n *\n * The optional third argument's `watchFiles` (#2167) declares extra\n * absolute filesystem paths a `zfb dev` watcher should track on this\n * loader's behalf — see [`ZfbVirtualModuleOptions`]. Every entry must\n * be an absolute path; a relative entry throws.\n *\n * Two plugins registering the same `specifier` raises\n * `VirtualModuleConflict` and aborts the build.\n */\n addVirtualModule(\n specifier: string,\n loader: ZfbVirtualModuleLoader,\n options?: ZfbVirtualModuleOptions,\n ): void;\n\n /**\n * Register a synthetic / package-owned page route. `pattern` uses the\n * same grammar as `pages/` filenames (`/blog/[slug]`, `/api/dev/x`,\n * `/docs/[...rest]`).\n *\n * - In **build** (package-owned routes), the route is materialised\n * into a per-build overlay pages root and **prerendered** through\n * the normal scan → bundle → render pipeline, so a preset can own a\n * route without the project shipping a `pages/` stub file. A `\"/\"`\n * package route becomes the project's root page when no user\n * `pages/index` exists, enabling a truly empty/absent user `pages/`.\n * A package route whose URL shape collides with a user `pages/` route\n * is dropped (user `pages/` wins). This is the supported, complete path.\n * - In **dev**, both static and dynamic injected routes are rendered\n * by `zfb dev`. Static routes (where the URL equals the pattern,\n * e.g. `/preset-about`) are seeded into the dev route universe at\n * boot; dynamic routes (e.g. `/preset-docs/[slug]`) are rendered\n * on first request via a request-time synthetic entry — params are\n * extracted from the URL by the Hono router inside the live bundle.\n * User `pages/` files take precedence over any injected route of\n * the same shape, including `pages/index` over an injected `\"/\"`.\n * Without a user index, an injected root is staged, seeded, and served\n * like any other static injected route. **HMR:** content the\n * route reads from watched collections live-refreshes normally.\n * Editing the package's **compiled entrypoint under `node_modules`**\n * is NOT watched and requires a `zfb dev` restart (restart-only\n * contract — a published package is not project source). **Per-route\n * data:** an injected route loads per-route data via a **dynamic\n * route's `paths()` export** (which returns `{ params, props }`);\n * `getStaticProps` on a package page is not forwarded by the overlay\n * (only `default` + the `prerender` hint are forwarded — same as\n * `zfb build`). A route that needs per-route data should be a\n * dynamic route whose `paths()` reads the data.\n *\n * `opts.prerender` controls the route's prerender shape during a\n * build: omit it (or `true`) for the SSG default; `false` marks an\n * SSR-shaped route, which `output: 'static'` rejects. It is build-only\n * metadata and ignored in dev.\n *\n * Two plugins registering the same `pattern` (or one plugin\n * re-registering it with a different entrypoint) raises\n * `InjectRouteConflict`.\n */\n injectRoute(pattern: string, entrypoint: string, opts?: { prerender?: boolean }): void;\n\n /**\n * Register a package-owned client-side side-effect entry (#1196).\n *\n * `entrypoint` **must** point to a `*.client.{ts,tsx,js,jsx}` file —\n * this is enforced (#1191 review [9]): a path missing the `.client.`\n * infix, or a bare `.client.ts` with an empty stem, throws an error\n * (`addClientEntry` JS-host validation + Rust `InvalidClientEntry`)\n * rather than being silently accepted under an invented name. The entry\n * name is derived from the filename stem minus `.client`\n * (e.g. `my-lib.client.ts` → `my-lib`), via the same canonical helper\n * as user-authored `*.client.*` discovery.\n *\n * The entry is bundled and shipped as\n * `/assets/client/<name>.js` (stable URL) / `/assets/client/<name>-<hash>.js`\n * (production, hashed). User-authored files win on name collision —\n * the registered entry is silently dropped when a user-authored file of\n * the same name exists in the discovery roots.\n *\n * Two plugins registering the same entry name with different entrypoints\n * raises `ClientEntryConflict` and aborts the build.\n *\n * `entrypoint` is resolved relative to the project root if given as a\n * relative path (same rule as `injectRoute`).\n */\n addClientEntry(entrypoint: string): void;\n};\n\n/**\n * The plugin-module shape. `name` is informational (the resolved module\n * specifier wins for identification on the Rust side) and helps the\n * plugin self-identify in logs.\n *\n * Five optional hooks; declaration-order matters when multiple plugins\n * touch the same surface. Each hook is independent — a plugin may\n * declare any subset:\n *\n * - `setup` (#255) — register virtual modules, aliases, injected\n * routes. Runs once at host boot, before `preBuild`. Also runs under\n * `zfb preview` (#1542) via the minimal non-V8 `run_preview_setup`\n * path — see [`ZfbSetupContext.command`](#command) for what is and\n * isn't meaningful there.\n * - `preBuild` — file-generation work that downstream stages will\n * see. Runs once per `zfb build` and once per `zfb dev` boot. Does\n * **NOT** fire under `zfb preview` (#1542) — preview serves an\n * already-built `dist/` and never re-triggers file generation.\n * - `postBuild` — finalisation work that runs after `dist/` has been\n * written. Does not fire under `zfb preview` either, for the same\n * reason as `preBuild`.\n * - `devMiddleware` — register HTTP handlers for ad-hoc dev-only\n * URLs. Per-request dispatch, distinct from `injectRoute` (which\n * goes through the page renderer). Fires only during `zfb dev`.\n * - `previewMiddleware` (#1542) — register HTTP handlers for ad-hoc\n * preview-only URLs. Same register-context shape as `devMiddleware`,\n * fires only during `zfb preview`. A plugin wanting coverage in both\n * modes registers the same handler under both hooks — `zfb` does\n * NOT reuse a `devMiddleware` registration for preview automatically\n * (explicit per-mode opt-in, by design).\n */\nexport type ZfbPlugin = {\n /** Plugin display name; surfaces in error / log lines. */\n name: string;\n setup?(ctx: ZfbSetupContext): Promise<void> | void;\n preBuild?(ctx: ZfbBuildHookContext): Promise<void> | void;\n postBuild?(ctx: ZfbBuildHookContext): Promise<void> | void;\n devMiddleware?(ctx: ZfbDevMiddlewareContext): Promise<void> | void;\n previewMiddleware?(ctx: ZfbPreviewMiddlewareContext): Promise<void> | void;\n};\n\n/**\n * Identity helper that types the supplied object as a [`ZfbPlugin`].\n * Use as the default export of a plugin module so editors surface\n * field-level types and typos surface at compile time.\n *\n * ```ts\n * import { definePlugin } from \"@takazudo/zfb/plugins\";\n *\n * export default definePlugin({\n * name: \"my-plugin\",\n * async preBuild({ outDir, logger }) {\n * logger.info(`generating index into ${outDir}`);\n * },\n * });\n * ```\n */\nexport function definePlugin(plugin: ZfbPlugin): ZfbPlugin {\n return plugin;\n}\n"]}
1
+ {"version":3,"file":"plugins.js","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,0EAA0E;AAC1E,wEAAwE;AACxE,wEAAwE;AACxE,oEAAoE;AACpE,oEAAoE;AACpE,EAAE;AACF,uEAAuE;AACvE,sEAAsE;AACtE,qEAAqE;AACrE,sEAAsE;AACtE,sEAAsE;AACtE,6CAA6C;AAC7C,EAAE;AACF,wCAAwC;AACxC,EAAE;AACF,qEAAqE;AACrE,mEAAmE;AACnE,oEAAoE;AACpE,oEAAoE;AACpE,+BAA+B;AAmb/B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,YAAY,CAAC,MAAiB;IAC5C,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["// `zfb/plugins` — TypeScript helper for the zfb plugin lifecycle.\n//\n// A plugin is a JS module whose default export is a [`ZfbPlugin`] object.\n// `zfb.config.ts` references plugins by `name` (npm bare specifier or a\n// `./`-relative path); the zfb config loader resolves each `name` to an\n// absolute module specifier and the Rust-side plugin host loads the\n// module via dynamic `import()` and dispatches the lifecycle hooks.\n//\n// Sub 3 / issue #108 — initial drop. Three optional hooks: `preBuild`,\n// `postBuild`, `devMiddleware`. Astro-migration epic #253 / sub-issue\n// #255 adds a fourth: `setup`, which runs once before `preBuild` and\n// lets plugins register virtual modules, import aliases, and dev-only\n// injected routes. None of the hooks see real Node IPC objects across\n// the boundary; everything is JSON-friendly.\n//\n// ## Inline functions are NOT supported\n//\n// `PluginConfig` (in `./config.ts`) carries only data. A user cannot\n// inline a function in `zfb.config.ts` — the config goes through a\n// JSON round-trip and any function value would be silently dropped.\n// Plugins must live in their own module (npm package or local file)\n// and be referenced by `name`.\n\n/**\n * Logger handed to every plugin hook. `info`/`warn`/`error` each render on\n * the `zfb dev`/`zfb build`/`zfb preview` terminal at exactly that level,\n * attributed to the plugin: `zfb <level>: [plugin:<name>] <message>`.\n * `console.*` is redirected the same way, but note it maps onto only two\n * underlying streams (stdout -> info, stderr -> warn/error/trace/assert ->\n * error) — `console.warn` therefore renders as `zfb error:`, not\n * `zfb warn:`. Prefer this logger over `console.*` when the level matters.\n */\nexport type ZfbPluginLogger = {\n info(msg: string): void;\n warn(msg: string): void;\n error(msg: string): void;\n};\n\n/**\n * One emitted route in the `postBuild` route manifest (#262).\n * Present on `ctx.routes.routes` so a `postBuild` plugin can iterate\n * every URL the build produced (e.g. to write a `sitemap.xml`).\n */\nexport type ZfbRouteEntry = {\n /** Emitted URL path, e.g. `/`, `/blog/hello/`, `/sitemap.xml`. */\n url: string;\n /** Path under `outDir`, e.g. `index.html`, `blog/hello/index.html`, `sitemap.xml`. */\n output: string;\n /** File extension: `html`, `xml`, `rss`, `txt`, `json`, … */\n extension: string;\n /** Source page module relative to the project root, e.g. `pages/blog/[slug].tsx`. */\n source: string;\n /**\n * `true` when the page is prerendered to disk (default / SSG); `false`\n * when the page exports `prerender = false` and is served by the\n * runtime adapter (SSR — no on-disk artifact under `outDir`).\n *\n * Indexes that enumerate on-disk URLs (sitemap.xml, search-index.json,\n * etc.) should filter `r.prerender !== false` to avoid surfacing SSR\n * routes that have no static output.\n */\n prerender: boolean;\n /**\n * Bound route parameters. Absent for static routes.\n * Dynamic (`[slug]`) params are string scalars; catchall (`[...rest]`)\n * params are string arrays.\n */\n params?: Record<string, string | string[]>;\n};\n\n/**\n * The route manifest exposed on `ctx.routes` during a `postBuild` callback\n * (#262). Sorted by `url` for byte-stable output across runs.\n */\nexport type ZfbRouteManifest = {\n routes: ZfbRouteEntry[];\n};\n\n/**\n * Context passed to `preBuild` and `postBuild`. `outDir` is the\n * resolved absolute path of the configured `outDir` (default\n * `<projectRoot>/dist`). `projectRoot` is the directory containing\n * `zfb.config.ts`.\n *\n * `routes` is **only present on `postBuild`** calls; it is `undefined`\n * on `preBuild`. This is intentional: the route manifest is not\n * available until the build finishes writing `dist/` (#262).\n */\nexport type ZfbBuildHookContext = {\n /** Project root — the directory containing `zfb.config.ts`. */\n projectRoot: string;\n /** Resolved absolute path of the build output directory. */\n outDir: string;\n /** The full loaded `ZfbConfig` (data-only view). */\n config: import(\"./config.js\").ZfbConfig;\n /** Plugin-specific options block, copied verbatim from the matching `PluginConfig.options`. */\n options: Record<string, unknown>;\n /** Logger that wraps the Rust-side `tracing` subscriber. */\n logger: ZfbPluginLogger;\n /**\n * All routes emitted by this build, sorted by URL (#262).\n * Present only on `postBuild` calls; `undefined` on `preBuild`.\n */\n routes?: ZfbRouteManifest;\n};\n\n/**\n * A request handed to a `devMiddleware` handler. Subset of the Node\n * `http.IncomingMessage` surface intentionally — the dev server is\n * Rust-side `axum`, not Node, so we expose only what survives a JSON\n * envelope hop.\n */\nexport type ZfbDevMiddlewareRequest = {\n method: string;\n url: string;\n /** Lower-cased header names → first value. */\n headers: Record<string, string>;\n /** Raw request body; absent for GET/HEAD. UTF-8 only — binary is out of scope for v1 dev plugins. */\n body?: string;\n};\n\n/**\n * Response returned by a `devMiddleware` handler. All fields optional\n * except `status`. `body` may be a string (UTF-8) or a base64-encoded\n * binary payload (set `bodyEncoding` to `\"base64\"` in that case).\n */\nexport type ZfbDevMiddlewareResponse = {\n status: number;\n headers?: Record<string, string>;\n body?: string;\n bodyEncoding?: \"utf8\" | \"base64\";\n};\n\n/**\n * Handler signature for a `devMiddleware` registration. The `next` callback\n * is reserved for future composition; v1 plugins should produce a response\n * directly. Returning `undefined` from the handler signals \"I did not handle\n * this request\" — the dev server then falls through to its built-in routes\n * (the page cache, /__zfb/livereload.js, etc.).\n */\nexport type ZfbDevMiddlewareHandler = (\n req: ZfbDevMiddlewareRequest,\n) => Promise<ZfbDevMiddlewareResponse | undefined> | ZfbDevMiddlewareResponse | undefined;\n\n/**\n * Context passed to `devMiddleware`. The `register` callback installs\n * one handler per URL path prefix. `path` is matched as an exact prefix\n * — a registration on `/doc-history` matches `/doc-history` and\n * `/doc-history/foo`, but NOT `/doc-historyx`.\n */\nexport type ZfbDevMiddlewareContext = {\n projectRoot: string;\n config: import(\"./config.js\").ZfbConfig;\n options: Record<string, unknown>;\n logger: ZfbPluginLogger;\n /** Register an HTTP handler at `path`. Calling twice on the same path overwrites. */\n register(path: string, handler: ZfbDevMiddlewareHandler): void;\n};\n\n/**\n * Handler signature for a `previewMiddleware` registration (#1542).\n * Deliberately reuses [`ZfbDevMiddlewareRequest`] /\n * [`ZfbDevMiddlewareResponse`] verbatim — the wire shape crossing the\n * Rust↔JS boundary is genuinely the SAME for dev and preview (mirrors\n * the Rust side, which shares `DevRequest`/`DevResponse` between both\n * hooks too), so there is nothing preview-specific to say about the\n * request/response contract itself. `next` is likewise reserved for\n * future composition; returning `undefined` signals \"I did not handle\n * this request\" and the preview server falls through to its built-in\n * routes (static-file serving, or the wrangler-backed adapter in\n * adapter mode).\n */\nexport type ZfbPreviewMiddlewareHandler = (\n req: ZfbDevMiddlewareRequest,\n) => Promise<ZfbDevMiddlewareResponse | undefined> | ZfbDevMiddlewareResponse | undefined;\n\n/**\n * Context passed to `previewMiddleware` (#1542). Structurally identical\n * to [`ZfbDevMiddlewareContext`] today — one handler per URL path\n * prefix, matched the same way — but declared as its own named type\n * (unlike the request/response types above, which are reused verbatim)\n * because the *context* is where a hook-specific capability would land\n * first if one were ever added (e.g. something preview-only that\n * `devMiddleware` has no equivalent for). Keeping it a separate\n * declaration costs nothing today and avoids a breaking rename later.\n */\nexport type ZfbPreviewMiddlewareContext = {\n projectRoot: string;\n config: import(\"./config.js\").ZfbConfig;\n options: Record<string, unknown>;\n logger: ZfbPluginLogger;\n /** Register an HTTP handler at `path`. Calling twice on the same path overwrites. */\n register(path: string, handler: ZfbPreviewMiddlewareHandler): void;\n};\n\n/**\n * Loader signature for a virtual-module registration. Must return the\n * **complete ESM module source text** as a string — the bundler /\n * embedded V8 host feeds the returned string in as the module's\n * source verbatim. The loader runs **eagerly**, not lazily on first\n * import: exactly once per `zfb build` run and once per `zfb dev`\n * host boot, during the setup phase right after every plugin's\n * `setup` hook has returned — even if the registered specifier is\n * never imported by any page or module. The resulting source is\n * memoised; every subsequent import of that specifier reuses it,\n * **unless a forced reload is requested** (#2167) — the plugin-host\n * protocol now supports bypassing the memo and re-invoking the loader,\n * intended for a loader whose registration also declares\n * [`watchFiles`](#watchFiles) and needs a fresh read after one of\n * those files changes on disk. `zfb dev` watches every declared\n * [`watchFiles`](#watchFiles) path and re-invokes the owning loader with\n * its memo bypassed when one of them changes (#2169, #2181); `zfb build`\n * invokes each loader exactly once and never re-invokes it. See the\n * Plugins concept page for the full refresh contract.\n * (Under `zfb preview`, `addVirtualModule` registrations are accepted\n * but inert — see [`ZfbSetupContext.command`](#command) — so the\n * loader never runs there.)\n *\n * Example:\n *\n * ```ts\n * addVirtualModule(\"virtual:my-data\", () =>\n * `export default ${JSON.stringify(myJson)}`,\n * );\n * ```\n */\nexport type ZfbVirtualModuleLoader = () => string | Promise<string>;\n\n/**\n * Optional third argument to `addVirtualModule` (#2167).\n */\nexport type ZfbVirtualModuleOptions = {\n /**\n * Extra absolute filesystem paths a `zfb dev` watcher should track on\n * this loader's behalf — useful when the loader's output depends on\n * files it reads directly (e.g. via `node:fs`) rather than static ESM\n * imports the dev bundler would otherwise notice on its own.\n *\n * Every entry **must be an absolute path**: this mirrors\n * `extraWatchPaths`'s absolute-only rule in `zfb.config.ts`, and for\n * the same reason — `watchFiles` entries are never resolved against\n * the project root, so a relative entry has no defined base directory\n * to resolve against. A relative (or otherwise malformed) entry throws\n * at `setup` time.\n */\n watchFiles?: string[];\n};\n\n/**\n * Context passed to the new `setup` hook (#255). Runs once per host\n * boot, in `Config.plugins` declaration order, **before** `preBuild`.\n *\n * `ctx.command` tells the plugin which lifecycle is active so it can\n * gate per-lifecycle registrations. A dev-only mock route stays gated\n * to `\"dev\"`; a package-owned page route is registered unconditionally\n * (it is prerendered during a build and dev-routed during dev):\n *\n * ```ts\n * setup({ command, injectRoute }) {\n * // package-owned page route (rendered in build and dev)\n * injectRoute(\"/preset-page\", \"./pages/preset-page.tsx\");\n * // dev-only mock endpoint\n * if (command === \"dev\") {\n * injectRoute(\"/api/dev/x\", \"./scripts/dev-x.ts\");\n * }\n * }\n * ```\n *\n * The hook's surface is intentionally **closed**: only `injectRoute`,\n * `addVirtualModule`, `addAlias`, and `addClientEntry`. There is no\n * `addRemarkPlugin` / `addRehypePlugin` / `addMarkdownVisitor` — by\n * design (see the concept doc for the rationale). `addVirtualModule`'s\n * optional `watchFiles` argument (#2167) is a registration OPTION on\n * that existing method, not a new closed-surface method — the closed\n * set of four stays exactly four.\n */\nexport type ZfbSetupContext = {\n /**\n * Active zfb command. `\"build\"` during `zfb build`; `\"dev\"` during\n * `zfb dev`; `\"preview\"` during `zfb preview` (#1542). It can guide\n * lifecycle-specific plugin behavior. `injectRoute` registrations are\n * accepted in both `\"dev\"` and `\"build\"`; user `pages/` routes retain\n * precedence over matching injected routes (see\n * [`injectRoute`](#injectRoute)).\n *\n * Under `\"preview\"`, `setup` still fires (Rust-side via the minimal\n * non-V8 `run_preview_setup` path) so plugin-side state\n * initialisation runs, but `zfb preview` serves an ALREADY-BUILT\n * `dist/` verbatim and never re-enters the scan → bundle → render\n * pipeline. Consequently `injectRoute` / `addVirtualModule` /\n * `addAlias` / `addClientEntry` calls made under `\"preview\"` are\n * accepted (for shape-consistency with `\"build\"`/`\"dev\"`) but are\n * **inert** — nothing downstream ever reads them. Only the hook's\n * side effects and a subsequent `previewMiddleware` registration do\n * anything meaningful under `\"preview\"`.\n */\n command: \"build\" | \"dev\" | \"preview\";\n /** Project root — the directory containing `zfb.config.ts`. */\n projectRoot: string;\n /** The full loaded `ZfbConfig` (data-only view). */\n config: import(\"./config.js\").ZfbConfig;\n /** Plugin-specific options block, copied verbatim from `PluginConfig.options`. */\n options: Record<string, unknown>;\n /** Logger that wraps the Rust-side `tracing` subscriber. */\n logger: ZfbPluginLogger;\n\n /**\n * Register an import alias. **Exact-match-only in v1**:\n * `addAlias(\"@/foo\", \"./src/foo.tsx\")` rewrites `import \"@/foo\"`\n * but does NOT match `import \"@/foo/bar\"`. Prefix-matching is\n * explicitly deferred to v2 — switch to one bare alias per file\n * until then.\n *\n * `to` is resolved relative to the project root. Two plugins\n * registering the same `from` with different `to` raises\n * `AliasConflict` and aborts the build.\n */\n addAlias(from: string, to: string): void;\n\n /**\n * Register a virtual module. `specifier` is a bare import\n * specifier (recommended `virtual:` prefix, not enforced).\n * `loader` returns the complete ESM source text as a string and\n * runs **eagerly, once per build/dev-boot during setup** — not\n * lazily at first import (see [`ZfbVirtualModuleLoader`], including\n * its forced-reload amendment).\n *\n * The optional third argument's `watchFiles` (#2167) declares extra\n * absolute filesystem paths a `zfb dev` watcher should track on this\n * loader's behalf — see [`ZfbVirtualModuleOptions`]. Every entry must\n * be an absolute path; a relative entry throws.\n *\n * Two plugins registering the same `specifier` raises\n * `VirtualModuleConflict` and aborts the build.\n */\n addVirtualModule(\n specifier: string,\n loader: ZfbVirtualModuleLoader,\n options?: ZfbVirtualModuleOptions,\n ): void;\n\n /**\n * Register a synthetic / package-owned page route. `pattern` uses the\n * same grammar as `pages/` filenames (`/blog/[slug]`, `/api/dev/x`,\n * `/docs/[...rest]`).\n * Patterns below `/__paths__/` are reserved for zfb's internal `paths()`\n * endpoint and raise `ReservedRoutePrefix` with this plugin's name.\n *\n * - In **build** (package-owned routes), the route is materialised\n * into a per-build overlay pages root and **prerendered** through\n * the normal scan → bundle → render pipeline, so a preset can own a\n * route without the project shipping a `pages/` stub file. A `\"/\"`\n * package route becomes the project's root page when no user\n * `pages/index` exists, enabling a truly empty/absent user `pages/`.\n * A package route whose URL shape collides with a user `pages/` route\n * is dropped (user `pages/` wins). This is the supported, complete path.\n * - In **dev**, both static and dynamic injected routes are rendered\n * by `zfb dev`. Static routes (where the URL equals the pattern,\n * e.g. `/preset-about`) are seeded into the dev route universe at\n * boot; dynamic routes (e.g. `/preset-docs/[slug]`) are rendered\n * on first request via a request-time synthetic entry — params are\n * extracted from the URL by the Hono router inside the live bundle.\n * User `pages/` files take precedence over any injected route of\n * the same shape, including `pages/index` over an injected `\"/\"`.\n * Without a user index, an injected root is staged, seeded, and served\n * like any other static injected route. **HMR:** content the\n * route reads from watched collections live-refreshes normally.\n * Editing the package's **compiled entrypoint under `node_modules`**\n * is NOT watched and requires a `zfb dev` restart (restart-only\n * contract — a published package is not project source). **Per-route\n * data:** an injected route loads per-route data via a **dynamic\n * route's `paths()` export** (which returns `{ params, props }`);\n * `getStaticProps` on a package page is not forwarded by the overlay\n * (only `default` + the `prerender` hint are forwarded — same as\n * `zfb build`). A route that needs per-route data should be a\n * dynamic route whose `paths()` reads the data.\n *\n * `opts.prerender` controls the route's prerender shape during a\n * build: omit it (or `true`) for the SSG default; `false` marks an\n * SSR-shaped route, which `output: 'static'` rejects. It is build-only\n * metadata and ignored in dev.\n *\n * Two plugins registering the same `pattern` (or one plugin\n * re-registering it with a different entrypoint) raises\n * `InjectRouteConflict`.\n */\n injectRoute(pattern: string, entrypoint: string, opts?: { prerender?: boolean }): void;\n\n /**\n * Register a package-owned client-side side-effect entry (#1196).\n *\n * `entrypoint` **must** point to a `*.client.{ts,tsx,js,jsx}` file —\n * this is enforced (#1191 review [9]): a path missing the `.client.`\n * infix, or a bare `.client.ts` with an empty stem, throws an error\n * (`addClientEntry` JS-host validation + Rust `InvalidClientEntry`)\n * rather than being silently accepted under an invented name. The entry\n * name is derived from the filename stem minus `.client`\n * (e.g. `my-lib.client.ts` → `my-lib`), via the same canonical helper\n * as user-authored `*.client.*` discovery.\n *\n * The entry is bundled and shipped as\n * `/assets/client/<name>.js` (stable URL) / `/assets/client/<name>-<hash>.js`\n * (production, hashed). User-authored files win on name collision —\n * the registered entry is silently dropped when a user-authored file of\n * the same name exists in the discovery roots.\n *\n * Two plugins registering the same entry name with different entrypoints\n * raises `ClientEntryConflict` and aborts the build.\n *\n * `entrypoint` is resolved relative to the project root if given as a\n * relative path (same rule as `injectRoute`).\n */\n addClientEntry(entrypoint: string): void;\n};\n\n/**\n * The plugin-module shape. `name` is informational (the resolved module\n * specifier wins for identification on the Rust side) and helps the\n * plugin self-identify in logs.\n *\n * Five optional hooks; declaration-order matters when multiple plugins\n * touch the same surface. Each hook is independent — a plugin may\n * declare any subset:\n *\n * - `setup` (#255) — register virtual modules, aliases, injected\n * routes. Runs once at host boot, before `preBuild`. Also runs under\n * `zfb preview` (#1542) via the minimal non-V8 `run_preview_setup`\n * path — see [`ZfbSetupContext.command`](#command) for what is and\n * isn't meaningful there.\n * - `preBuild` — file-generation work that downstream stages will\n * see. Runs once per `zfb build` and once per `zfb dev` boot. Does\n * **NOT** fire under `zfb preview` (#1542) — preview serves an\n * already-built `dist/` and never re-triggers file generation.\n * - `postBuild` — finalisation work that runs after `dist/` has been\n * written. Does not fire under `zfb preview` either, for the same\n * reason as `preBuild`.\n * - `devMiddleware` — register HTTP handlers for ad-hoc dev-only\n * URLs. Per-request dispatch, distinct from `injectRoute` (which\n * goes through the page renderer). Fires only during `zfb dev`.\n * - `previewMiddleware` (#1542) — register HTTP handlers for ad-hoc\n * preview-only URLs. Same register-context shape as `devMiddleware`,\n * fires only during `zfb preview`. A plugin wanting coverage in both\n * modes registers the same handler under both hooks — `zfb` does\n * NOT reuse a `devMiddleware` registration for preview automatically\n * (explicit per-mode opt-in, by design).\n */\nexport type ZfbPlugin = {\n /** Plugin display name; surfaces in error / log lines. */\n name: string;\n setup?(ctx: ZfbSetupContext): Promise<void> | void;\n preBuild?(ctx: ZfbBuildHookContext): Promise<void> | void;\n postBuild?(ctx: ZfbBuildHookContext): Promise<void> | void;\n devMiddleware?(ctx: ZfbDevMiddlewareContext): Promise<void> | void;\n previewMiddleware?(ctx: ZfbPreviewMiddlewareContext): Promise<void> | void;\n};\n\n/**\n * Identity helper that types the supplied object as a [`ZfbPlugin`].\n * Use as the default export of a plugin module so editors surface\n * field-level types and typos surface at compile time.\n *\n * ```ts\n * import { definePlugin } from \"@takazudo/zfb/plugins\";\n *\n * export default definePlugin({\n * name: \"my-plugin\",\n * async preBuild({ outDir, logger }) {\n * logger.info(`generating index into ${outDir}`);\n * },\n * });\n * ```\n */\nexport function definePlugin(plugin: ZfbPlugin): ZfbPlugin {\n return plugin;\n}\n"]}
package/dist/runtime.d.ts CHANGED
@@ -44,6 +44,37 @@ interface IslandModule {
44
44
  */
45
45
  export type IslandManifestValue = string | IslandModule;
46
46
  export type IslandManifest = Readonly<Record<string, IslandManifestValue>>;
47
+ /**
48
+ * Public DOM signal written after an island's mount function returns.
49
+ *
50
+ * State table (the marker is observational only and is never a mount guard):
51
+ *
52
+ * - initial: absent; `mountIslands` / `mountNewIslands` strip a marker that is
53
+ * stale relative to this module instance's `mounted` map before scheduling.
54
+ * - deferred idle / visible / media: absent while the scheduler is waiting.
55
+ * - importing: absent while the URL module is in `pending`.
56
+ * - mounted via URL: `scheduleMount`'s URL success handler writes it only after
57
+ * `fn(propsForMount, element, mode)` returns, alongside the `mounted` entry.
58
+ * - mounted via inline module: `fireInlineMount` writes it only after
59
+ * `fn(props, element, mode)` returns, alongside the `mounted` entry.
60
+ * - missing manifest entry: absent; `scheduleMount` returns without writing.
61
+ * - no `mount` export: absent; both manifest paths return without writing.
62
+ * - synchronous mount throw: absent; the `mounted` entry is not written, so a
63
+ * later walk can retry the element.
64
+ * - rejected import: absent; the URL rejection handler clears `pending` and
65
+ * any defensive `mounted` entry.
66
+ * - detached during import: absent; the URL success handler clears `pending`
67
+ * and returns before calling mount.
68
+ * - unmounted (discarded): `unmountIslands` clears the marker and `mounted`
69
+ * entry in `finally`, even when the unmount thunk throws.
70
+ * - unmounted (persisted-lifted): retained together with the `mounted` entry;
71
+ * `unmountIslands` skips elements whose persist id exists in the incoming body.
72
+ * - props-changed remount: `clearMountedForRemount` clears the marker and map
73
+ * entry in `finally`, then the forced mount writes it again after mount returns.
74
+ * - dev hot-swap over a marked DOM: a fresh module's `mountIslands` strips the
75
+ * stale marker before scheduling, then writes it after its own mount returns.
76
+ */
77
+ export declare const ISLAND_MOUNTED_ATTR = "data-zfb-island-mounted";
47
78
  /**
48
79
  * Walk the DOM and mount every `[data-zfb-island]` / `[data-zfb-island-skip-ssr]`
49
80
  * element using `manifest`.
package/dist/runtime.js CHANGED
@@ -194,6 +194,37 @@ const PERSIST_ATTR = "data-zfb-transition-persist";
194
194
  // cross-package contract as PERSIST_ATTR above — both packages must agree on the
195
195
  // string). Consumed by clearMountedForRemount(). See #1389.
196
196
  const ISLAND_REMOUNT_ATTR = "data-zfb-island-remount";
197
+ /**
198
+ * Public DOM signal written after an island's mount function returns.
199
+ *
200
+ * State table (the marker is observational only and is never a mount guard):
201
+ *
202
+ * - initial: absent; `mountIslands` / `mountNewIslands` strip a marker that is
203
+ * stale relative to this module instance's `mounted` map before scheduling.
204
+ * - deferred idle / visible / media: absent while the scheduler is waiting.
205
+ * - importing: absent while the URL module is in `pending`.
206
+ * - mounted via URL: `scheduleMount`'s URL success handler writes it only after
207
+ * `fn(propsForMount, element, mode)` returns, alongside the `mounted` entry.
208
+ * - mounted via inline module: `fireInlineMount` writes it only after
209
+ * `fn(props, element, mode)` returns, alongside the `mounted` entry.
210
+ * - missing manifest entry: absent; `scheduleMount` returns without writing.
211
+ * - no `mount` export: absent; both manifest paths return without writing.
212
+ * - synchronous mount throw: absent; the `mounted` entry is not written, so a
213
+ * later walk can retry the element.
214
+ * - rejected import: absent; the URL rejection handler clears `pending` and
215
+ * any defensive `mounted` entry.
216
+ * - detached during import: absent; the URL success handler clears `pending`
217
+ * and returns before calling mount.
218
+ * - unmounted (discarded): `unmountIslands` clears the marker and `mounted`
219
+ * entry in `finally`, even when the unmount thunk throws.
220
+ * - unmounted (persisted-lifted): retained together with the `mounted` entry;
221
+ * `unmountIslands` skips elements whose persist id exists in the incoming body.
222
+ * - props-changed remount: `clearMountedForRemount` clears the marker and map
223
+ * entry in `finally`, then the forced mount writes it again after mount returns.
224
+ * - dev hot-swap over a marked DOM: a fresh module's `mountIslands` strips the
225
+ * stale marker before scheduling, then writes it after its own mount returns.
226
+ */
227
+ export const ISLAND_MOUNTED_ATTR = "data-zfb-island-mounted";
197
228
  // WeakMap<Element, unmount thunk> — replaces the old WeakSet.
198
229
  // Value is a per-element function that calls the bundle's unmount(element)
199
230
  // (or a noop if the bundle does not expose one). Used by unmountIslands()
@@ -241,6 +272,7 @@ export function mountIslands(manifest) {
241
272
  capturedManifest = manifest;
242
273
  const ssrIslands = document.querySelectorAll("[data-zfb-island]");
243
274
  for (const el of Array.from(ssrIslands)) {
275
+ stripStaleMountedMarker(el);
244
276
  // Skip the empty-skeleton case left behind when the server-side
245
277
  // rewriter has not run yet (data-zfb-island="" with no component
246
278
  // name). The hydration emit step is expected to fill this in
@@ -254,6 +286,7 @@ export function mountIslands(manifest) {
254
286
  }
255
287
  const skipSsrIslands = document.querySelectorAll("[data-zfb-island-skip-ssr]");
256
288
  for (const el of Array.from(skipSsrIslands)) {
289
+ stripStaleMountedMarker(el);
257
290
  const name = el.getAttribute("data-zfb-island-skip-ssr");
258
291
  if (!name)
259
292
  continue;
@@ -280,6 +313,7 @@ export function mountNewIslands() {
280
313
  const manifest = capturedManifest;
281
314
  const ssrIslands = document.querySelectorAll("[data-zfb-island]");
282
315
  for (const el of Array.from(ssrIslands)) {
316
+ stripStaleMountedMarker(el);
283
317
  const name = el.getAttribute("data-zfb-island");
284
318
  if (!name)
285
319
  continue;
@@ -293,6 +327,7 @@ export function mountNewIslands() {
293
327
  }
294
328
  const skipSsrIslands = document.querySelectorAll("[data-zfb-island-skip-ssr]");
295
329
  for (const el of Array.from(skipSsrIslands)) {
330
+ stripStaleMountedMarker(el);
296
331
  const name = el.getAttribute("data-zfb-island-skip-ssr");
297
332
  if (!name)
298
333
  continue;
@@ -336,14 +371,24 @@ function clearMountedForRemount(el) {
336
371
  return false;
337
372
  const thunk = mounted.get(el);
338
373
  if (thunk) {
339
- thunk();
340
- mounted.delete(el);
341
- el.removeAttribute(ISLAND_REMOUNT_ATTR);
374
+ try {
375
+ thunk();
376
+ }
377
+ finally {
378
+ mounted.delete(el);
379
+ el.removeAttribute(ISLAND_MOUNTED_ATTR);
380
+ el.removeAttribute(ISLAND_REMOUNT_ATTR);
381
+ }
342
382
  return true;
343
383
  }
384
+ el.removeAttribute(ISLAND_MOUNTED_ATTR);
344
385
  el.removeAttribute(ISLAND_REMOUNT_ATTR);
345
386
  return false;
346
387
  }
388
+ function stripStaleMountedMarker(el) {
389
+ if (!mounted.has(el))
390
+ el.removeAttribute(ISLAND_MOUNTED_ATTR);
391
+ }
347
392
  /**
348
393
  * Cancel deferred-hydration callbacks for all islands in the old body before a
349
394
  * swap. Prevents idle / visibility callbacks from running against orphan elements
@@ -488,9 +533,14 @@ function scheduleMount(manifest, element, componentName, mode, options = {}) {
488
533
  : () => {
489
534
  // noop — bundle does not expose unmount
490
535
  };
491
- mounted.set(element, unmountThunk);
492
- pending.delete(element);
493
- fn(propsForMount, element, mode);
536
+ try {
537
+ fn(propsForMount, element, mode);
538
+ mounted.set(element, unmountThunk);
539
+ element.setAttribute(ISLAND_MOUNTED_ATTR, "");
540
+ }
541
+ finally {
542
+ pending.delete(element);
543
+ }
494
544
  }, (err) => {
495
545
  // Surface the error in dev so the user notices, then clear
496
546
  // both guards so a later retry (e.g. another scheduleHydrate
@@ -561,8 +611,9 @@ function fireInlineMount(element, mod, mode, options = {}) {
561
611
  : () => {
562
612
  // noop — inline module does not expose unmount
563
613
  };
564
- mounted.set(element, unmountThunk);
565
614
  fn(props, element, mode);
615
+ mounted.set(element, unmountThunk);
616
+ element.setAttribute(ISLAND_MOUNTED_ATTR, "");
566
617
  };
567
618
  if (mode === "render") {
568
619
  fire();
@@ -626,9 +677,12 @@ export function unmountIslands(root = document.body, incomingBody) {
626
677
  if (persistId !== null && preservedPersistIds.has(persistId))
627
678
  continue;
628
679
  const thunk = mounted.get(el);
629
- if (thunk) {
630
- thunk();
680
+ try {
681
+ thunk?.();
682
+ }
683
+ finally {
631
684
  mounted.delete(el);
685
+ el.removeAttribute(ISLAND_MOUNTED_ATTR);
632
686
  }
633
687
  }
634
688
  }
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["// Hydration scheduling helper consumed by the hydration runtime (Sub 3).\n//\n// Sub 3 owns the hydration runtime that walks the DOM, finds elements\n// marked with `data-zfb-island`, and dispatches each one through this\n// helper to decide *when* to fire the actual hydrate() call. The helper\n// itself does not know how to hydrate — it only schedules the supplied\n// `fire` callback.\n//\n// The branching matches the `When` union exactly:\n// \"visible\" → IntersectionObserver, threshold 0.0, hydrate on first\n// intersection, then disconnect.\n// \"idle\" → requestIdleCallback if available, otherwise setTimeout(0).\n// \"media\" → matchMedia(target's data-media), hydrate when the query\n// first matches (now or on a later change event), then\n// remove the listener.\n// \"load\" → immediate, synchronous fire.\n//\n// Anything else is treated as \"load\" (with a console.warn in development).\n// The helper is environment-tolerant: callers can run it in jsdom /\n// happy-dom or bare Node, and the absence of `IntersectionObserver` /\n// `requestIdleCallback` / `matchMedia` is handled gracefully.\n\nimport { resolveWhen, type When } from \"./types.js\";\n\n/**\n * Subset of the global object that this module touches. Cast once at the\n * module top so individual scheduler functions don't repeat the inline\n * widening.\n */\ntype SchedulerGlobal = typeof globalThis & {\n requestIdleCallback?: (\n cb: (deadline: { didTimeout: boolean; timeRemaining: () => number }) => void,\n options?: { timeout?: number },\n ) => number;\n cancelIdleCallback?: (handle: number) => void;\n IntersectionObserver?: typeof IntersectionObserver;\n matchMedia?: typeof matchMedia;\n};\n\nconst g = globalThis as SchedulerGlobal;\n\n/**\n * Internal variant of `scheduleHydrate` that also reports whether the fire\n * callback was invoked synchronously. Unexported — call sites in this module\n * use this to decide whether to register a `pendingCancels` entry.\n */\nfunction scheduleHydrateInternal(\n target: Element,\n when: When | string | undefined,\n fire: () => void,\n): { fired: boolean; cancel: () => void } {\n const resolved = resolveWhen(when);\n\n if (resolved === \"load\") {\n fire();\n return { fired: true, cancel: noop };\n }\n\n if (resolved === \"idle\") {\n return { fired: false, cancel: scheduleIdle(fire) };\n }\n\n if (resolved === \"media\") {\n return scheduleMedia(target, fire);\n }\n\n // \"visible\"\n return scheduleVisible(target, fire);\n}\n\n/**\n * Schedule a hydration `fire` callback for `target` according to `when`.\n *\n * Returns a `cancel` function that aborts the scheduling if it has not\n * fired yet. After firing, calling `cancel` is a no-op. If the helper\n * cannot find the relevant browser API (e.g. running in pure Node with no\n * polyfill), it falls back to firing synchronously so server-side smoke\n * tests still observe the call.\n */\nexport function scheduleHydrate(\n target: Element,\n when: When | string | undefined,\n fire: () => void,\n): () => void {\n return scheduleHydrateInternal(target, when, fire).cancel;\n}\n\nfunction noop(): void {\n // intentionally empty\n}\n\n/**\n * Build a one-shot gate around `fn`. The returned `run` invokes `fn`\n * exactly once provided `cancel` has not been called first; `cancel`\n * marks the gate as cancelled (later `run` invocations become no-ops)\n * and reports whether the gate had already fired.\n */\nfunction oneShot(fn: () => void): {\n run: () => void;\n cancel: () => boolean;\n} {\n let fired = false;\n let cancelled = false;\n return {\n run(): void {\n if (cancelled || fired) return;\n fired = true;\n fn();\n },\n cancel(): boolean {\n if (fired) return true;\n cancelled = true;\n return false;\n },\n };\n}\n\nfunction scheduleIdle(fire: () => void): () => void {\n const gate = oneShot(fire);\n\n if (typeof g.requestIdleCallback === \"function\") {\n const handle = g.requestIdleCallback(gate.run);\n return () => {\n const alreadyFired = gate.cancel();\n if (alreadyFired) return;\n if (typeof g.cancelIdleCallback === \"function\") g.cancelIdleCallback(handle);\n };\n }\n\n const handle = setTimeout(gate.run, 0);\n return () => {\n const alreadyFired = gate.cancel();\n if (alreadyFired) return;\n clearTimeout(handle);\n };\n}\n\nfunction scheduleVisible(\n target: Element,\n fire: () => void,\n): { fired: boolean; cancel: () => void } {\n const Observer = g.IntersectionObserver;\n\n // No IntersectionObserver (e.g. very old browsers, bare Node) — fail\n // open and hydrate immediately so the island is at least functional.\n if (typeof Observer !== \"function\") {\n fire();\n return { fired: true, cancel: noop };\n }\n\n const gate = oneShot(fire);\n const observer = new Observer(\n (entries, obs) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n obs.disconnect();\n gate.run();\n return;\n }\n }\n },\n { threshold: 0 },\n );\n\n observer.observe(target);\n\n return {\n fired: false,\n cancel: () => {\n const alreadyFired = gate.cancel();\n if (alreadyFired) return;\n observer.disconnect();\n },\n };\n}\n\nfunction scheduleMedia(target: Element, fire: () => void): { fired: boolean; cancel: () => void } {\n const query = target.getAttribute(\"data-media\");\n\n // No matchMedia API (e.g. bare Node, very old browser) or missing/empty\n // query — fail open and hydrate immediately so the island is at least\n // functional.\n if (typeof g.matchMedia !== \"function\" || !query) {\n fire();\n return { fired: true, cancel: noop };\n }\n\n const mql = g.matchMedia(query);\n\n // Already matches — fire synchronously (no pending listener needed).\n if (mql.matches) {\n fire();\n return { fired: true, cancel: noop };\n }\n\n const gate = oneShot(fire);\n\n let removeListener = noop;\n\n // Listen for the first change event where the query matches.\n // We do NOT use `{once:true}` because we must ignore un-match events\n // (e.g. viewport widens back above breakpoint) and only fire on the\n // first match event — `{once:true}` would consume any change, including\n // un-match changes.\n const handler = (e: MediaQueryListEvent): void => {\n if (!e.matches) return; // ignore un-match changes\n removeListener();\n gate.run();\n };\n\n // Modern browsers expose the EventTarget API on MediaQueryList; older\n // Safari (<14) only has the deprecated addListener/removeListener pair\n // and throws on addEventListener. Prefer modern, fall back to legacy,\n // and fail open when neither exists (mirrors the missing-matchMedia case).\n if (typeof mql.addEventListener === \"function\") {\n mql.addEventListener(\"change\", handler);\n removeListener = () => mql.removeEventListener(\"change\", handler);\n } else if (typeof mql.addListener === \"function\") {\n mql.addListener(handler);\n removeListener = () => mql.removeListener(handler);\n } else {\n fire();\n return { fired: true, cancel: noop };\n }\n\n return {\n fired: false,\n cancel: () => {\n const alreadyFired = gate.cancel();\n if (alreadyFired) return;\n removeListener();\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// mountIslands — DOM walk + dynamic-import dispatcher.\n//\n// `mountIslands` is the entry point the generated `islands-runtime-<hash>.js`\n// bundle calls at script load time. It walks the DOM for the two island\n// markers emitted by the server-side hydration step and the `<Island>`\n// JSX wrapper:\n//\n// 1. `[data-zfb-island]` — SSR'd islands. We `hydrate()` (Preact) /\n// `hydrateRoot()` (React) against the existing server-rendered\n// DOM, gated by `scheduleHydrate(when)`.\n//\n// 2. `[data-zfb-island-skip-ssr]` — SSR-skip islands. The server\n// emitted no markup for these, so we `render()` (Preact) /\n// `createRoot().render()` (React). Skipping hydrate for this case\n// avoids the hydrate-mismatch warnings React/Preact would emit\n// against an empty DOM container.\n//\n// The per-island bundles each export a `mount(props, element, mode)`\n// function (see zfb_islands::render_island_entry_source). The\n// framework-specific glue lives inside that bundle, so this runtime is\n// framework-agnostic.\n//\n// ## Module-level singleton\n//\n// Dynamic imports of the same URL are cached by the JS runtime, so\n// \"switching pages\" (in an SPA shell) reuses the loaded bundle for free.\n// We keep an extra in-memory dedup map keyed by element so an island is\n// never mounted twice (e.g. on hot-reload / repeat-mount scenarios).\n// ---------------------------------------------------------------------------\n\n/**\n * The shape of the default export each per-island bundle ships.\n *\n * `mode === \"hydrate\"` is used for SSR'd islands, `\"render\"` for\n * SSR-skip islands.\n */\ntype IslandMount = (\n props: Record<string, unknown>,\n element: Element,\n mode: \"hydrate\" | \"render\",\n) => void;\n\ntype IslandUnmount = (element: Element) => void;\n\ninterface IslandModule {\n mount?: IslandMount;\n default?: IslandMount;\n unmount?: IslandUnmount;\n}\n\n/**\n * Map of `componentName → island descriptor` baked into the runtime entry.\n *\n * Two descriptor shapes are accepted so the same `mountIslands` runtime\n * handles both bundling strategies the build emits:\n *\n * 1. `string` — a per-island bundle URL. The runtime fetches it via\n * dynamic `import()` and reads `mount` / `default` off the loaded\n * module. Used by the per-island bundling path\n * (`bundle_per_island` / `render_runtime_entry_source`).\n *\n * 2. `IslandModule` — an inline module-shaped object whose `mount` (or\n * `default`) is called directly. Used by the shared-bundle path\n * (`render_shared_bundle_entry_source`): every island's source code\n * is already in the same bundle, so the synthesised entry can hand\n * the runtime the constructed mount functions inline without a\n * second HTTP fetch. This preserves the one-request shared-bundle\n * contract while giving up nothing on hydration semantics\n * (zudolab/zudo-doc#1355 wave 6).\n */\nexport type IslandManifestValue = string | IslandModule;\nexport type IslandManifest = Readonly<Record<string, IslandManifestValue>>;\n\n// data-zfb-transition-persist marker attribute — the client-router's persist\n// contract. Mirrored from client-router/swap-functions.ts: that package owns the\n// body swap (lifting persisted nodes into the incoming body), this package owns\n// island mount/unmount. Both must agree on the literal string. See the port\n// spec at packages/zfb-runtime/docs/client-router/port-spec.md §12.3.\nconst PERSIST_ATTR = \"data-zfb-transition-persist\";\n\n// Cross-package \"needs-remount\" flag set by client-router/swap-functions.ts on a\n// persisted island whose props changed across a body swap. Mirrored literal (same\n// cross-package contract as PERSIST_ATTR above — both packages must agree on the\n// string). Consumed by clearMountedForRemount(). See #1389.\nconst ISLAND_REMOUNT_ATTR = \"data-zfb-island-remount\";\n\n// WeakMap<Element, unmount thunk> — replaces the old WeakSet.\n// Value is a per-element function that calls the bundle's unmount(element)\n// (or a noop if the bundle does not expose one). Used by unmountIslands()\n// to fire framework lifecycle cleanups before a body swap.\nconst mounted = new WeakMap<Element, () => void>();\n// Elements for which the nested-island self-wrap warning has already been\n// emitted. Guards against repeated warn spam across re-walks (e.g. SPA swaps).\nconst warnedNested = new WeakSet<Element>();\n// Elements with an in-flight dynamic import that has not yet resolved.\n// Two concurrent `mountIslands` invocations (or two `scheduleMount`\n// calls hitting the same element through different code paths) could\n// otherwise both pass the `mounted` guard and both spawn an\n// `importIsland(url)` -> `fn()` chain, double-mounting the component.\n// Adding the element to `pending` synchronously, before the import is\n// fired, closes that window; the entry is removed in both the success\n// (after `mounted.set`) and failure branches.\nconst pending = new WeakSet<Element>();\n\n// Module-level captured manifest — set by the first `mountIslands` call and reused by\n// `mountNewIslands()` so the client-router does not need to know the manifest directly.\n// Named technical cause (W1B §12.1): the router lives in @takazudo/zfb-runtime; the\n// islands manifest lives in @takazudo/zfb. Passing the manifest through the swap event\n// would require widening the event API or threading manifest into router options. The\n// captured-manifest pattern keeps the package boundary clean.\nlet capturedManifest: IslandManifest | null = null;\n\n// Map of element → cancel-function for deferred-hydration islands\n// (data-when=\"idle\"|\"visible\"|\"media\").\n// Populated in scheduleMount; consulted on `zfb:before-swap` so deferred fires do not run\n// against orphan elements after a body swap. (W1B §12.5)\nconst pendingCancels = new Map<Element, () => void>();\n\n/**\n * Walk the DOM and mount every `[data-zfb-island]` / `[data-zfb-island-skip-ssr]`\n * element using `manifest`.\n *\n * No-op when `document` is undefined (SSR, edge runtime). Safe to call\n * multiple times: each element is mounted at most once thanks to the\n * `mounted` WeakSet guard.\n *\n * The manifest is captured at module level so `mountNewIslands()` can re-use\n * it after an SPA body swap without needing the caller to re-supply it.\n */\nexport function mountIslands(manifest: IslandManifest): void {\n if (typeof document === \"undefined\") return;\n\n // Capture the manifest for post-swap re-walks via mountNewIslands().\n capturedManifest = manifest;\n\n const ssrIslands = document.querySelectorAll<HTMLElement>(\"[data-zfb-island]\");\n for (const el of Array.from(ssrIslands)) {\n // Skip the empty-skeleton case left behind when the server-side\n // rewriter has not run yet (data-zfb-island=\"\" with no component\n // name). The hydration emit step is expected to fill this in\n // before the page reaches the browser; if it didn't, we cannot\n // dispatch.\n const name = el.getAttribute(\"data-zfb-island\");\n if (!name) continue;\n warnIfNestedIsland(el, name);\n scheduleMount(manifest, el, name, \"hydrate\");\n }\n\n const skipSsrIslands = document.querySelectorAll<HTMLElement>(\"[data-zfb-island-skip-ssr]\");\n for (const el of Array.from(skipSsrIslands)) {\n const name = el.getAttribute(\"data-zfb-island-skip-ssr\");\n if (!name) continue;\n warnIfNestedIsland(el, name);\n scheduleMount(manifest, el, name, \"render\");\n }\n}\n\n/**\n * Re-walk the current document body and mount any new island markers introduced\n * by an SPA body swap. Uses the manifest captured by the previous `mountIslands`\n * call — no manifest arg required.\n *\n * The caller (client-router `router.ts`) invokes this after `swap()` + `runScripts()`\n * and before dispatching `zfb:page-load`, per W1B §12.2 contract.\n *\n * No-op when called before `mountIslands` (capturedManifest is null) or when\n * `document` is undefined.\n */\nexport function mountNewIslands(): void {\n if (typeof document === \"undefined\") return;\n if (capturedManifest === null) return;\n\n const manifest = capturedManifest;\n\n const ssrIslands = document.querySelectorAll<HTMLElement>(\"[data-zfb-island]\");\n for (const el of Array.from(ssrIslands)) {\n const name = el.getAttribute(\"data-zfb-island\");\n if (!name) continue;\n // A persisted island whose props changed across the body swap is flagged\n // for remount by swap-functions.swapBodyElement. Clear its surviving mounted\n // entry BEFORE scheduleMount's already-mounted guard so it re-mounts fresh\n // with the refreshed data-props. No-op for every other element.\n const forceRemount = clearMountedForRemount(el);\n warnIfNestedIsland(el, name);\n scheduleMount(manifest, el, name, \"hydrate\", { force: forceRemount });\n }\n\n const skipSsrIslands = document.querySelectorAll<HTMLElement>(\"[data-zfb-island-skip-ssr]\");\n for (const el of Array.from(skipSsrIslands)) {\n const name = el.getAttribute(\"data-zfb-island-skip-ssr\");\n if (!name) continue;\n warnIfNestedIsland(el, name);\n scheduleMount(manifest, el, name, \"render\");\n }\n}\n\n/**\n * Consume the cross-package \"needs-remount\" signal for the persist-props hybrid\n * path (port-spec §12.3.1 hybrid case / §12.3.2). When a persisted island's\n * props differ from the incoming markup, `swapBodyElement` refreshes the\n * surviving element's `data-props` and marks it with `ISLAND_REMOUNT_ATTR`.\n * That attribute is the ONLY channel that crosses the zfb-runtime → zfb package\n * boundary — the `mounted` map is module-private to this file, so a shared\n * in-memory \"needs-remount\" queue between the two packages is impossible; the\n * live DOM node carrying the flag IS the queue.\n *\n * On a flagged mounted element: fire the old instance's unmount thunk (so its\n * useEffect/framework cleanups run against the still-connected node), drop the\n * `mounted` entry so `scheduleMount`'s guard no longer short-circuits, strip the\n * flag, and ask the caller to force the replacement mount through immediately\n * instead of re-entering any deferred scheduler. This keeps a deferred persisted\n * island from blanking while it waits for idle/visible/media to fire again.\n *\n * On a flagged element whose URL import is still pending, leave the flag in\n * place. The already-running import's success handler consumes it after the\n * module resolves and re-reads `data-props` at that point, so a props refresh\n * that happened during the import wins without starting a duplicate import.\n *\n * A no-op for elements without the flag (the common case: fresh markers and\n * props-unchanged persisted islands).\n *\n * Scope: only the `[data-zfb-island]` (hydrated) loop calls this, mirroring the\n * writer side — swapBodyElement sets the flag only for `newTarget.matches(\n * \"[data-zfb-island]\")`, never for skip-ssr islands.\n */\nfunction clearMountedForRemount(el: Element): boolean {\n if (!el.hasAttribute(ISLAND_REMOUNT_ATTR)) return false;\n if (pending.has(el)) return false;\n\n const thunk = mounted.get(el);\n if (thunk) {\n thunk();\n mounted.delete(el);\n el.removeAttribute(ISLAND_REMOUNT_ATTR);\n return true;\n }\n el.removeAttribute(ISLAND_REMOUNT_ATTR);\n return false;\n}\n\n/**\n * Cancel deferred-hydration callbacks for all islands in the old body before a\n * swap. Prevents idle / visibility callbacks from running against orphan elements\n * after `swapBodyElement` removes them from the live document. (W1B §12.5)\n *\n * Call this on `zfb:before-swap` (or equivalently, in the router's swap sequence\n * before `swap()` mutates the DOM). Fire-and-forget; safe to call if nothing is\n * pending.\n */\nexport function cancelPendingIslands(): void {\n for (const [el, cancel] of pendingCancels) {\n cancel();\n pendingCancels.delete(el);\n }\n}\n\n/**\n * Warn (once per element, dev-only) when an island marker element is found\n * nested inside another island marker. Self-wrapping an island — emitting a\n * `data-zfb-island` or `data-zfb-island-skip-ssr` container *inside* another\n * island component's render output — mis-hydrates because the runtime will\n * try to mount both the outer and inner islands independently. The outer\n * island's framework instance owns the inner DOM, so a second `hydrate()` /\n * `render()` call against the inner element races with the outer render and\n * produces undefined behaviour.\n *\n * The fix is to author the inner component bare (no `<Island>` in its own\n * render output) and apply the `<Island when=\"...\">` wrapper at the call site.\n */\nfunction warnIfNestedIsland(el: Element, componentName: string): void {\n if (typeof process === \"undefined\" || !process.env || process.env[\"NODE_ENV\"] === \"production\") {\n return;\n }\n if (warnedNested.has(el)) return;\n const parent = el.parentElement;\n if (!parent || typeof parent.closest !== \"function\") return;\n const ancestor = parent.closest(\"[data-zfb-island],[data-zfb-island-skip-ssr]\");\n if (!ancestor) return;\n warnedNested.add(el);\n // eslint-disable-next-line no-console\n console.warn(\n `[zfb] Island \"${componentName}\" is nested inside another island marker. ` +\n `Self-wrapping an island mis-hydrates: the outer framework instance owns ` +\n `the inner DOM, causing a conflicting mount. ` +\n `Fix: author \"${componentName}\" bare (remove <Island> from its own render output) ` +\n `and apply <Island when=\"...\"> at the call site instead.`,\n );\n}\n\nfunction scheduleMount(\n manifest: IslandManifest,\n element: Element,\n componentName: string,\n mode: \"hydrate\" | \"render\",\n options: { force?: boolean } = {},\n): void {\n // Skip elements already mounted OR currently importing — the latter\n // prevents two concurrent `mountIslands` calls from each firing a\n // separate dynamic import for the same element.\n if (mounted.has(element) || pending.has(element)) return;\n\n const entry = manifest[componentName];\n if (entry == null) {\n if (typeof process !== \"undefined\" && process.env && process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n `[zfb] no island manifest entry for component \"${componentName}\" — ` +\n `the runtime manifest is out of sync with the rendered HTML.`,\n );\n }\n return;\n }\n\n const when = element.getAttribute(\"data-when\") ?? undefined;\n\n // Two manifest shapes:\n //\n // - `string` (per-island bundle URL): fetch via dynamic `import()`\n // and call `mount` / `default` on the resolved module.\n // - `IslandModule` (inline descriptor): the shared-bundle path has\n // already imported every island's source into the same bundle and\n // constructed a mount function for it. Skip the dynamic import\n // and call the supplied function directly.\n if (typeof entry !== \"string\") {\n fireInlineMount(element, entry, mode, options);\n return;\n }\n\n const url: string = entry;\n\n const fire = (): void => {\n // Re-check both guards in case `fire` is invoked from a deferred\n // scheduler (rIC/rAF/visibility) after a sibling caller already\n // mounted or started importing for this element.\n if (mounted.has(element) || pending.has(element)) return;\n\n // When the deferred fire actually runs, the cancel handle is no longer\n // needed — remove it so pendingCancels doesn't hold stale entries.\n pendingCancels.delete(element);\n\n // Lazy props parse: read and parse data-props only now that we know we\n // are actually going to mount this island. For deferred strategies\n // (media, visible, idle) this avoids JSON.parse work at boot time for\n // islands that may never hydrate (e.g. media query never matches).\n const props = readProps(element);\n\n // Mark as pending BEFORE firing the import so any concurrent\n // `mountIslands` invocation that arrives during the await window\n // is short-circuited by `scheduleMount`'s guard.\n pending.add(element);\n\n // Dynamic-import is cached by the JS runtime, so repeat hits for\n // the same URL share the resolved module — module-level\n // singletons are fine.\n //\n // We move the element from `pending` to `mounted` only on the\n // success path so a failed import (e.g. transient network blip\n // in dev) doesn't permanently block a retry of the same element.\n let started: Promise<IslandModule>;\n try {\n started = importIsland(url);\n } catch (err) {\n // Some implementations of dynamic-import wrappers can throw\n // synchronously (e.g. URL parsing errors). Treat the same as\n // an async rejection.\n pending.delete(element);\n // eslint-disable-next-line no-console\n console.error(`[zfb] failed to start dynamic import for ${url}`, err);\n return;\n }\n started.then(\n (mod) => {\n const fn = mod.mount ?? mod.default;\n if (typeof fn !== \"function\") {\n pending.delete(element);\n if (\n typeof process !== \"undefined\" &&\n process.env &&\n process.env[\"NODE_ENV\"] !== \"production\"\n ) {\n // eslint-disable-next-line no-console\n console.warn(`[zfb] island bundle at ${url} did not export mount() or default()`);\n }\n return;\n }\n // Stale-mount race guard: if the element was detached while the\n // dynamic import was in-flight (e.g. a body swap happened), skip\n // mounting — the element is no longer in the live document and\n // its useEffect listeners would never receive a cleanup call.\n if (!element.isConnected) {\n pending.delete(element);\n return;\n }\n const shouldRefreshProps = element.hasAttribute(ISLAND_REMOUNT_ATTR);\n const propsForMount = shouldRefreshProps ? readProps(element) : props;\n if (shouldRefreshProps) element.removeAttribute(ISLAND_REMOUNT_ATTR);\n const unmountThunk = mod.unmount\n ? () => mod.unmount!(element)\n : () => {\n // noop — bundle does not expose unmount\n };\n mounted.set(element, unmountThunk);\n pending.delete(element);\n fn(propsForMount, element, mode);\n },\n (err: unknown) => {\n // Surface the error in dev so the user notices, then clear\n // both guards so a later retry (e.g. another scheduleHydrate\n // fire) can attempt the import again.\n pending.delete(element);\n mounted.delete(element);\n // eslint-disable-next-line no-console\n console.error(`[zfb] failed to load island bundle ${url}`, err);\n },\n );\n };\n\n if (mode === \"render\") {\n // SSR-skip islands ignore data-when: there is nothing to defer\n // hydration of, just an empty container we paint into. Mount\n // immediately so the user sees output.\n fire();\n return;\n }\n\n if (options.force) {\n fire();\n return;\n }\n\n const { fired, cancel } = scheduleHydrateInternal(element, when, fire);\n // Track deferred-hydration cancel handle so cancelPendingIslands() can abort\n // idle / visibility callbacks before a body swap. (W1B §12.5)\n // Only register when the scheduler did NOT fire synchronously — a synchronous\n // fire means the island is already handling its import and there is no\n // deferred callback to cancel. Registering noop after a sync fire would leave\n // a stale pendingCancels entry for an already-handled element. (#743)\n if (when && when !== \"load\" && !fired) {\n pendingCancels.set(element, cancel);\n }\n}\n\n/**\n * Run the mount step for the inline-module manifest shape used by the\n * shared-bundle path. The module is already in memory (it was imported\n * into the bundle at build time), so there is no async window to\n * coordinate around — we just call `mount` / `default` directly,\n * gated by the same `data-when` semantics as the URL path.\n */\nfunction fireInlineMount(\n element: Element,\n mod: IslandModule,\n mode: \"hydrate\" | \"render\",\n options: { force?: boolean } = {},\n): void {\n const fn = mod.mount ?? mod.default;\n if (typeof fn !== \"function\") {\n if (typeof process !== \"undefined\" && process.env && process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\"[zfb] inline island manifest entry did not export mount() or default()\");\n }\n return;\n }\n\n const fire = (): void => {\n // Re-check the guard in case `fire` is invoked from a deferred\n // scheduler (rIC/rAF/visibility) after a sibling caller already\n // mounted this element.\n if (mounted.has(element)) return;\n // When the deferred fire actually runs, the cancel handle is no longer\n // needed — remove it so pendingCancels doesn't hold stale entries.\n pendingCancels.delete(element);\n // Stale-mount race guard for deferred inline mounts: skip if the element\n // was detached (e.g. body swap) while the idle/visible callback was queued.\n if (!element.isConnected) return;\n // Lazy props parse: read and parse data-props only at mount time.\n // For deferred strategies (media, visible, idle) this avoids JSON.parse\n // work at boot time for islands that may never hydrate.\n const props = readProps(element);\n const unmountThunk = mod.unmount\n ? () => mod.unmount!(element)\n : () => {\n // noop — inline module does not expose unmount\n };\n mounted.set(element, unmountThunk);\n fn(props, element, mode);\n };\n\n if (mode === \"render\") {\n fire();\n return;\n }\n\n if (options.force) {\n fire();\n return;\n }\n\n const when = element.getAttribute(\"data-when\") ?? undefined;\n const { fired, cancel } = scheduleHydrateInternal(element, when, fire);\n // Track deferred-hydration cancel handle so cancelPendingIslands() can abort\n // idle / visibility callbacks before a body swap. (W1B §12.5)\n // Only register when the scheduler did NOT fire synchronously — a synchronous\n // fire means the island is already handling its mount and there is no\n // deferred callback to cancel. Registering noop after a sync fire would leave\n // a stale pendingCancels entry for an already-handled element. (#743)\n if (when && when !== \"load\" && !fired) {\n pendingCancels.set(element, cancel);\n }\n}\n\n/**\n * Unmount the mounted islands within `root` (default: `document.body`) that will\n * NOT survive the body swap.\n *\n * Walks `root` for `[data-zfb-island]` and `[data-zfb-island-skip-ssr]` elements,\n * looks up each element's unmount thunk in the `mounted` WeakMap, calls it (which\n * triggers `render(null, element)` for Preact or `root.unmount()` for React), and\n * removes the entry from the map so `mountNewIslands()` can re-mount later.\n *\n * Call this before `swapBodyElement(...)` so the OLD body's islands receive proper\n * framework lifecycle cleanup (useEffect teardowns, etc.) before being discarded.\n *\n * When `incomingBody` is supplied (the client-router passes the parsed incoming\n * document body), any island whose `data-zfb-transition-persist` id matches a\n * marker in that body is DELIBERATELY SKIPPED: swapBodyElement will physically\n * lift the node into the new body, so its component instance and internal state\n * must survive — unmounting it here would empty the container before the lift and\n * defeat the persist contract (issue #1389). Omit `incomingBody` (or pass null)\n * to unmount everything, the pre-#1389 behavior.\n *\n * No-op for elements not in the `mounted` map (e.g. never-mounted or already cleaned up).\n */\nexport function unmountIslands(\n root: ParentNode = document.body,\n incomingBody?: ParentNode | null,\n): void {\n const selector = \"[data-zfb-island],[data-zfb-island-skip-ssr]\";\n // Persist ids that `swapBodyElement` will physically LIFT from the old body\n // into the incoming body — an old marker survives iff the incoming body has a\n // marker with the same `data-zfb-transition-persist` id. Those DOM nodes are\n // moved, not discarded, so their component instance and internal state MUST\n // survive the swap: skip their framework unmount here or the persist contract\n // preserves nothing (port-spec §12.3.1 case (a) / issue #1389). A persisted\n // island whose props changed is skipped here too — its refreshed remount runs\n // later in mountNewIslands via the `data-zfb-island-remount` flag (see\n // `clearMountedForRemount`) swapBodyElement sets. With no incoming body (a\n // call outside a swap) nothing is preserved, so the walk is byte-identical to\n // the pre-#1389 behavior.\n const preservedPersistIds = collectPersistIds(incomingBody);\n const elements = root.querySelectorAll<HTMLElement>(selector);\n for (const el of Array.from(elements)) {\n const persistId = el.getAttribute(PERSIST_ATTR);\n if (persistId !== null && preservedPersistIds.has(persistId)) continue;\n const thunk = mounted.get(el);\n if (thunk) {\n thunk();\n mounted.delete(el);\n }\n }\n}\n\n/**\n * Collect the `data-zfb-transition-persist` ids present in the incoming body so\n * `unmountIslands` can tell which old-body islands `swapBodyElement` will lift\n * (and therefore must be left mounted). Returns an empty set when no incoming\n * body is supplied.\n */\nfunction collectPersistIds(incomingBody?: ParentNode | null): Set<string> {\n const ids = new Set<string>();\n if (!incomingBody) return ids;\n for (const el of incomingBody.querySelectorAll(`[${PERSIST_ATTR}]`)) {\n const id = el.getAttribute(PERSIST_ATTR);\n if (id !== null) ids.add(id);\n }\n return ids;\n}\n\nfunction readProps(element: Element): Record<string, unknown> {\n const raw = element.getAttribute(\"data-props\");\n if (!raw) return {};\n try {\n const parsed = JSON.parse(raw) as unknown;\n // Reject arrays explicitly: `typeof [] === \"object\"` is true but\n // an array is not a valid props bag, and passing it through would\n // mean the component receives index-keyed values where it\n // expected a record. Fall through to the empty-object default\n // instead of forwarding a malformed shape.\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n } catch {\n // fall through\n }\n return {};\n}\n\n/**\n * Indirection so tests can stub the dynamic import without intercepting\n * the global `import()`. In production this is a thin wrapper over\n * native `import(url)`.\n */\nlet importImpl: (url: string) => Promise<IslandModule> = (url) =>\n // Modern bundlers (esbuild, Vite, Rollup, webpack) preserve a plain\n // `import(<dynamic>)` call when the argument isn't a static literal,\n // so we no longer need the `new Function(...)` indirection — which\n // also failed under strict CSPs that disallow `unsafe-eval`.\n import(/* @vite-ignore */ /* webpackIgnore: true */ url) as Promise<IslandModule>;\n\nfunction importIsland(url: string): Promise<IslandModule> {\n return importImpl(url);\n}\n\n/**\n * Test-only seam. Replace the module dynamic-import with a fake.\n * Returns the previous implementation so tests can restore it.\n */\nexport function __setIslandImporterForTests(\n impl: (url: string) => Promise<IslandModule>,\n): (url: string) => Promise<IslandModule> {\n const prev = importImpl;\n importImpl = impl;\n return prev;\n}\n\n/**\n * Test-only seam. Returns whether the given element has an entry in the\n * module-private `pendingCancels` Map. Used to assert that a synchronous\n * scheduler fire does not leave a stale entry behind. (#743)\n */\nexport function __hasPendingCancelForTests(element: Element): boolean {\n return pendingCancels.has(element);\n}\n"]}
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,EAAE;AACF,sEAAsE;AACtE,sEAAsE;AACtE,wEAAwE;AACxE,uEAAuE;AACvE,mBAAmB;AACnB,EAAE;AACF,kDAAkD;AAClD,sEAAsE;AACtE,gDAAgD;AAChD,2EAA2E;AAC3E,wEAAwE;AACxE,sEAAsE;AACtE,sCAAsC;AACtC,6CAA6C;AAC7C,EAAE;AACF,2EAA2E;AAC3E,oEAAoE;AACpE,sEAAsE;AACtE,8DAA8D;AAE9D,OAAO,EAAE,WAAW,EAAa,MAAM,YAAY,CAAC;AAiBpD,MAAM,CAAC,GAAG,UAA6B,CAAC;AAExC;;;;GAIG;AACH,SAAS,uBAAuB,CAC9B,MAAe,EACf,IAA+B,EAC/B,IAAgB;IAEhB,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAEnC,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;QACxB,IAAI,EAAE,CAAC;QACP,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACvC,CAAC;IAED,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;QACxB,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;IACtD,CAAC;IAED,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACrC,CAAC;IAED,YAAY;IACZ,OAAO,eAAe,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAe,EACf,IAA+B,EAC/B,IAAgB;IAEhB,OAAO,uBAAuB,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,MAAM,CAAC;AAC5D,CAAC;AAED,SAAS,IAAI;IACX,sBAAsB;AACxB,CAAC;AAED;;;;;GAKG;AACH,SAAS,OAAO,CAAC,EAAc;IAI7B,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,OAAO;QACL,GAAG;YACD,IAAI,SAAS,IAAI,KAAK;gBAAE,OAAO;YAC/B,KAAK,GAAG,IAAI,CAAC;YACb,EAAE,EAAE,CAAC;QACP,CAAC;QACD,MAAM;YACJ,IAAI,KAAK;gBAAE,OAAO,IAAI,CAAC;YACvB,SAAS,GAAG,IAAI,CAAC;YACjB,OAAO,KAAK,CAAC;QACf,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,IAAgB;IACpC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3B,IAAI,OAAO,CAAC,CAAC,mBAAmB,KAAK,UAAU,EAAE,CAAC;QAChD,MAAM,MAAM,GAAG,CAAC,CAAC,mBAAmB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/C,OAAO,GAAG,EAAE;YACV,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YACnC,IAAI,YAAY;gBAAE,OAAO;YACzB,IAAI,OAAO,CAAC,CAAC,kBAAkB,KAAK,UAAU;gBAAE,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC;QAC/E,CAAC,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACvC,OAAO,GAAG,EAAE;QACV,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACnC,IAAI,YAAY;YAAE,OAAO;QACzB,YAAY,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CACtB,MAAe,EACf,IAAgB;IAEhB,MAAM,QAAQ,GAAG,CAAC,CAAC,oBAAoB,CAAC;IAExC,qEAAqE;IACrE,qEAAqE;IACrE,IAAI,OAAO,QAAQ,KAAK,UAAU,EAAE,CAAC;QACnC,IAAI,EAAE,CAAC;QACP,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACvC,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAC3B,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE;QACf,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;gBACzB,GAAG,CAAC,UAAU,EAAE,CAAC;gBACjB,IAAI,CAAC,GAAG,EAAE,CAAC;gBACX,OAAO;YACT,CAAC;QACH,CAAC;IACH,CAAC,EACD,EAAE,SAAS,EAAE,CAAC,EAAE,CACjB,CAAC;IAEF,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAEzB,OAAO;QACL,KAAK,EAAE,KAAK;QACZ,MAAM,EAAE,GAAG,EAAE;YACX,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YACnC,IAAI,YAAY;gBAAE,OAAO;YACzB,QAAQ,CAAC,UAAU,EAAE,CAAC;QACxB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,MAAe,EAAE,IAAgB;IACtD,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;IAEhD,wEAAwE;IACxE,sEAAsE;IACtE,cAAc;IACd,IAAI,OAAO,CAAC,CAAC,UAAU,KAAK,UAAU,IAAI,CAAC,KAAK,EAAE,CAAC;QACjD,IAAI,EAAE,CAAC;QACP,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACvC,CAAC;IAED,MAAM,GAAG,GAAG,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IAEhC,qEAAqE;IACrE,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;QAChB,IAAI,EAAE,CAAC;QACP,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACvC,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3B,IAAI,cAAc,GAAG,IAAI,CAAC;IAE1B,6DAA6D;IAC7D,qEAAqE;IACrE,oEAAoE;IACpE,wEAAwE;IACxE,oBAAoB;IACpB,MAAM,OAAO,GAAG,CAAC,CAAsB,EAAQ,EAAE;QAC/C,IAAI,CAAC,CAAC,CAAC,OAAO;YAAE,OAAO,CAAC,0BAA0B;QAClD,cAAc,EAAE,CAAC;QACjB,IAAI,CAAC,GAAG,EAAE,CAAC;IACb,CAAC,CAAC;IAEF,sEAAsE;IACtE,uEAAuE;IACvE,sEAAsE;IACtE,2EAA2E;IAC3E,IAAI,OAAO,GAAG,CAAC,gBAAgB,KAAK,UAAU,EAAE,CAAC;QAC/C,GAAG,CAAC,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACxC,cAAc,GAAG,GAAG,EAAE,CAAC,GAAG,CAAC,mBAAmB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACpE,CAAC;SAAM,IAAI,OAAO,GAAG,CAAC,WAAW,KAAK,UAAU,EAAE,CAAC;QACjD,GAAG,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QACzB,cAAc,GAAG,GAAG,EAAE,CAAC,GAAG,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;IACrD,CAAC;SAAM,CAAC;QACN,IAAI,EAAE,CAAC;QACP,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACvC,CAAC;IAED,OAAO;QACL,KAAK,EAAE,KAAK;QACZ,MAAM,EAAE,GAAG,EAAE;YACX,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YACnC,IAAI,YAAY;gBAAE,OAAO;YACzB,cAAc,EAAE,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC;AA4ED,6EAA6E;AAC7E,iFAAiF;AACjF,gFAAgF;AAChF,4EAA4E;AAC5E,sEAAsE;AACtE,MAAM,YAAY,GAAG,6BAA6B,CAAC;AAEnD,iFAAiF;AACjF,kFAAkF;AAClF,iFAAiF;AACjF,4DAA4D;AAC5D,MAAM,mBAAmB,GAAG,yBAAyB,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,yBAAyB,CAAC;AAE7D,8DAA8D;AAC9D,2EAA2E;AAC3E,0EAA0E;AAC1E,2DAA2D;AAC3D,MAAM,OAAO,GAAG,IAAI,OAAO,EAAuB,CAAC;AACnD,0EAA0E;AAC1E,+EAA+E;AAC/E,MAAM,YAAY,GAAG,IAAI,OAAO,EAAW,CAAC;AAC5C,uEAAuE;AACvE,oEAAoE;AACpE,qEAAqE;AACrE,4DAA4D;AAC5D,sEAAsE;AACtE,sEAAsE;AACtE,sEAAsE;AACtE,8CAA8C;AAC9C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAW,CAAC;AAEvC,sFAAsF;AACtF,wFAAwF;AACxF,oFAAoF;AACpF,uFAAuF;AACvF,sFAAsF;AACtF,8DAA8D;AAC9D,IAAI,gBAAgB,GAA0B,IAAI,CAAC;AAEnD,kEAAkE;AAClE,wCAAwC;AACxC,0FAA0F;AAC1F,yDAAyD;AACzD,MAAM,cAAc,GAAG,IAAI,GAAG,EAAuB,CAAC;AAEtD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,QAAwB;IACnD,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO;IAE5C,qEAAqE;IACrE,gBAAgB,GAAG,QAAQ,CAAC;IAE5B,MAAM,UAAU,GAAG,QAAQ,CAAC,gBAAgB,CAAc,mBAAmB,CAAC,CAAC;IAC/E,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACxC,uBAAuB,CAAC,EAAE,CAAC,CAAC;QAC5B,gEAAgE;QAChE,iEAAiE;QACjE,6DAA6D;QAC7D,+DAA+D;QAC/D,YAAY;QACZ,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,iBAAiB,CAAC,CAAC;QAChD,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,kBAAkB,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAC7B,aAAa,CAAC,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;IAC/C,CAAC;IAED,MAAM,cAAc,GAAG,QAAQ,CAAC,gBAAgB,CAAc,4BAA4B,CAAC,CAAC;IAC5F,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC;QAC5C,uBAAuB,CAAC,EAAE,CAAC,CAAC;QAC5B,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,0BAA0B,CAAC,CAAC;QACzD,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,kBAAkB,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAC7B,aAAa,CAAC,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC9C,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe;IAC7B,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO;IAC5C,IAAI,gBAAgB,KAAK,IAAI;QAAE,OAAO;IAEtC,MAAM,QAAQ,GAAG,gBAAgB,CAAC;IAElC,MAAM,UAAU,GAAG,QAAQ,CAAC,gBAAgB,CAAc,mBAAmB,CAAC,CAAC;IAC/E,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACxC,uBAAuB,CAAC,EAAE,CAAC,CAAC;QAC5B,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,iBAAiB,CAAC,CAAC;QAChD,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,yEAAyE;QACzE,6EAA6E;QAC7E,2EAA2E;QAC3E,gEAAgE;QAChE,MAAM,YAAY,GAAG,sBAAsB,CAAC,EAAE,CAAC,CAAC;QAChD,kBAAkB,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAC7B,aAAa,CAAC,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,cAAc,GAAG,QAAQ,CAAC,gBAAgB,CAAc,4BAA4B,CAAC,CAAC;IAC5F,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC;QAC5C,uBAAuB,CAAC,EAAE,CAAC,CAAC;QAC5B,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,0BAA0B,CAAC,CAAC;QACzD,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,kBAAkB,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAC7B,aAAa,CAAC,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC9C,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,SAAS,sBAAsB,CAAC,EAAW;IACzC,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,mBAAmB,CAAC;QAAE,OAAO,KAAK,CAAC;IACxD,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAAE,OAAO,KAAK,CAAC;IAElC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC9B,IAAI,KAAK,EAAE,CAAC;QACV,IAAI,CAAC;YACH,KAAK,EAAE,CAAC;QACV,CAAC;gBAAS,CAAC;YACT,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACnB,EAAE,CAAC,eAAe,CAAC,mBAAmB,CAAC,CAAC;YACxC,EAAE,CAAC,eAAe,CAAC,mBAAmB,CAAC,CAAC;QAC1C,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,EAAE,CAAC,eAAe,CAAC,mBAAmB,CAAC,CAAC;IACxC,EAAE,CAAC,eAAe,CAAC,mBAAmB,CAAC,CAAC;IACxC,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,uBAAuB,CAAC,EAAW;IAC1C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAAE,EAAE,CAAC,eAAe,CAAC,mBAAmB,CAAC,CAAC;AAChE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB;IAClC,KAAK,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,IAAI,cAAc,EAAE,CAAC;QAC1C,MAAM,EAAE,CAAC;QACT,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC5B,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAS,kBAAkB,CAAC,EAAW,EAAE,aAAqB;IAC5D,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY,EAAE,CAAC;QAC/F,OAAO;IACT,CAAC;IACD,IAAI,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;QAAE,OAAO;IACjC,MAAM,MAAM,GAAG,EAAE,CAAC,aAAa,CAAC;IAChC,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,UAAU;QAAE,OAAO;IAC5D,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,8CAA8C,CAAC,CAAC;IAChF,IAAI,CAAC,QAAQ;QAAE,OAAO;IACtB,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACrB,sCAAsC;IACtC,OAAO,CAAC,IAAI,CACV,iBAAiB,aAAa,4CAA4C;QACxE,0EAA0E;QAC1E,8CAA8C;QAC9C,gBAAgB,aAAa,sDAAsD;QACnF,yDAAyD,CAC5D,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CACpB,QAAwB,EACxB,OAAgB,EAChB,aAAqB,EACrB,IAA0B,EAC1B,UAA+B,EAAE;IAEjC,oEAAoE;IACpE,kEAAkE;IAClE,gDAAgD;IAChD,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO;IAEzD,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAC;IACtC,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;QAClB,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY,EAAE,CAAC;YAC9F,sCAAsC;YACtC,OAAO,CAAC,IAAI,CACV,iDAAiD,aAAa,MAAM;gBAClE,6DAA6D,CAChE,CAAC;QACJ,CAAC;QACD,OAAO;IACT,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,IAAI,SAAS,CAAC;IAE5D,uBAAuB;IACvB,EAAE;IACF,qEAAqE;IACrE,2DAA2D;IAC3D,qEAAqE;IACrE,sEAAsE;IACtE,mEAAmE;IACnE,+CAA+C;IAC/C,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,eAAe,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QAC/C,OAAO;IACT,CAAC;IAED,MAAM,GAAG,GAAW,KAAK,CAAC;IAE1B,MAAM,IAAI,GAAG,GAAS,EAAE;QACtB,iEAAiE;QACjE,gEAAgE;QAChE,iDAAiD;QACjD,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO;QAEzD,uEAAuE;QACvE,mEAAmE;QACnE,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAE/B,uEAAuE;QACvE,mEAAmE;QACnE,sEAAsE;QACtE,mEAAmE;QACnE,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;QAEjC,6DAA6D;QAC7D,iEAAiE;QACjE,iDAAiD;QACjD,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAErB,iEAAiE;QACjE,wDAAwD;QACxD,uBAAuB;QACvB,EAAE;QACF,8DAA8D;QAC9D,+DAA+D;QAC/D,iEAAiE;QACjE,IAAI,OAA8B,CAAC;QACnC,IAAI,CAAC;YACH,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QAC9B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,4DAA4D;YAC5D,6DAA6D;YAC7D,sBAAsB;YACtB,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACxB,sCAAsC;YACtC,OAAO,CAAC,KAAK,CAAC,4CAA4C,GAAG,EAAE,EAAE,GAAG,CAAC,CAAC;YACtE,OAAO;QACT,CAAC;QACD,OAAO,CAAC,IAAI,CACV,CAAC,GAAG,EAAE,EAAE;YACN,MAAM,EAAE,GAAG,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC;YACpC,IAAI,OAAO,EAAE,KAAK,UAAU,EAAE,CAAC;gBAC7B,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBACxB,IACE,OAAO,OAAO,KAAK,WAAW;oBAC9B,OAAO,CAAC,GAAG;oBACX,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY,EACxC,CAAC;oBACD,sCAAsC;oBACtC,OAAO,CAAC,IAAI,CAAC,0BAA0B,GAAG,sCAAsC,CAAC,CAAC;gBACpF,CAAC;gBACD,OAAO;YACT,CAAC;YACD,gEAAgE;YAChE,iEAAiE;YACjE,+DAA+D;YAC/D,8DAA8D;YAC9D,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;gBACzB,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBACxB,OAAO;YACT,CAAC;YACD,MAAM,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,mBAAmB,CAAC,CAAC;YACrE,MAAM,aAAa,GAAG,kBAAkB,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YACtE,IAAI,kBAAkB;gBAAE,OAAO,CAAC,eAAe,CAAC,mBAAmB,CAAC,CAAC;YACrE,MAAM,YAAY,GAAG,GAAG,CAAC,OAAO;gBAC9B,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,OAAQ,CAAC,OAAO,CAAC;gBAC7B,CAAC,CAAC,GAAG,EAAE;oBACH,wCAAwC;gBAC1C,CAAC,CAAC;YACN,IAAI,CAAC;gBACH,EAAE,CAAC,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;gBACjC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;gBACnC,OAAO,CAAC,YAAY,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC;YAChD,CAAC;oBAAS,CAAC;gBACT,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAC1B,CAAC;QACH,CAAC,EACD,CAAC,GAAY,EAAE,EAAE;YACf,2DAA2D;YAC3D,6DAA6D;YAC7D,sCAAsC;YACtC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACxB,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACxB,sCAAsC;YACtC,OAAO,CAAC,KAAK,CAAC,sCAAsC,GAAG,EAAE,EAAE,GAAG,CAAC,CAAC;QAClE,CAAC,CACF,CAAC;IACJ,CAAC,CAAC;IAEF,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,+DAA+D;QAC/D,6DAA6D;QAC7D,uCAAuC;QACvC,IAAI,EAAE,CAAC;QACP,OAAO;IACT,CAAC;IAED,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,IAAI,EAAE,CAAC;QACP,OAAO;IACT,CAAC;IAED,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,uBAAuB,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACvE,6EAA6E;IAC7E,8DAA8D;IAC9D,8EAA8E;IAC9E,uEAAuE;IACvE,8EAA8E;IAC9E,sEAAsE;IACtE,IAAI,IAAI,IAAI,IAAI,KAAK,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;QACtC,cAAc,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,eAAe,CACtB,OAAgB,EAChB,GAAiB,EACjB,IAA0B,EAC1B,UAA+B,EAAE;IAEjC,MAAM,EAAE,GAAG,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC;IACpC,IAAI,OAAO,EAAE,KAAK,UAAU,EAAE,CAAC;QAC7B,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY,EAAE,CAAC;YAC9F,sCAAsC;YACtC,OAAO,CAAC,IAAI,CAAC,wEAAwE,CAAC,CAAC;QACzF,CAAC;QACD,OAAO;IACT,CAAC;IAED,MAAM,IAAI,GAAG,GAAS,EAAE;QACtB,+DAA+D;QAC/D,gEAAgE;QAChE,wBAAwB;QACxB,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO;QACjC,uEAAuE;QACvE,mEAAmE;QACnE,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC/B,yEAAyE;QACzE,4EAA4E;QAC5E,IAAI,CAAC,OAAO,CAAC,WAAW;YAAE,OAAO;QACjC,kEAAkE;QAClE,wEAAwE;QACxE,wDAAwD;QACxD,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;QACjC,MAAM,YAAY,GAAG,GAAG,CAAC,OAAO;YAC9B,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,OAAQ,CAAC,OAAO,CAAC;YAC7B,CAAC,CAAC,GAAG,EAAE;gBACH,+CAA+C;YACjD,CAAC,CAAC;QACN,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QACnC,OAAO,CAAC,YAAY,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC;IAChD,CAAC,CAAC;IAEF,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,IAAI,EAAE,CAAC;QACP,OAAO;IACT,CAAC;IAED,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,IAAI,EAAE,CAAC;QACP,OAAO;IACT,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,IAAI,SAAS,CAAC;IAC5D,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,uBAAuB,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACvE,6EAA6E;IAC7E,8DAA8D;IAC9D,8EAA8E;IAC9E,sEAAsE;IACtE,8EAA8E;IAC9E,sEAAsE;IACtE,IAAI,IAAI,IAAI,IAAI,KAAK,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;QACtC,cAAc,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAmB,QAAQ,CAAC,IAAI,EAChC,YAAgC;IAEhC,MAAM,QAAQ,GAAG,8CAA8C,CAAC;IAChE,4EAA4E;IAC5E,8EAA8E;IAC9E,6EAA6E;IAC7E,4EAA4E;IAC5E,8EAA8E;IAC9E,4EAA4E;IAC5E,8EAA8E;IAC9E,uEAAuE;IACvE,2EAA2E;IAC3E,8EAA8E;IAC9E,0BAA0B;IAC1B,MAAM,mBAAmB,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAC5D,MAAM,QAAQ,GAAG,IAAI,CAAC,gBAAgB,CAAc,QAAQ,CAAC,CAAC;IAC9D,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtC,MAAM,SAAS,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;QAChD,IAAI,SAAS,KAAK,IAAI,IAAI,mBAAmB,CAAC,GAAG,CAAC,SAAS,CAAC;YAAE,SAAS;QACvE,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9B,IAAI,CAAC;YACH,KAAK,EAAE,EAAE,CAAC;QACZ,CAAC;gBAAS,CAAC;YACT,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACnB,EAAE,CAAC,eAAe,CAAC,mBAAmB,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,YAAgC;IACzD,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,IAAI,CAAC,YAAY;QAAE,OAAO,GAAG,CAAC;IAC9B,KAAK,MAAM,EAAE,IAAI,YAAY,CAAC,gBAAgB,CAAC,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;QACpE,MAAM,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;QACzC,IAAI,EAAE,KAAK,IAAI;YAAE,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,SAAS,CAAC,OAAgB;IACjC,MAAM,GAAG,GAAG,OAAO,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;IAC/C,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;QAC1C,iEAAiE;QACjE,kEAAkE;QAClE,0DAA0D;QAC1D,8DAA8D;QAC9D,2CAA2C;QAC3C,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACnE,OAAO,MAAiC,CAAC;QAC3C,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,eAAe;IACjB,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;GAIG;AACH,IAAI,UAAU,GAA2C,CAAC,GAAG,EAAE,EAAE;AAC/D,oEAAoE;AACpE,qEAAqE;AACrE,mEAAmE;AACnE,6DAA6D;AAC7D,MAAM,CAAC,kBAAkB,CAAC,yBAAyB,CAAC,GAAG,CAA0B,CAAC;AAEpF,SAAS,YAAY,CAAC,GAAW;IAC/B,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,2BAA2B,CACzC,IAA4C;IAE5C,MAAM,IAAI,GAAG,UAAU,CAAC;IACxB,UAAU,GAAG,IAAI,CAAC;IAClB,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,0BAA0B,CAAC,OAAgB;IACzD,OAAO,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AACrC,CAAC","sourcesContent":["// Hydration scheduling helper consumed by the hydration runtime (Sub 3).\n//\n// Sub 3 owns the hydration runtime that walks the DOM, finds elements\n// marked with `data-zfb-island`, and dispatches each one through this\n// helper to decide *when* to fire the actual hydrate() call. The helper\n// itself does not know how to hydrate — it only schedules the supplied\n// `fire` callback.\n//\n// The branching matches the `When` union exactly:\n// \"visible\" → IntersectionObserver, threshold 0.0, hydrate on first\n// intersection, then disconnect.\n// \"idle\" → requestIdleCallback if available, otherwise setTimeout(0).\n// \"media\" → matchMedia(target's data-media), hydrate when the query\n// first matches (now or on a later change event), then\n// remove the listener.\n// \"load\" → immediate, synchronous fire.\n//\n// Anything else is treated as \"load\" (with a console.warn in development).\n// The helper is environment-tolerant: callers can run it in jsdom /\n// happy-dom or bare Node, and the absence of `IntersectionObserver` /\n// `requestIdleCallback` / `matchMedia` is handled gracefully.\n\nimport { resolveWhen, type When } from \"./types.js\";\n\n/**\n * Subset of the global object that this module touches. Cast once at the\n * module top so individual scheduler functions don't repeat the inline\n * widening.\n */\ntype SchedulerGlobal = typeof globalThis & {\n requestIdleCallback?: (\n cb: (deadline: { didTimeout: boolean; timeRemaining: () => number }) => void,\n options?: { timeout?: number },\n ) => number;\n cancelIdleCallback?: (handle: number) => void;\n IntersectionObserver?: typeof IntersectionObserver;\n matchMedia?: typeof matchMedia;\n};\n\nconst g = globalThis as SchedulerGlobal;\n\n/**\n * Internal variant of `scheduleHydrate` that also reports whether the fire\n * callback was invoked synchronously. Unexported — call sites in this module\n * use this to decide whether to register a `pendingCancels` entry.\n */\nfunction scheduleHydrateInternal(\n target: Element,\n when: When | string | undefined,\n fire: () => void,\n): { fired: boolean; cancel: () => void } {\n const resolved = resolveWhen(when);\n\n if (resolved === \"load\") {\n fire();\n return { fired: true, cancel: noop };\n }\n\n if (resolved === \"idle\") {\n return { fired: false, cancel: scheduleIdle(fire) };\n }\n\n if (resolved === \"media\") {\n return scheduleMedia(target, fire);\n }\n\n // \"visible\"\n return scheduleVisible(target, fire);\n}\n\n/**\n * Schedule a hydration `fire` callback for `target` according to `when`.\n *\n * Returns a `cancel` function that aborts the scheduling if it has not\n * fired yet. After firing, calling `cancel` is a no-op. If the helper\n * cannot find the relevant browser API (e.g. running in pure Node with no\n * polyfill), it falls back to firing synchronously so server-side smoke\n * tests still observe the call.\n */\nexport function scheduleHydrate(\n target: Element,\n when: When | string | undefined,\n fire: () => void,\n): () => void {\n return scheduleHydrateInternal(target, when, fire).cancel;\n}\n\nfunction noop(): void {\n // intentionally empty\n}\n\n/**\n * Build a one-shot gate around `fn`. The returned `run` invokes `fn`\n * exactly once provided `cancel` has not been called first; `cancel`\n * marks the gate as cancelled (later `run` invocations become no-ops)\n * and reports whether the gate had already fired.\n */\nfunction oneShot(fn: () => void): {\n run: () => void;\n cancel: () => boolean;\n} {\n let fired = false;\n let cancelled = false;\n return {\n run(): void {\n if (cancelled || fired) return;\n fired = true;\n fn();\n },\n cancel(): boolean {\n if (fired) return true;\n cancelled = true;\n return false;\n },\n };\n}\n\nfunction scheduleIdle(fire: () => void): () => void {\n const gate = oneShot(fire);\n\n if (typeof g.requestIdleCallback === \"function\") {\n const handle = g.requestIdleCallback(gate.run);\n return () => {\n const alreadyFired = gate.cancel();\n if (alreadyFired) return;\n if (typeof g.cancelIdleCallback === \"function\") g.cancelIdleCallback(handle);\n };\n }\n\n const handle = setTimeout(gate.run, 0);\n return () => {\n const alreadyFired = gate.cancel();\n if (alreadyFired) return;\n clearTimeout(handle);\n };\n}\n\nfunction scheduleVisible(\n target: Element,\n fire: () => void,\n): { fired: boolean; cancel: () => void } {\n const Observer = g.IntersectionObserver;\n\n // No IntersectionObserver (e.g. very old browsers, bare Node) — fail\n // open and hydrate immediately so the island is at least functional.\n if (typeof Observer !== \"function\") {\n fire();\n return { fired: true, cancel: noop };\n }\n\n const gate = oneShot(fire);\n const observer = new Observer(\n (entries, obs) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n obs.disconnect();\n gate.run();\n return;\n }\n }\n },\n { threshold: 0 },\n );\n\n observer.observe(target);\n\n return {\n fired: false,\n cancel: () => {\n const alreadyFired = gate.cancel();\n if (alreadyFired) return;\n observer.disconnect();\n },\n };\n}\n\nfunction scheduleMedia(target: Element, fire: () => void): { fired: boolean; cancel: () => void } {\n const query = target.getAttribute(\"data-media\");\n\n // No matchMedia API (e.g. bare Node, very old browser) or missing/empty\n // query — fail open and hydrate immediately so the island is at least\n // functional.\n if (typeof g.matchMedia !== \"function\" || !query) {\n fire();\n return { fired: true, cancel: noop };\n }\n\n const mql = g.matchMedia(query);\n\n // Already matches — fire synchronously (no pending listener needed).\n if (mql.matches) {\n fire();\n return { fired: true, cancel: noop };\n }\n\n const gate = oneShot(fire);\n\n let removeListener = noop;\n\n // Listen for the first change event where the query matches.\n // We do NOT use `{once:true}` because we must ignore un-match events\n // (e.g. viewport widens back above breakpoint) and only fire on the\n // first match event — `{once:true}` would consume any change, including\n // un-match changes.\n const handler = (e: MediaQueryListEvent): void => {\n if (!e.matches) return; // ignore un-match changes\n removeListener();\n gate.run();\n };\n\n // Modern browsers expose the EventTarget API on MediaQueryList; older\n // Safari (<14) only has the deprecated addListener/removeListener pair\n // and throws on addEventListener. Prefer modern, fall back to legacy,\n // and fail open when neither exists (mirrors the missing-matchMedia case).\n if (typeof mql.addEventListener === \"function\") {\n mql.addEventListener(\"change\", handler);\n removeListener = () => mql.removeEventListener(\"change\", handler);\n } else if (typeof mql.addListener === \"function\") {\n mql.addListener(handler);\n removeListener = () => mql.removeListener(handler);\n } else {\n fire();\n return { fired: true, cancel: noop };\n }\n\n return {\n fired: false,\n cancel: () => {\n const alreadyFired = gate.cancel();\n if (alreadyFired) return;\n removeListener();\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// mountIslands — DOM walk + dynamic-import dispatcher.\n//\n// `mountIslands` is the entry point the generated `islands-runtime-<hash>.js`\n// bundle calls at script load time. It walks the DOM for the two island\n// markers emitted by the server-side hydration step and the `<Island>`\n// JSX wrapper:\n//\n// 1. `[data-zfb-island]` — SSR'd islands. We `hydrate()` (Preact) /\n// `hydrateRoot()` (React) against the existing server-rendered\n// DOM, gated by `scheduleHydrate(when)`.\n//\n// 2. `[data-zfb-island-skip-ssr]` — SSR-skip islands. The server\n// emitted no markup for these, so we `render()` (Preact) /\n// `createRoot().render()` (React). Skipping hydrate for this case\n// avoids the hydrate-mismatch warnings React/Preact would emit\n// against an empty DOM container.\n//\n// The per-island bundles each export a `mount(props, element, mode)`\n// function (see zfb_islands::render_island_entry_source). The\n// framework-specific glue lives inside that bundle, so this runtime is\n// framework-agnostic.\n//\n// ## Module-level singleton\n//\n// Dynamic imports of the same URL are cached by the JS runtime, so\n// \"switching pages\" (in an SPA shell) reuses the loaded bundle for free.\n// We keep an extra in-memory dedup map keyed by element so an island is\n// never mounted twice (e.g. on hot-reload / repeat-mount scenarios).\n// ---------------------------------------------------------------------------\n\n/**\n * The shape of the default export each per-island bundle ships.\n *\n * `mode === \"hydrate\"` is used for SSR'd islands, `\"render\"` for\n * SSR-skip islands.\n */\ntype IslandMount = (\n props: Record<string, unknown>,\n element: Element,\n mode: \"hydrate\" | \"render\",\n) => void;\n\ntype IslandUnmount = (element: Element) => void;\n\ninterface IslandModule {\n mount?: IslandMount;\n default?: IslandMount;\n unmount?: IslandUnmount;\n}\n\n/**\n * Map of `componentName → island descriptor` baked into the runtime entry.\n *\n * Two descriptor shapes are accepted so the same `mountIslands` runtime\n * handles both bundling strategies the build emits:\n *\n * 1. `string` — a per-island bundle URL. The runtime fetches it via\n * dynamic `import()` and reads `mount` / `default` off the loaded\n * module. Used by the per-island bundling path\n * (`bundle_per_island` / `render_runtime_entry_source`).\n *\n * 2. `IslandModule` — an inline module-shaped object whose `mount` (or\n * `default`) is called directly. Used by the shared-bundle path\n * (`render_shared_bundle_entry_source`): every island's source code\n * is already in the same bundle, so the synthesised entry can hand\n * the runtime the constructed mount functions inline without a\n * second HTTP fetch. This preserves the one-request shared-bundle\n * contract while giving up nothing on hydration semantics\n * (zudolab/zudo-doc#1355 wave 6).\n */\nexport type IslandManifestValue = string | IslandModule;\nexport type IslandManifest = Readonly<Record<string, IslandManifestValue>>;\n\n// data-zfb-transition-persist marker attribute — the client-router's persist\n// contract. Mirrored from client-router/swap-functions.ts: that package owns the\n// body swap (lifting persisted nodes into the incoming body), this package owns\n// island mount/unmount. Both must agree on the literal string. See the port\n// spec at packages/zfb-runtime/docs/client-router/port-spec.md §12.3.\nconst PERSIST_ATTR = \"data-zfb-transition-persist\";\n\n// Cross-package \"needs-remount\" flag set by client-router/swap-functions.ts on a\n// persisted island whose props changed across a body swap. Mirrored literal (same\n// cross-package contract as PERSIST_ATTR above — both packages must agree on the\n// string). Consumed by clearMountedForRemount(). See #1389.\nconst ISLAND_REMOUNT_ATTR = \"data-zfb-island-remount\";\n\n/**\n * Public DOM signal written after an island's mount function returns.\n *\n * State table (the marker is observational only and is never a mount guard):\n *\n * - initial: absent; `mountIslands` / `mountNewIslands` strip a marker that is\n * stale relative to this module instance's `mounted` map before scheduling.\n * - deferred idle / visible / media: absent while the scheduler is waiting.\n * - importing: absent while the URL module is in `pending`.\n * - mounted via URL: `scheduleMount`'s URL success handler writes it only after\n * `fn(propsForMount, element, mode)` returns, alongside the `mounted` entry.\n * - mounted via inline module: `fireInlineMount` writes it only after\n * `fn(props, element, mode)` returns, alongside the `mounted` entry.\n * - missing manifest entry: absent; `scheduleMount` returns without writing.\n * - no `mount` export: absent; both manifest paths return without writing.\n * - synchronous mount throw: absent; the `mounted` entry is not written, so a\n * later walk can retry the element.\n * - rejected import: absent; the URL rejection handler clears `pending` and\n * any defensive `mounted` entry.\n * - detached during import: absent; the URL success handler clears `pending`\n * and returns before calling mount.\n * - unmounted (discarded): `unmountIslands` clears the marker and `mounted`\n * entry in `finally`, even when the unmount thunk throws.\n * - unmounted (persisted-lifted): retained together with the `mounted` entry;\n * `unmountIslands` skips elements whose persist id exists in the incoming body.\n * - props-changed remount: `clearMountedForRemount` clears the marker and map\n * entry in `finally`, then the forced mount writes it again after mount returns.\n * - dev hot-swap over a marked DOM: a fresh module's `mountIslands` strips the\n * stale marker before scheduling, then writes it after its own mount returns.\n */\nexport const ISLAND_MOUNTED_ATTR = \"data-zfb-island-mounted\";\n\n// WeakMap<Element, unmount thunk> — replaces the old WeakSet.\n// Value is a per-element function that calls the bundle's unmount(element)\n// (or a noop if the bundle does not expose one). Used by unmountIslands()\n// to fire framework lifecycle cleanups before a body swap.\nconst mounted = new WeakMap<Element, () => void>();\n// Elements for which the nested-island self-wrap warning has already been\n// emitted. Guards against repeated warn spam across re-walks (e.g. SPA swaps).\nconst warnedNested = new WeakSet<Element>();\n// Elements with an in-flight dynamic import that has not yet resolved.\n// Two concurrent `mountIslands` invocations (or two `scheduleMount`\n// calls hitting the same element through different code paths) could\n// otherwise both pass the `mounted` guard and both spawn an\n// `importIsland(url)` -> `fn()` chain, double-mounting the component.\n// Adding the element to `pending` synchronously, before the import is\n// fired, closes that window; the entry is removed in both the success\n// (after `mounted.set`) and failure branches.\nconst pending = new WeakSet<Element>();\n\n// Module-level captured manifest — set by the first `mountIslands` call and reused by\n// `mountNewIslands()` so the client-router does not need to know the manifest directly.\n// Named technical cause (W1B §12.1): the router lives in @takazudo/zfb-runtime; the\n// islands manifest lives in @takazudo/zfb. Passing the manifest through the swap event\n// would require widening the event API or threading manifest into router options. The\n// captured-manifest pattern keeps the package boundary clean.\nlet capturedManifest: IslandManifest | null = null;\n\n// Map of element → cancel-function for deferred-hydration islands\n// (data-when=\"idle\"|\"visible\"|\"media\").\n// Populated in scheduleMount; consulted on `zfb:before-swap` so deferred fires do not run\n// against orphan elements after a body swap. (W1B §12.5)\nconst pendingCancels = new Map<Element, () => void>();\n\n/**\n * Walk the DOM and mount every `[data-zfb-island]` / `[data-zfb-island-skip-ssr]`\n * element using `manifest`.\n *\n * No-op when `document` is undefined (SSR, edge runtime). Safe to call\n * multiple times: each element is mounted at most once thanks to the\n * `mounted` WeakSet guard.\n *\n * The manifest is captured at module level so `mountNewIslands()` can re-use\n * it after an SPA body swap without needing the caller to re-supply it.\n */\nexport function mountIslands(manifest: IslandManifest): void {\n if (typeof document === \"undefined\") return;\n\n // Capture the manifest for post-swap re-walks via mountNewIslands().\n capturedManifest = manifest;\n\n const ssrIslands = document.querySelectorAll<HTMLElement>(\"[data-zfb-island]\");\n for (const el of Array.from(ssrIslands)) {\n stripStaleMountedMarker(el);\n // Skip the empty-skeleton case left behind when the server-side\n // rewriter has not run yet (data-zfb-island=\"\" with no component\n // name). The hydration emit step is expected to fill this in\n // before the page reaches the browser; if it didn't, we cannot\n // dispatch.\n const name = el.getAttribute(\"data-zfb-island\");\n if (!name) continue;\n warnIfNestedIsland(el, name);\n scheduleMount(manifest, el, name, \"hydrate\");\n }\n\n const skipSsrIslands = document.querySelectorAll<HTMLElement>(\"[data-zfb-island-skip-ssr]\");\n for (const el of Array.from(skipSsrIslands)) {\n stripStaleMountedMarker(el);\n const name = el.getAttribute(\"data-zfb-island-skip-ssr\");\n if (!name) continue;\n warnIfNestedIsland(el, name);\n scheduleMount(manifest, el, name, \"render\");\n }\n}\n\n/**\n * Re-walk the current document body and mount any new island markers introduced\n * by an SPA body swap. Uses the manifest captured by the previous `mountIslands`\n * call — no manifest arg required.\n *\n * The caller (client-router `router.ts`) invokes this after `swap()` + `runScripts()`\n * and before dispatching `zfb:page-load`, per W1B §12.2 contract.\n *\n * No-op when called before `mountIslands` (capturedManifest is null) or when\n * `document` is undefined.\n */\nexport function mountNewIslands(): void {\n if (typeof document === \"undefined\") return;\n if (capturedManifest === null) return;\n\n const manifest = capturedManifest;\n\n const ssrIslands = document.querySelectorAll<HTMLElement>(\"[data-zfb-island]\");\n for (const el of Array.from(ssrIslands)) {\n stripStaleMountedMarker(el);\n const name = el.getAttribute(\"data-zfb-island\");\n if (!name) continue;\n // A persisted island whose props changed across the body swap is flagged\n // for remount by swap-functions.swapBodyElement. Clear its surviving mounted\n // entry BEFORE scheduleMount's already-mounted guard so it re-mounts fresh\n // with the refreshed data-props. No-op for every other element.\n const forceRemount = clearMountedForRemount(el);\n warnIfNestedIsland(el, name);\n scheduleMount(manifest, el, name, \"hydrate\", { force: forceRemount });\n }\n\n const skipSsrIslands = document.querySelectorAll<HTMLElement>(\"[data-zfb-island-skip-ssr]\");\n for (const el of Array.from(skipSsrIslands)) {\n stripStaleMountedMarker(el);\n const name = el.getAttribute(\"data-zfb-island-skip-ssr\");\n if (!name) continue;\n warnIfNestedIsland(el, name);\n scheduleMount(manifest, el, name, \"render\");\n }\n}\n\n/**\n * Consume the cross-package \"needs-remount\" signal for the persist-props hybrid\n * path (port-spec §12.3.1 hybrid case / §12.3.2). When a persisted island's\n * props differ from the incoming markup, `swapBodyElement` refreshes the\n * surviving element's `data-props` and marks it with `ISLAND_REMOUNT_ATTR`.\n * That attribute is the ONLY channel that crosses the zfb-runtime → zfb package\n * boundary — the `mounted` map is module-private to this file, so a shared\n * in-memory \"needs-remount\" queue between the two packages is impossible; the\n * live DOM node carrying the flag IS the queue.\n *\n * On a flagged mounted element: fire the old instance's unmount thunk (so its\n * useEffect/framework cleanups run against the still-connected node), drop the\n * `mounted` entry so `scheduleMount`'s guard no longer short-circuits, strip the\n * flag, and ask the caller to force the replacement mount through immediately\n * instead of re-entering any deferred scheduler. This keeps a deferred persisted\n * island from blanking while it waits for idle/visible/media to fire again.\n *\n * On a flagged element whose URL import is still pending, leave the flag in\n * place. The already-running import's success handler consumes it after the\n * module resolves and re-reads `data-props` at that point, so a props refresh\n * that happened during the import wins without starting a duplicate import.\n *\n * A no-op for elements without the flag (the common case: fresh markers and\n * props-unchanged persisted islands).\n *\n * Scope: only the `[data-zfb-island]` (hydrated) loop calls this, mirroring the\n * writer side — swapBodyElement sets the flag only for `newTarget.matches(\n * \"[data-zfb-island]\")`, never for skip-ssr islands.\n */\nfunction clearMountedForRemount(el: Element): boolean {\n if (!el.hasAttribute(ISLAND_REMOUNT_ATTR)) return false;\n if (pending.has(el)) return false;\n\n const thunk = mounted.get(el);\n if (thunk) {\n try {\n thunk();\n } finally {\n mounted.delete(el);\n el.removeAttribute(ISLAND_MOUNTED_ATTR);\n el.removeAttribute(ISLAND_REMOUNT_ATTR);\n }\n return true;\n }\n el.removeAttribute(ISLAND_MOUNTED_ATTR);\n el.removeAttribute(ISLAND_REMOUNT_ATTR);\n return false;\n}\n\nfunction stripStaleMountedMarker(el: Element): void {\n if (!mounted.has(el)) el.removeAttribute(ISLAND_MOUNTED_ATTR);\n}\n\n/**\n * Cancel deferred-hydration callbacks for all islands in the old body before a\n * swap. Prevents idle / visibility callbacks from running against orphan elements\n * after `swapBodyElement` removes them from the live document. (W1B §12.5)\n *\n * Call this on `zfb:before-swap` (or equivalently, in the router's swap sequence\n * before `swap()` mutates the DOM). Fire-and-forget; safe to call if nothing is\n * pending.\n */\nexport function cancelPendingIslands(): void {\n for (const [el, cancel] of pendingCancels) {\n cancel();\n pendingCancels.delete(el);\n }\n}\n\n/**\n * Warn (once per element, dev-only) when an island marker element is found\n * nested inside another island marker. Self-wrapping an island — emitting a\n * `data-zfb-island` or `data-zfb-island-skip-ssr` container *inside* another\n * island component's render output — mis-hydrates because the runtime will\n * try to mount both the outer and inner islands independently. The outer\n * island's framework instance owns the inner DOM, so a second `hydrate()` /\n * `render()` call against the inner element races with the outer render and\n * produces undefined behaviour.\n *\n * The fix is to author the inner component bare (no `<Island>` in its own\n * render output) and apply the `<Island when=\"...\">` wrapper at the call site.\n */\nfunction warnIfNestedIsland(el: Element, componentName: string): void {\n if (typeof process === \"undefined\" || !process.env || process.env[\"NODE_ENV\"] === \"production\") {\n return;\n }\n if (warnedNested.has(el)) return;\n const parent = el.parentElement;\n if (!parent || typeof parent.closest !== \"function\") return;\n const ancestor = parent.closest(\"[data-zfb-island],[data-zfb-island-skip-ssr]\");\n if (!ancestor) return;\n warnedNested.add(el);\n // eslint-disable-next-line no-console\n console.warn(\n `[zfb] Island \"${componentName}\" is nested inside another island marker. ` +\n `Self-wrapping an island mis-hydrates: the outer framework instance owns ` +\n `the inner DOM, causing a conflicting mount. ` +\n `Fix: author \"${componentName}\" bare (remove <Island> from its own render output) ` +\n `and apply <Island when=\"...\"> at the call site instead.`,\n );\n}\n\nfunction scheduleMount(\n manifest: IslandManifest,\n element: Element,\n componentName: string,\n mode: \"hydrate\" | \"render\",\n options: { force?: boolean } = {},\n): void {\n // Skip elements already mounted OR currently importing — the latter\n // prevents two concurrent `mountIslands` calls from each firing a\n // separate dynamic import for the same element.\n if (mounted.has(element) || pending.has(element)) return;\n\n const entry = manifest[componentName];\n if (entry == null) {\n if (typeof process !== \"undefined\" && process.env && process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n `[zfb] no island manifest entry for component \"${componentName}\" — ` +\n `the runtime manifest is out of sync with the rendered HTML.`,\n );\n }\n return;\n }\n\n const when = element.getAttribute(\"data-when\") ?? undefined;\n\n // Two manifest shapes:\n //\n // - `string` (per-island bundle URL): fetch via dynamic `import()`\n // and call `mount` / `default` on the resolved module.\n // - `IslandModule` (inline descriptor): the shared-bundle path has\n // already imported every island's source into the same bundle and\n // constructed a mount function for it. Skip the dynamic import\n // and call the supplied function directly.\n if (typeof entry !== \"string\") {\n fireInlineMount(element, entry, mode, options);\n return;\n }\n\n const url: string = entry;\n\n const fire = (): void => {\n // Re-check both guards in case `fire` is invoked from a deferred\n // scheduler (rIC/rAF/visibility) after a sibling caller already\n // mounted or started importing for this element.\n if (mounted.has(element) || pending.has(element)) return;\n\n // When the deferred fire actually runs, the cancel handle is no longer\n // needed — remove it so pendingCancels doesn't hold stale entries.\n pendingCancels.delete(element);\n\n // Lazy props parse: read and parse data-props only now that we know we\n // are actually going to mount this island. For deferred strategies\n // (media, visible, idle) this avoids JSON.parse work at boot time for\n // islands that may never hydrate (e.g. media query never matches).\n const props = readProps(element);\n\n // Mark as pending BEFORE firing the import so any concurrent\n // `mountIslands` invocation that arrives during the await window\n // is short-circuited by `scheduleMount`'s guard.\n pending.add(element);\n\n // Dynamic-import is cached by the JS runtime, so repeat hits for\n // the same URL share the resolved module — module-level\n // singletons are fine.\n //\n // We move the element from `pending` to `mounted` only on the\n // success path so a failed import (e.g. transient network blip\n // in dev) doesn't permanently block a retry of the same element.\n let started: Promise<IslandModule>;\n try {\n started = importIsland(url);\n } catch (err) {\n // Some implementations of dynamic-import wrappers can throw\n // synchronously (e.g. URL parsing errors). Treat the same as\n // an async rejection.\n pending.delete(element);\n // eslint-disable-next-line no-console\n console.error(`[zfb] failed to start dynamic import for ${url}`, err);\n return;\n }\n started.then(\n (mod) => {\n const fn = mod.mount ?? mod.default;\n if (typeof fn !== \"function\") {\n pending.delete(element);\n if (\n typeof process !== \"undefined\" &&\n process.env &&\n process.env[\"NODE_ENV\"] !== \"production\"\n ) {\n // eslint-disable-next-line no-console\n console.warn(`[zfb] island bundle at ${url} did not export mount() or default()`);\n }\n return;\n }\n // Stale-mount race guard: if the element was detached while the\n // dynamic import was in-flight (e.g. a body swap happened), skip\n // mounting — the element is no longer in the live document and\n // its useEffect listeners would never receive a cleanup call.\n if (!element.isConnected) {\n pending.delete(element);\n return;\n }\n const shouldRefreshProps = element.hasAttribute(ISLAND_REMOUNT_ATTR);\n const propsForMount = shouldRefreshProps ? readProps(element) : props;\n if (shouldRefreshProps) element.removeAttribute(ISLAND_REMOUNT_ATTR);\n const unmountThunk = mod.unmount\n ? () => mod.unmount!(element)\n : () => {\n // noop — bundle does not expose unmount\n };\n try {\n fn(propsForMount, element, mode);\n mounted.set(element, unmountThunk);\n element.setAttribute(ISLAND_MOUNTED_ATTR, \"\");\n } finally {\n pending.delete(element);\n }\n },\n (err: unknown) => {\n // Surface the error in dev so the user notices, then clear\n // both guards so a later retry (e.g. another scheduleHydrate\n // fire) can attempt the import again.\n pending.delete(element);\n mounted.delete(element);\n // eslint-disable-next-line no-console\n console.error(`[zfb] failed to load island bundle ${url}`, err);\n },\n );\n };\n\n if (mode === \"render\") {\n // SSR-skip islands ignore data-when: there is nothing to defer\n // hydration of, just an empty container we paint into. Mount\n // immediately so the user sees output.\n fire();\n return;\n }\n\n if (options.force) {\n fire();\n return;\n }\n\n const { fired, cancel } = scheduleHydrateInternal(element, when, fire);\n // Track deferred-hydration cancel handle so cancelPendingIslands() can abort\n // idle / visibility callbacks before a body swap. (W1B §12.5)\n // Only register when the scheduler did NOT fire synchronously — a synchronous\n // fire means the island is already handling its import and there is no\n // deferred callback to cancel. Registering noop after a sync fire would leave\n // a stale pendingCancels entry for an already-handled element. (#743)\n if (when && when !== \"load\" && !fired) {\n pendingCancels.set(element, cancel);\n }\n}\n\n/**\n * Run the mount step for the inline-module manifest shape used by the\n * shared-bundle path. The module is already in memory (it was imported\n * into the bundle at build time), so there is no async window to\n * coordinate around — we just call `mount` / `default` directly,\n * gated by the same `data-when` semantics as the URL path.\n */\nfunction fireInlineMount(\n element: Element,\n mod: IslandModule,\n mode: \"hydrate\" | \"render\",\n options: { force?: boolean } = {},\n): void {\n const fn = mod.mount ?? mod.default;\n if (typeof fn !== \"function\") {\n if (typeof process !== \"undefined\" && process.env && process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\"[zfb] inline island manifest entry did not export mount() or default()\");\n }\n return;\n }\n\n const fire = (): void => {\n // Re-check the guard in case `fire` is invoked from a deferred\n // scheduler (rIC/rAF/visibility) after a sibling caller already\n // mounted this element.\n if (mounted.has(element)) return;\n // When the deferred fire actually runs, the cancel handle is no longer\n // needed — remove it so pendingCancels doesn't hold stale entries.\n pendingCancels.delete(element);\n // Stale-mount race guard for deferred inline mounts: skip if the element\n // was detached (e.g. body swap) while the idle/visible callback was queued.\n if (!element.isConnected) return;\n // Lazy props parse: read and parse data-props only at mount time.\n // For deferred strategies (media, visible, idle) this avoids JSON.parse\n // work at boot time for islands that may never hydrate.\n const props = readProps(element);\n const unmountThunk = mod.unmount\n ? () => mod.unmount!(element)\n : () => {\n // noop — inline module does not expose unmount\n };\n fn(props, element, mode);\n mounted.set(element, unmountThunk);\n element.setAttribute(ISLAND_MOUNTED_ATTR, \"\");\n };\n\n if (mode === \"render\") {\n fire();\n return;\n }\n\n if (options.force) {\n fire();\n return;\n }\n\n const when = element.getAttribute(\"data-when\") ?? undefined;\n const { fired, cancel } = scheduleHydrateInternal(element, when, fire);\n // Track deferred-hydration cancel handle so cancelPendingIslands() can abort\n // idle / visibility callbacks before a body swap. (W1B §12.5)\n // Only register when the scheduler did NOT fire synchronously — a synchronous\n // fire means the island is already handling its mount and there is no\n // deferred callback to cancel. Registering noop after a sync fire would leave\n // a stale pendingCancels entry for an already-handled element. (#743)\n if (when && when !== \"load\" && !fired) {\n pendingCancels.set(element, cancel);\n }\n}\n\n/**\n * Unmount the mounted islands within `root` (default: `document.body`) that will\n * NOT survive the body swap.\n *\n * Walks `root` for `[data-zfb-island]` and `[data-zfb-island-skip-ssr]` elements,\n * looks up each element's unmount thunk in the `mounted` WeakMap, calls it (which\n * triggers `render(null, element)` for Preact or `root.unmount()` for React), and\n * removes the entry from the map so `mountNewIslands()` can re-mount later.\n *\n * Call this before `swapBodyElement(...)` so the OLD body's islands receive proper\n * framework lifecycle cleanup (useEffect teardowns, etc.) before being discarded.\n *\n * When `incomingBody` is supplied (the client-router passes the parsed incoming\n * document body), any island whose `data-zfb-transition-persist` id matches a\n * marker in that body is DELIBERATELY SKIPPED: swapBodyElement will physically\n * lift the node into the new body, so its component instance and internal state\n * must survive — unmounting it here would empty the container before the lift and\n * defeat the persist contract (issue #1389). Omit `incomingBody` (or pass null)\n * to unmount everything, the pre-#1389 behavior.\n *\n * No-op for elements not in the `mounted` map (e.g. never-mounted or already cleaned up).\n */\nexport function unmountIslands(\n root: ParentNode = document.body,\n incomingBody?: ParentNode | null,\n): void {\n const selector = \"[data-zfb-island],[data-zfb-island-skip-ssr]\";\n // Persist ids that `swapBodyElement` will physically LIFT from the old body\n // into the incoming body — an old marker survives iff the incoming body has a\n // marker with the same `data-zfb-transition-persist` id. Those DOM nodes are\n // moved, not discarded, so their component instance and internal state MUST\n // survive the swap: skip their framework unmount here or the persist contract\n // preserves nothing (port-spec §12.3.1 case (a) / issue #1389). A persisted\n // island whose props changed is skipped here too — its refreshed remount runs\n // later in mountNewIslands via the `data-zfb-island-remount` flag (see\n // `clearMountedForRemount`) swapBodyElement sets. With no incoming body (a\n // call outside a swap) nothing is preserved, so the walk is byte-identical to\n // the pre-#1389 behavior.\n const preservedPersistIds = collectPersistIds(incomingBody);\n const elements = root.querySelectorAll<HTMLElement>(selector);\n for (const el of Array.from(elements)) {\n const persistId = el.getAttribute(PERSIST_ATTR);\n if (persistId !== null && preservedPersistIds.has(persistId)) continue;\n const thunk = mounted.get(el);\n try {\n thunk?.();\n } finally {\n mounted.delete(el);\n el.removeAttribute(ISLAND_MOUNTED_ATTR);\n }\n }\n}\n\n/**\n * Collect the `data-zfb-transition-persist` ids present in the incoming body so\n * `unmountIslands` can tell which old-body islands `swapBodyElement` will lift\n * (and therefore must be left mounted). Returns an empty set when no incoming\n * body is supplied.\n */\nfunction collectPersistIds(incomingBody?: ParentNode | null): Set<string> {\n const ids = new Set<string>();\n if (!incomingBody) return ids;\n for (const el of incomingBody.querySelectorAll(`[${PERSIST_ATTR}]`)) {\n const id = el.getAttribute(PERSIST_ATTR);\n if (id !== null) ids.add(id);\n }\n return ids;\n}\n\nfunction readProps(element: Element): Record<string, unknown> {\n const raw = element.getAttribute(\"data-props\");\n if (!raw) return {};\n try {\n const parsed = JSON.parse(raw) as unknown;\n // Reject arrays explicitly: `typeof [] === \"object\"` is true but\n // an array is not a valid props bag, and passing it through would\n // mean the component receives index-keyed values where it\n // expected a record. Fall through to the empty-object default\n // instead of forwarding a malformed shape.\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n } catch {\n // fall through\n }\n return {};\n}\n\n/**\n * Indirection so tests can stub the dynamic import without intercepting\n * the global `import()`. In production this is a thin wrapper over\n * native `import(url)`.\n */\nlet importImpl: (url: string) => Promise<IslandModule> = (url) =>\n // Modern bundlers (esbuild, Vite, Rollup, webpack) preserve a plain\n // `import(<dynamic>)` call when the argument isn't a static literal,\n // so we no longer need the `new Function(...)` indirection — which\n // also failed under strict CSPs that disallow `unsafe-eval`.\n import(/* @vite-ignore */ /* webpackIgnore: true */ url) as Promise<IslandModule>;\n\nfunction importIsland(url: string): Promise<IslandModule> {\n return importImpl(url);\n}\n\n/**\n * Test-only seam. Replace the module dynamic-import with a fake.\n * Returns the previous implementation so tests can restore it.\n */\nexport function __setIslandImporterForTests(\n impl: (url: string) => Promise<IslandModule>,\n): (url: string) => Promise<IslandModule> {\n const prev = importImpl;\n importImpl = impl;\n return prev;\n}\n\n/**\n * Test-only seam. Returns whether the given element has an entry in the\n * module-private `pendingCancels` Map. Used to assert that a synchronous\n * scheduler fire does not leave a stale entry behind. (#743)\n */\nexport function __hasPendingCancelForTests(element: Element): boolean {\n return pendingCancels.has(element);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takazudo/zfb",
3
- "version": "2.10.1",
3
+ "version": "2.11.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Rust-built static-site engine for Astro and Next.js users — millisecond rebuilds, single binary. SDK with islands, content collections, pagination, and config helpers.",
@@ -74,11 +74,11 @@
74
74
  "LICENSE"
75
75
  ],
76
76
  "optionalDependencies": {
77
- "@takazudo/zfb-darwin-arm64": "2.10.1",
78
- "@takazudo/zfb-darwin-x64": "2.10.1",
79
- "@takazudo/zfb-linux-arm64-gnu": "2.10.1",
80
- "@takazudo/zfb-linux-x64-gnu": "2.10.1",
81
- "@takazudo/zfb-win32-x64-msvc": "2.10.1"
77
+ "@takazudo/zfb-darwin-arm64": "2.11.0",
78
+ "@takazudo/zfb-darwin-x64": "2.11.0",
79
+ "@takazudo/zfb-linux-arm64-gnu": "2.11.0",
80
+ "@takazudo/zfb-linux-x64-gnu": "2.11.0",
81
+ "@takazudo/zfb-win32-x64-msvc": "2.11.0"
82
82
  },
83
83
  "publishConfig": {
84
84
  "access": "public"