From c476755ba527f4546a159002c51cf750f01b0e4c Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Thu, 6 Aug 2026 15:51:08 +0200 Subject: [PATCH 1/6] fix(release-assets): stop one unbuildable page from 503ing every route A page whose import closure cannot be finalized used to push a coverage gap into the fatal set, which failed the whole release asset manifest. The renderer then had no manifest to admit browser modules against, so every module on every route returned 503 and the entire site went dead over one page nothing linked to. A production site sat like that for seven weeks: a leftover scratch page imported a URL that had since turned into a sign-in redirect, and the healthy pages went down with it. Module-level failures now degrade per route. A module that cannot be finalized is absent from `modules`, so the admission boundary still refuses it, and the routes whose closure needs it are omitted rather than published with a hole. Coverage gaps only become fatal when nothing serveable is left, so a project whose only page is broken still fails closed and keeps its previous release. Also admit the fonts the platform itself emits. `veryfront/fonts` writes a fonts.googleapis.com stylesheet and a fonts.gstatic.com preconnect into the document, but the default CSP allowed neither, so a project using the framework's own font API rendered in a fallback font with only a console error to show for it. Both origins go in the droppable baseline, not the required floor: the renderer only emits them for projects that call the component. Finally, export `useDocumentNonce` from `veryfront/ui`. Third-party providers that render their own inline script take the nonce as a prop and emit an unusable empty attribute without it; only the non-hook getter was reachable from project code before. --- src/react/components/ui/index.test.ts | 1 + src/react/components/ui/index.ts | 6 +- src/release-assets/build-executor.test.ts | 49 +++++++++++++++ src/release-assets/build-executor.ts | 40 +++++++++++- src/security/http/platform-asset-origins.ts | 26 +++++++- .../http/response/security-handler.test.ts | 61 +++++++++++++++++-- .../http/response/security-handler.ts | 12 ++-- 7 files changed, 181 insertions(+), 14 deletions(-) diff --git a/src/react/components/ui/index.test.ts b/src/react/components/ui/index.test.ts index 5ad705c6ba..3ecf137622 100644 --- a/src/react/components/ui/index.test.ts +++ b/src/react/components/ui/index.test.ts @@ -156,6 +156,7 @@ const expectedRuntimeExports = [ "useAppShell", "useColorMode", "useColorModeOptional", + "useDocumentNonce", "useToast", ]; diff --git a/src/react/components/ui/index.ts b/src/react/components/ui/index.ts index e0d3f72d8b..899abbc059 100644 --- a/src/react/components/ui/index.ts +++ b/src/react/components/ui/index.ts @@ -40,7 +40,11 @@ export { cva, cx, type VariantProps } from "./cva.ts"; export { generateTokenCSS } from "./design-tokens.ts"; export { DesignTokenStyle } from "./tokens.tsx"; -export { getDocumentNonce } from "./csp-nonce.ts"; +// `useDocumentNonce` ships alongside the getter because third-party providers +// that render their own inline script -- next-themes is the common one -- take +// the nonce as a prop and emit an unusable empty attribute without it, which +// the default CSP then blocks with nothing but a console error to show for it. +export { getDocumentNonce, useDocumentNonce } from "./csp-nonce.ts"; export { ColorModeProvider, type ColorModeProviderProps, diff --git a/src/release-assets/build-executor.test.ts b/src/release-assets/build-executor.test.ts index 3bbff45430..bdf4279473 100644 --- a/src/release-assets/build-executor.test.ts +++ b/src/release-assets/build-executor.test.ts @@ -628,6 +628,55 @@ describe("release asset build executor", () => { assertCoverageFailure(result, rec, "module-rewrite-failed:pages/index.tsx"); }); + it("publishes healthy routes when one page has an unresolvable import", async () => { + // A production outage: one leftover scratch page imported a URL that had + // since become a sign-in redirect. Its coverage gap failed the entire + // manifest, so the renderer had no manifest to admit against and 503'd + // every browser module on every route -- the whole site went dead over one + // page nothing linked to. The broken page must cost only itself. + const rec: Recorded = { began: false, uploads: [], manifest: null, states: [] }; + const client = makeClient([ + { path: "pages/index.tsx", content: "export default () => null;" }, + { path: "pages/scratch.tsx", content: 'import "./missing.ts"; export default null;' }, + ], rec); + + const result = await runReleaseAssetBuild( + baseInput(client, (source) => Promise.resolve(source)), + await tmp(), + ); + + assertEquals(result.success, true); + assertEquals(result.state, "ready"); + + const manifest = parseReleaseAssetManifest(rec.manifest); + assertExists(manifest); + // The healthy page ships and stays admissible. + assertEquals(manifest.routes["/"]?.modules, ["pages/index.tsx"]); + assertExists(manifest.modules["pages/index.tsx"]); + // The broken page ships nowhere: no route, and no manifest entry, so the + // browser-module endpoint still refuses it rather than serving a hole. + assertEquals(manifest.routes["/scratch"], undefined); + assertEquals(manifest.modules["pages/scratch.tsx"], undefined); + }); + + it("still fails closed when every page is unbuildable", async () => { + // Degrading per route must not become "publish an empty manifest". With no + // serveable route left there is nothing to ship, so the build fails and the + // previous release keeps serving. + const rec: Recorded = { began: false, uploads: [], manifest: null, states: [] }; + const client = makeClient([ + { path: "pages/index.tsx", content: 'import "./missing.ts"; export default null;' }, + { path: "pages/other.tsx", content: 'import "./gone.ts"; export default null;' }, + ], rec); + + const result = await runReleaseAssetBuild( + baseInput(client, (source) => Promise.resolve(source)), + await tmp(), + ); + + assertCoverageFailure(result, rec, "module-rewrite-failed:pages/"); + }); + it("never publishes project modules with unresolved relative imports", async () => { const rec: Recorded = { began: false, uploads: [], manifest: null, states: [] }; const client = makeClient( diff --git a/src/release-assets/build-executor.ts b/src/release-assets/build-executor.ts index 155498052f..86444edfd4 100644 --- a/src/release-assets/build-executor.ts +++ b/src/release-assets/build-executor.ts @@ -2651,13 +2651,20 @@ async function runBuildInner( ); } + // Module-level failures are held apart from the structural gaps above. A + // module that cannot be finalized costs its own routes, not the release: it + // is simply absent from `modules`, and the browser-module endpoint already + // refuses anything absent from the manifest. These only become fatal below, + // when they leave the release with no serveable route at all. + const moduleGaps: string[] = []; + const { modules, skippedModules } = await finalizeProjectModules( transformedModules, knownPaths, dependencyUrls, uploadQueue, pendingBytes, - gaps, + moduleGaps, !vendorDependencies, ); @@ -2735,6 +2742,7 @@ async function runBuildInner( // B2. Routes: walk the transformed browser import closure from each page entrypoint. // Modules missing from transformedModules are recorded as closure gaps. const routes: Record = {}; + const droppedRoutes = new Map(); const pageModules = Object.keys(modules).filter((p) => routeForConfiguredPage(p, routeDirectories) !== null ); @@ -2769,13 +2777,41 @@ async function runBuildInner( pushGap(closureGaps, `route-gap:${route}:${missing}`); } } + + // A route with a hole in its closure is omitted rather than published with + // one. Shipping it would hand the browser an import map pointing at a + // module the admission boundary refuses. if (closureGaps.length > 0) { - for (const gap of closureGaps) pushGap(gaps, gap); + droppedRoutes.set(route, closureGaps); + continue; } routes[route] = { modules: manifestedModules, css: cssHashes }; } + // Every route the release could not cover, so an operator sees which pages + // this build left unserveable even when the manifest publishes. + if (droppedRoutes.size > 0) { + logger.warn("Omitting routes with incomplete release asset coverage", { + dropped: [...droppedRoutes.keys()], + published: Object.keys(routes).length, + }); + } + + // One unbuildable page must not take the site down. Module and route gaps + // only become fatal when they leave nothing to serve -- a project whose sole + // page is broken still fails closed, while a project with one bad page among + // many publishes the rest. Before this, a single unresolvable import failed + // the whole manifest, and the renderer then 503'd every module on every + // route because manifest admission had nothing to admit against. + const hasServeableRoute = Object.keys(routes).length > 0; + if (!hasServeableRoute) { + for (const gap of moduleGaps) pushGap(gaps, gap); + for (const routeGaps of droppedRoutes.values()) { + for (const gap of routeGaps) pushGap(gaps, gap); + } + } + // A v2 manifest is publishable only when every requested module, // dependency, route closure, and stylesheet has complete immutable coverage. assertCompleteReleaseAssetCoverage(gaps); diff --git a/src/security/http/platform-asset-origins.ts b/src/security/http/platform-asset-origins.ts index 4453ecec93..3e5d877667 100644 --- a/src/security/http/platform-asset-origins.ts +++ b/src/security/http/platform-asset-origins.ts @@ -7,11 +7,19 @@ * response's own assets. Keeping them in one list means the policy and the * emitters cannot drift apart silently. * - * Project-owned external origins (analytics, Google Fonts, a customer CDN) are - * deliberately absent: those belong to the project, which declares them through + * Project-owned external origins (analytics, a customer CDN) are deliberately + * absent: those belong to the project, which declares them through * `security.csp`. Widening the default for them would grant every hosted site * an origin it never asked for. * + * Google Fonts is the one external family that is platform-emitted rather than + * project-owned, because `veryfront/fonts` (`GoogleFonts` in + * `src/react/fonts/index.ts`) writes the `fonts.googleapis.com` stylesheet and + * the `fonts.gstatic.com` preconnect into the document itself. It sits in the + * droppable baseline rather than the required floor: the renderer only emits + * those URLs for projects that call the component, so a project that does not + * use it may drop them with `security.csp: { styleSrc: null, fontSrc: null }`. + * * @module security/http/platform-asset-origins */ @@ -32,8 +40,22 @@ export const PLATFORM_IMAGE_ORIGINS = [ PLATFORM_CDN_ORIGIN, ] as const; +/** Origin serving the stylesheet `veryfront/fonts` links. */ +export const GOOGLE_FONTS_STYLESHEET_ORIGIN = "https://fonts.googleapis.com"; + +/** Origin serving the font files that stylesheet references. */ +export const GOOGLE_FONTS_FILE_ORIGIN = "https://fonts.gstatic.com"; + +/** Style origins `veryfront/fonts` emits `` tags for. */ +export const PLATFORM_FONT_STYLE_ORIGINS = [GOOGLE_FONTS_STYLESHEET_ORIGIN] as const; + +/** Font-file origins the stylesheet those tags load then fetches from. */ +export const PLATFORM_FONT_FILE_ORIGINS = [GOOGLE_FONTS_FILE_ORIGIN] as const; + /** Every platform-owned origin permitted by the default policy. */ export const PLATFORM_ASSET_ORIGINS = [ ...PLATFORM_SCRIPT_ORIGINS, ...PLATFORM_IMAGE_ORIGINS, + ...PLATFORM_FONT_STYLE_ORIGINS, + ...PLATFORM_FONT_FILE_ORIGINS, ] as const; diff --git a/src/security/http/response/security-handler.test.ts b/src/security/http/response/security-handler.test.ts index 7cfc5e0f84..64af39aa30 100644 --- a/src/security/http/response/security-handler.test.ts +++ b/src/security/http/response/security-handler.test.ts @@ -12,6 +12,8 @@ import { import type { SecurityConfig } from "./types.ts"; import { PLATFORM_ASSET_ORIGINS, + PLATFORM_FONT_FILE_ORIGINS, + PLATFORM_FONT_STYLE_ORIGINS, PLATFORM_IMAGE_ORIGINS, PLATFORM_SCRIPT_ORIGINS, } from "#veryfront/security/http/platform-asset-origins.ts"; @@ -169,6 +171,38 @@ describe("security/http/response/security-handler", () => { assert(!result.includes("style-src-elem"), "no shadowing directive is emitted"); }); + it("admits the fonts veryfront/fonts emits for a project with no config", () => { + // GoogleFonts (src/react/fonts/index.ts) writes the googleapis stylesheet + // and gstatic preconnect into the document itself, so a policy omitting + // them forbids the same response's own assets. A config-free project used + // to render in a fallback font with only a console error to show for it. + const result = buildCSP(false, "n", null); + + assertAllows( + parseDirectiveRemoteHosts(result, "style-src"), + "fonts.googleapis.com", + "the stylesheet the framework links is loadable", + ); + assertAllows( + parseDirectiveRemoteHosts(result, "font-src"), + "fonts.gstatic.com", + "the font files that stylesheet references are loadable", + ); + }); + + it("lets a project that never calls veryfront/fonts drop the font origins", () => { + // Baseline, not floor: dropping it can only affect the project's own + // content, so hardening past the default stays available. + const result = buildCSP(false, "n", { + csp: { styleSrc: null, fontSrc: null }, + }); + + assertEquals(parseDirectiveRemoteHosts(result, "style-src"), []); + assertEquals(parseDirectiveRemoteHosts(result, "font-src"), []); + assert(parseDirectiveSources(result, "style-src").includes("'self'"), "floor survives"); + assert(result.includes("'nonce-n'"), "script-src floor survives"); + }); + it("should handle camelCase and kebab-case directive keys alike", () => { const camel = buildCSP(false, "n2", { csp: { fontSrc: ["https://a.example"] } }); const kebab = buildCSP(false, "n2", { csp: { "font-src": ["https://a.example"] } }); @@ -184,9 +218,11 @@ describe("security/http/response/security-handler", () => { "font-src": ["https://b.example"], }, }); + // Sorted by the helper; fonts.gstatic.com rides along from the baseline. assertEquals(parseDirectiveRemoteHosts(result, "font-src"), [ "a.example", "b.example", + "fonts.gstatic.com", ]); }); @@ -367,12 +403,19 @@ describe("security/http/response/security-handler", () => { const permitted = new Set( PLATFORM_ASSET_ORIGINS.map((origin) => new URL(origin).hostname), ); - // Only these two carry a platform asset. Every other directive must be + // Only these carry a platform asset. Every other directive must be // exactly host-free, checked by exclusion so a directive added to the // policy later is covered here without anyone remembering to list it. // connect-src carries the script origins so the browser may fetch the - // source maps those modules reference. - const mayCarryPlatformHosts = new Set(["script-src", "img-src", "connect-src"]); + // source maps those modules reference. style-src and font-src carry the + // Google Fonts origins because `veryfront/fonts` emits those tags. + const mayCarryPlatformHosts = new Set([ + "script-src", + "img-src", + "connect-src", + "style-src", + "font-src", + ]); for ( const directive of [ @@ -407,7 +450,11 @@ describe("security/http/response/security-handler", () => { parseDirectiveSources(csp, "connect-src"), ["'self'", ...PLATFORM_SCRIPT_ORIGINS], ); - assertEquals(parseDirectiveSources(csp, "font-src"), ["'self'", "data:"]); + assertEquals(parseDirectiveSources(csp, "font-src"), [ + "'self'", + "data:", + ...PLATFORM_FONT_FILE_ORIGINS, + ]); }); it("default CSP admits the platform assets the renderer emits", () => { @@ -472,7 +519,11 @@ describe("security/http/response/security-handler", () => { !styleElemSources.some((source) => source.startsWith("'nonce-")), "style-src should not mix a nonce with unsafe-inline because browsers ignore unsafe-inline when nonce/hash sources are present", ); - assertEquals(styleElemSources, ["'self'", "'unsafe-inline'"]); + assertEquals(styleElemSources, [ + "'self'", + "'unsafe-inline'", + ...PLATFORM_FONT_STYLE_ORIGINS, + ]); assert( mediaSources.includes("blob:"), "media-src should allow blob media URLs generated by browser media pipelines", diff --git a/src/security/http/response/security-handler.ts b/src/security/http/response/security-handler.ts index 45cbf4308a..36f6d04709 100644 --- a/src/security/http/response/security-handler.ts +++ b/src/security/http/response/security-handler.ts @@ -4,6 +4,8 @@ import { HOSTED_STUDIO_ORIGINS } from "#veryfront/security/http/studio-origin-po import { isCorsPolicyResponseHeaderName } from "#veryfront/utils/cors-policy-limits.ts"; import { serverLogger } from "#veryfront/utils/logger/logger.ts"; import { + PLATFORM_FONT_FILE_ORIGINS, + PLATFORM_FONT_STYLE_ORIGINS, PLATFORM_IMAGE_ORIGINS, PLATFORM_SCRIPT_ORIGINS, } from "#veryfront/security/http/platform-asset-origins.ts"; @@ -77,8 +79,10 @@ const VERYFRONT_FRAME_ANCESTORS = ["'self'", ...HOSTED_STUDIO_ORIGINS]; * the renderer emitted `esm.sh` and no hosted page hydrated. * * The rule this encodes: the floor contains what the platform emits, never a - * guess at what a project needs. Project-specific origins (fonts, analytics, - * embeds) belong in `security.csp`, which is merged on top. + * guess at what a project needs. Project-specific origins (analytics, embeds) + * belong in `security.csp`, which is merged on top. Google Fonts is in the + * baseline rather than here because the platform does emit it -- but only via + * `veryfront/fonts`, so a project that never calls it may drop it. * * Notes on individual directives: * - script-src carries the nonce. Style directives must not: browsers ignore @@ -125,10 +129,10 @@ function requiredDirectives( * the project's own content — never the platform's. */ const BASELINE_DIRECTIVES: ReadonlyMap = new Map([ - ["style-src", ["'unsafe-inline'"]], + ["style-src", ["'unsafe-inline'", ...PLATFORM_FONT_STYLE_ORIGINS]], ["style-src-attr", ["'unsafe-inline'"]], ["img-src", ["data:"]], - ["font-src", ["data:"]], + ["font-src", ["data:", ...PLATFORM_FONT_FILE_ORIGINS]], ["media-src", ["blob:"]], ["worker-src", ["blob:"]], ]); From 20dad89a18b0c698c3fda782d3858bdc832867df Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Thu, 6 Aug 2026 16:08:49 +0200 Subject: [PATCH 2/6] fix(import-map): resolve veryfront/ui so projects can import it deno.json has exported `veryfront/ui` for as long as the barrel has existed, but neither the SSR import map nor the browser one named it. A project writing `import { getDocumentNonce } from "veryfront/ui"` got a bare specifier the browser could not resolve, and the release asset build counted the importing module as uncovered -- which, until the previous commit, failed the entire manifest. default-import-map.ts already carried the rule in a comment: a new deno.json export containing React hooks or components has to be added there too, or it never goes through the SSR transform and produces dual-React errors or "Module not found" 500s. veryfront/ui is exactly that kind of export and was missed. This is what makes `useDocumentNonce` reachable from project code at all; exporting it from the barrel in the previous commit was necessary but not sufficient. --- src/html/utils.test.ts | 4 ++++ src/html/utils.ts | 6 ++++++ src/modules/import-map/default-import-map.test.ts | 14 ++++++++++++++ src/modules/import-map/default-import-map.ts | 2 ++ 4 files changed, 26 insertions(+) diff --git a/src/html/utils.test.ts b/src/html/utils.test.ts index a1fbae386f..487ad1f7d4 100644 --- a/src/html/utils.test.ts +++ b/src/html/utils.test.ts @@ -376,6 +376,10 @@ describe("html-generation/utils", () => { assertEquals(imports["veryfront/head"], "/_vf_modules/_veryfront/react/runtime/core.js"); assertEquals(imports["veryfront/context"], "/_vf_modules/_veryfront/react/runtime/core.js"); assertEquals(imports["veryfront/fonts"], "/_vf_modules/_veryfront/react/fonts/index.js"); + assertEquals( + imports["veryfront/ui"], + "/_vf_modules/_veryfront/react/components/ui/index.js", + ); // React must come from esm.sh even under unpkg — unpkg only ships UMD // globals, which cannot be loaded through an import map, so hydration would diff --git a/src/html/utils.ts b/src/html/utils.ts index 495dca6a98..87b80f2bd9 100644 --- a/src/html/utils.ts +++ b/src/html/utils.ts @@ -87,6 +87,7 @@ const PLATFORM_UTILITY_PATHS = { router: CORE_REACT_RUNTIME_PATH, context: CORE_REACT_RUNTIME_PATH, fonts: "/_vf_modules/_veryfront/react/fonts/index.js", + ui: "/_vf_modules/_veryfront/react/components/ui/index.js", // Client-side AI/chat modules - use local module server in dev for faster iteration // NOTE: These are NOT available in compiled binaries, so we use CDN URLs there instead chat: "/_vf_modules/_veryfront/chat/index.js", @@ -101,6 +102,11 @@ const CORE_PLATFORM_UTILITIES: Record = { "veryfront/router": PLATFORM_UTILITY_PATHS.router, "veryfront/context": PLATFORM_UTILITY_PATHS.context, "veryfront/fonts": PLATFORM_UTILITY_PATHS.fonts, + // deno.json has exported `veryfront/ui` for as long as the barrel has + // existed, but no import map named it, so the bare specifier reached the + // browser unresolved and the release build counted the importing module as + // uncovered. Anything a project can import must be resolvable here. + "veryfront/ui": PLATFORM_UTILITY_PATHS.ui, "veryfront/react/head": PLATFORM_UTILITY_PATHS.head, "veryfront/react/router": PLATFORM_UTILITY_PATHS.router, "veryfront/react/context": PLATFORM_UTILITY_PATHS.context, diff --git a/src/modules/import-map/default-import-map.test.ts b/src/modules/import-map/default-import-map.test.ts index bd38ad475e..b7dafdf153 100644 --- a/src/modules/import-map/default-import-map.test.ts +++ b/src/modules/import-map/default-import-map.test.ts @@ -32,6 +32,20 @@ describe("modules/import-map/default-import-map", () => { assert("veryfront/router" in imports, "should have 'veryfront/router' mapping"); assert("veryfront/context" in imports, "should have 'veryfront/context' mapping"); assert("veryfront/fonts" in imports, "should have 'veryfront/fonts' mapping"); + assert("veryfront/ui" in imports, "should have 'veryfront/ui' mapping"); + }); + + it("maps every React-bearing deno.json export so the specifier resolves", () => { + // veryfront/ui was exported from deno.json but named in no import map, so + // a project importing it shipped a bare specifier the browser could not + // resolve -- and the release build then counted the importing module as + // uncovered, which is fatal to the whole manifest. + const imports = getImports(); + + assertEquals( + imports["veryfront/ui"], + "/_vf_modules/_veryfront/react/components/ui/index.js?ssr=true", + ); }); it("should map veryfront/react to the browser public barrel", () => { diff --git a/src/modules/import-map/default-import-map.ts b/src/modules/import-map/default-import-map.ts index 6aebe4533c..58e5bf4b99 100644 --- a/src/modules/import-map/default-import-map.ts +++ b/src/modules/import-map/default-import-map.ts @@ -18,6 +18,7 @@ function getVeryfrontSsrImportMap(): Record { const router = coreReact; const context = coreReact; const fonts = `${base}/react/fonts/index.js${ssr}`; + const ui = `${base}/react/components/ui/index.js${ssr}`; const markdown = `${base}/markdown/index.js${ssr}`; const chat = `${base}/chat/index.js${ssr}`; @@ -38,6 +39,7 @@ function getVeryfrontSsrImportMap(): Record { "veryfront/router": router, "veryfront/context": context, "veryfront/fonts": fonts, + "veryfront/ui": ui, "veryfront/markdown": markdown, "veryfront/chat": chat, "veryfront/mdx": mdx, From dedad64838c796210a14a9c652a45f4f34988995 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Thu, 6 Aug 2026 16:19:28 +0200 Subject: [PATCH 3/6] chore: regenerate prebundled runtime and dev-ui candidates deno task generate:manifests:check gates typecheck on the committed bundles matching source. Editing the import map changed the hydration runtime bundle, so the committed copy went stale. --- src/server/handlers/dev/framework-candidates.generated.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/server/handlers/dev/framework-candidates.generated.ts b/src/server/handlers/dev/framework-candidates.generated.ts index 2bbc2de3b3..0897221922 100644 --- a/src/server/handlers/dev/framework-candidates.generated.ts +++ b/src/server/handlers/dev/framework-candidates.generated.ts @@ -8610,6 +8610,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "committedScopeRef", "committedScopeRef.current", "committedScopeRef.current;", + "common", "compact", "companion", "compat", @@ -8771,6 +8772,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "consecutive", "considered", "consistent", + "console", "console.warn(", "console.warn(MISSING_MARKDOWN_RENDERER_WARNING);", "const", @@ -10305,6 +10307,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "embed", "embedded", "embedded:", + "emit", "emit**", "emits", "emitted", @@ -11160,9 +11163,9 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "getChildRef(", "getChildRef(child,", "getDerivedStateFromError(error:", - "getDocumentNonce", "getDocumentNonce():", "getDocumentNonce();", + "getDocumentNonce,", "getElementRef(", "getElementRef(child,", "getExtension(attachment.name)", @@ -13676,6 +13679,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "next)", "next);", "next,", + "next-themes", "next.", "next.arrowOffset", "next.delete(id);", @@ -18973,6 +18977,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "until", "untitled", "untouched.", + "unusable", "unversioned", "unwrapEnvelope(", "unwrapEnvelope(parsed,", From 32cb6fef385abd3b9f77f5c0ee3f1ee8c6b8f387 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Thu, 6 Aug 2026 16:29:27 +0200 Subject: [PATCH 4/6] fix(release-assets): make transform-stage failures route-local too Review catch: moduleGaps only covered failures from finalization onward. A page that failed earlier -- at transform, at the size check, or at import parsing -- still pushed a fatal gap, so an MDX or TSX compile error in one page kept taking down every other route. That is at least as common as an unresolvable import. Those three now record route-local gaps as well. Vendoring failure deliberately stays fatal: unlike the others it does not drop the module, it continues with the unvendored code, and that gap is what stops the result from being published. A structural gap still fails the build immediately, but the report now carries the module gaps collected on the way there. Without that, a release that failed on a dependency error never named the page that broke, which is the part anyone fixing it needs. Tests: a page failing to transform no longer costs its siblings; a project where every page fails to transform still fails closed. The old "fails closed when one module transform fails" case now asserts the half that still holds -- the failed module never reaches the manifest. --- src/release-assets/build-executor.test.ts | 30 ++++++++++++- src/release-assets/build-executor.ts | 54 +++++++++++++++-------- 2 files changed, 64 insertions(+), 20 deletions(-) diff --git a/src/release-assets/build-executor.test.ts b/src/release-assets/build-executor.test.ts index bdf4279473..5a0e9760ff 100644 --- a/src/release-assets/build-executor.test.ts +++ b/src/release-assets/build-executor.test.ts @@ -498,7 +498,11 @@ describe("release asset build executor", () => { ); }); - it("fails closed when one module transform fails", async () => { + it("never admits a module whose transform failed", async () => { + // Previously this failed the whole build. It no longer does -- a broken page + // costs only its own route -- but the safety half still holds: the module + // that failed must never reach the manifest, so the browser-module endpoint + // keeps refusing it. const rec: Recorded = { began: false, uploads: [], manifest: null, states: [] }; const files = [ { path: "pages/index.tsx", content: "export default () => null;" }, @@ -514,7 +518,14 @@ describe("release asset build executor", () => { const result = await runReleaseAssetBuild(baseInput(client, transform), await tmp()); - assertCoverageFailure(result, rec, "module-transform-failed:pages/broken.tsx"); + assertEquals(result.success, true); + const manifest = parseReleaseAssetManifest(rec.manifest); + assertExists(manifest); + assertEquals(manifest.modules["pages/broken.tsx"], undefined); + assertEquals(manifest.routes["/broken"], undefined); + // The healthy page is unaffected. + assertExists(manifest.modules["pages/index.tsx"]); + assertEquals(manifest.routes["/"]?.modules, ["pages/index.tsx"]); }); it("fails closed when HTTP dependency vendoring fails", async () => { @@ -659,6 +670,21 @@ describe("release asset build executor", () => { assertEquals(manifest.modules["pages/scratch.tsx"], undefined); }); + it("still fails closed when every page fails to transform", async () => { + const rec: Recorded = { began: false, uploads: [], manifest: null, states: [] }; + const client = makeClient([ + { path: "pages/index.tsx", content: "export default () => null;" }, + { path: "pages/other.tsx", content: "export default () => null;" }, + ], rec); + + const result = await runReleaseAssetBuild( + baseInput(client, () => Promise.reject(new Error("compile error"))), + await tmp(), + ); + + assertCoverageFailure(result, rec, "module-transform-failed:pages/"); + }); + it("still fails closed when every page is unbuildable", async () => { // Degrading per route must not become "publish an empty manifest". With no // serveable route left there is nothing to ship, so the build fails and the diff --git a/src/release-assets/build-executor.ts b/src/release-assets/build-executor.ts index 86444edfd4..f2071435b2 100644 --- a/src/release-assets/build-executor.ts +++ b/src/release-assets/build-executor.ts @@ -1168,10 +1168,24 @@ class IncompleteReleaseAssetBuildError extends Error { } } -function assertCompleteReleaseAssetCoverage(coverageFailures: readonly string[]): void { - if (coverageFailures.length > 0) { - throw new IncompleteReleaseAssetBuildError(coverageFailures); - } +/** + * Fail the build when any structural gap remains. + * + * `moduleGaps` never fails a build on its own -- per-module failures cost only + * their own routes. It is passed here so that when something structural does + * fail, the report still names the modules that failed on the way there. Those + * are usually the actionable part, and omitting them hid the failing page + * behind a generic dependency error. + */ +function assertCompleteReleaseAssetCoverage( + coverageFailures: readonly string[], + moduleGaps: readonly string[] = [], +): void { + if (coverageFailures.length === 0) return; + + const combined = [...coverageFailures]; + for (const gap of moduleGaps) pushGap(combined, gap); + throw new IncompleteReleaseAssetBuildError(combined); } function dependencyLookupKeys(specifier: string): Set { @@ -2416,6 +2430,12 @@ async function runBuildInner( const transformedModules = new Map(); const dependencyModules = createDependencyModuleCollection(); const gaps: string[] = []; + // Per-module failures are held apart from the structural gaps in `gaps`. A + // module that cannot be built costs its own routes, not the release: it never + // reaches `modules`, and the browser-module endpoint already refuses anything + // absent from the manifest. These are promoted into `gaps` at route assembly, + // and only when they leave the release with no serveable route at all. + const moduleGaps: string[] = []; const uploadQueue: PreparedAsset[] = []; // Bytes are held per-hash only until uploaded, then dropped (M3). const pendingBytes = createPendingAssetStore(); @@ -2477,7 +2497,7 @@ async function runBuildInner( }); } catch (error) { const sanitized = sanitizeError(error); - pushGap(gaps, `module-transform-failed:${logicalPath}`); + pushGap(moduleGaps, `module-transform-failed:${logicalPath}`); logger.warn("Module transform failed during release asset build", { path: logicalPath, error: sanitized, @@ -2485,7 +2505,7 @@ async function runBuildInner( return []; } if (typeof code !== "string") { - pushGap(gaps, `module-transform-failed:${logicalPath}`); + pushGap(moduleGaps, `module-transform-failed:${logicalPath}`); logger.warn("Module transform returned a non-string result", { path: logicalPath, }); @@ -2493,7 +2513,7 @@ async function runBuildInner( } const transformedSize = textEncoder.encode(code).byteLength; if (transformedSize > RELEASE_ASSET_MAX_SIZE_BYTES) { - pushGap(gaps, `oversized:${logicalPath}`); + pushGap(moduleGaps, `oversized:${logicalPath}`); logger.warn("Module transform output exceeds the release asset limit", { path: logicalPath, size: transformedSize, @@ -2523,6 +2543,11 @@ async function runBuildInner( imports = vendoredImports; } catch (error) { const sanitized = sanitizeError(error); + // Deliberately fatal rather than route-local, unlike the other + // per-module failures. This path does not drop the module: it keeps + // going with the *unvendored* code, and it is this gap that stops that + // result from being published. Degrading it to a route-local gap would + // let a module whose dependencies were never vendored reach a manifest. pushGap(gaps, `module-dependency-vendor-failed:${logicalPath}`); logger.warn("HTTP dependency vendoring failed during release asset build", { path: logicalPath, @@ -2537,7 +2562,7 @@ async function runBuildInner( imports = await collectProjectModuleImports(code, logicalPath, knownPaths); } catch (error) { const sanitized = sanitizeError(error); - pushGap(gaps, `module-import-parse-failed:${logicalPath}`); + pushGap(moduleGaps, `module-import-parse-failed:${logicalPath}`); logger.warn("Module import parse failed during release asset build", { path: logicalPath, error: sanitized, @@ -2651,13 +2676,6 @@ async function runBuildInner( ); } - // Module-level failures are held apart from the structural gaps above. A - // module that cannot be finalized costs its own routes, not the release: it - // is simply absent from `modules`, and the browser-module endpoint already - // refuses anything absent from the manifest. These only become fatal below, - // when they leave the release with no serveable route at all. - const moduleGaps: string[] = []; - const { modules, skippedModules } = await finalizeProjectModules( transformedModules, knownPaths, @@ -2692,10 +2710,10 @@ async function runBuildInner( const resolvedStylesheet = resolveProjectStylesheet(sourceByPath, stylesheetPath); if (stylesheetPath !== undefined && resolvedStylesheet === undefined) { pushGap(gaps, `stylesheet-missing:${stylesheetPath}`); - assertCompleteReleaseAssetCoverage(gaps); + assertCompleteReleaseAssetCoverage(gaps, moduleGaps); } const stylesheet = await mergeModuleCssImports(sourceByPath, resolvedStylesheet); - assertCompleteReleaseAssetCoverage(gaps); + assertCompleteReleaseAssetCoverage(gaps, moduleGaps); const cssRequested = candidates.size > 0 || stylesheet !== undefined; if (cssRequested) { const stylesheetBytes = stylesheet ? textEncoder.encode(stylesheet).byteLength : 0; @@ -2814,7 +2832,7 @@ async function runBuildInner( // A v2 manifest is publishable only when every requested module, // dependency, route closure, and stylesheet has complete immutable coverage. - assertCompleteReleaseAssetCoverage(gaps); + assertCompleteReleaseAssetCoverage(gaps, moduleGaps); // Upload only after coverage is proven complete, so failed builds do not // leave unreferenced immutable assets behind. From 2fa826c516e872241c941e8e0619306e8488b252 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Thu, 6 Aug 2026 16:41:24 +0200 Subject: [PATCH 5/6] docs: update the CSP guide and API reference for the new font defaults The security-headers guide was built around "here is how to allow Google Fonts", which is exactly what the default policy now does for you. Its own test asserts the documented policy against what buildCSP emits, so it failed the moment the default changed -- which is the test working. The guide now documents the fonts as part of the default, points at the null opt-out for projects that never call veryfront/fonts, and keeps a worked example for a third-party font service, where both the stylesheet origin and the font-file origin are still needed. Regenerates docs/api-reference for the newly exported useDocumentNonce. --- docs/api-reference/veryfront/security.md | 6 ++--- docs/api-reference/veryfront/ui.md | 1 + docs/guides/security-headers.md | 34 ++++++++++++++++-------- tests/docs/guide-code-examples.test.ts | 32 ++++++++++++++++------ 4 files changed, 51 insertions(+), 22 deletions(-) diff --git a/docs/api-reference/veryfront/security.md b/docs/api-reference/veryfront/security.md index 4d607e9a1f..86825ef1f3 100644 --- a/docs/api-reference/veryfront/security.md +++ b/docs/api-reference/veryfront/security.md @@ -55,7 +55,7 @@ applySecurityHeaders(response.headers, false, generateNonce(), null); | `applyCORSHeaders` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/cors/headers.ts#L86) | | `applyCORSHeadersSync` | Apply CORS synchronously. Promise-returning values still fail closed at runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/cors/headers.ts#L112) | | `applyCsrfCookie` | Set CSRF cookie on GET/HEAD responses when not already present. Uses httpOnly: false so client JS can read the cookie for double-submit. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/csrf/helpers.ts#L150) | -| `applySecurityHeaders` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/response/security-handler.ts#L262) | +| `applySecurityHeaders` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/response/security-handler.ts#L266) | | `buildCacheControl` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/response/cache-handler.ts#L86) | | `cors` | Create CORS middleware. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/cors/middleware.ts#L10) | | `corsSimple` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/cors/middleware.ts#L39) | @@ -65,8 +65,8 @@ applySecurityHeaders(response.headers, false, generateNonce(), null); | `createValidationError` | Create an input validation error. Convenience wrapper around INPUT_VALIDATION_FAILED.create(). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/input-validation/errors.ts#L12) | | `createValidator` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/path-validation/index.ts#L446) | | `generateCsrfToken` | Generate a CSRF token and return value + Set-Cookie header string | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/csrf/helpers.ts#L70) | -| `generateNonce` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/response/security-handler.ts#L49) | -| `getSecurityHeader` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/response/security-handler.ts#L249) | +| `generateNonce` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/response/security-handler.ts#L51) | +| `getSecurityHeader` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/response/security-handler.ts#L253) | | `handleCORSPreflight` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/cors/preflight.ts#L126) | | `isPreflightRequest` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/cors/preflight.ts#L186) | | `isRequestBodyTooLargeError` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/input-validation/limits.ts#L100) | diff --git a/docs/api-reference/veryfront/ui.md b/docs/api-reference/veryfront/ui.md index 312ec572c8..59c7ea1571 100644 --- a/docs/api-reference/veryfront/ui.md +++ b/docs/api-reference/veryfront/ui.md @@ -197,6 +197,7 @@ export default function App({ children }: { children: React.ReactNode }) { | `getFileTypeLabel` | Human label for a file extension, falling back to the media type. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/file-type.tsx#L233) | | `useAdapter` | Resolve the active UI-primitive adapter. Never returns null (defaults to builtin). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/adapter/context.tsx#L53) | | `useColorModeOptional` | Non-throwing variant - returns `null` when there is no `ColorModeProvider`. Use for components that should render standalone (e.g. a `CodeBlock` dropped into markdown) and fall back to light mode. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/color-mode.tsx#L143) | +| `useDocumentNonce` | Read the nonce from the Suspense-safe server provider or the browser DOM. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/csp-nonce.ts#L23) | | `useToast` | Returns `{ toast, dismiss }`. Call `toast(options)` to enqueue (returns the new id), `toast.custom((id) => node)` for a fully custom toast, and `dismiss(id)` to remove one early. Must be used within a `ToastProvider`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/toast.tsx#L136) | ### Types diff --git a/docs/guides/security-headers.md b/docs/guides/security-headers.md index 399e564ceb..cbff875166 100644 --- a/docs/guides/security-headers.md +++ b/docs/guides/security-headers.md @@ -1,6 +1,6 @@ --- title: "Security headers and CSP" -description: "Veryfront applies a Content-Security-Policy by default. Use this guide to allow Google Fonts, analytics, and other third-party origins." +description: "Veryfront applies a Content-Security-Policy by default. Use this guide to allow analytics, embeds, and other third-party origins your site needs." order: 11 --- @@ -13,10 +13,10 @@ In production, Veryfront serves this policy: ```http default-src 'self'; script-src 'self' 'nonce-' https://esm.sh; -style-src 'self' 'unsafe-inline'; +style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; style-src-attr 'unsafe-inline'; img-src 'self' https://images.veryfront.com https://cdn.veryfront.com data:; -font-src 'self' data:; +font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://esm.sh; media-src 'self' blob:; worker-src 'self' blob:; @@ -31,9 +31,10 @@ Alongside it: `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Refer Development serves no CSP at all, so HMR and dev tooling are never blocked and a local allowance can never widen your production policy. -Two directives are worth understanding: +Three directives are worth understanding: - **`script-src` includes `https://esm.sh`** because the renderer writes React imports from that CDN into every document. A fresh nonce is generated per response for the framework's own inline bootstrap. +- **`style-src` and `font-src` include the Google Fonts origins** because `veryfront/fonts` writes those tags into the document itself. Google Fonts therefore works with no configuration. If your project never uses it, see [Tightening the policy](#tightening-the-policy). - **`frame-ancestors`** is `'none'` on your own domain. On `*.veryfront.com` addresses it instead allows the Studio origins, so the Studio preview iframe works. ## Adding an origin @@ -44,25 +45,36 @@ Set `security.csp` in `veryfront.config.ts`. Values are **added to** the default export default { security: { csp: { - styleSrc: ["https://fonts.googleapis.com"], - fontSrc: ["https://fonts.gstatic.com"], + // An analytics endpoint your client code posts to + connectSrc: ["https://analytics.example.com"], }, }, }; ``` -That is the complete Google Fonts setup: `fonts.googleapis.com` serves the stylesheet, `fonts.gstatic.com` serves the font files, and both directives keep everything they already had. +`connect-src` keeps everything it already had and gains your origin. Directive names may be camelCase (`fontSrc`) or the CSP spelling (`font-src`). Both work; camelCase matches the rest of your config. You do not need to repeat `'self'`; it is already there. +A font service other than Google's needs both halves, the stylesheet origin and the font-file origin: + +```ts +export default { + security: { + csp: { + styleSrc: ["https://use.typekit.net"], + fontSrc: ["https://use.typekit.net"], + }, + }, +}; +``` + A few more examples: ```ts export default { security: { csp: { - // An analytics endpoint your client code posts to - connectSrc: ["https://analytics.example.com"], // Embedding YouTube frameSrc: ["https://www.youtube.com"], // Images from your own CDN @@ -88,7 +100,7 @@ To remove the platform's optional sources for one directive, set it to `null`: export default { security: { csp: { - // Serve no inline styles. Keeps 'self', drops 'unsafe-inline'. + // Keeps 'self'. Drops 'unsafe-inline' and the Google Fonts origin. styleSrc: null, }, }, @@ -97,7 +109,7 @@ export default { `null` removes the optional half of a directive and keeps the required half. It cannot lock you out of your own site. -Before doing this, check what your components actually need. `'unsafe-inline'` is in the default `style-src` because many React component libraries, including Veryfront's own, create styles at runtime. Removing it is safe only if you are certain yours do not. +Before doing this, check what your components actually need. `'unsafe-inline'` is in the default `style-src` because many React component libraries, including Veryfront's own, create styles at runtime. Removing it is safe only if you are certain yours do not. The same setting drops the Google Fonts stylesheet origin, so only reach for it if your project does not use `veryfront/fonts`. ## Replacing the policy entirely diff --git a/tests/docs/guide-code-examples.test.ts b/tests/docs/guide-code-examples.test.ts index a3083eb605..63f23bbfaf 100644 --- a/tests/docs/guide-code-examples.test.ts +++ b/tests/docs/guide-code-examples.test.ts @@ -308,25 +308,41 @@ describe("Guide: security-headers.md", () => { } }); - it("documents a Google Fonts config that actually admits the fonts", async () => { + it("documents that Google Fonts needs no config, and it actually does not", async () => { const guide = await readGuide("security-headers.md"); - assertStringIncludes(guide, 'styleSrc: ["https://fonts.googleapis.com"]'); - assertStringIncludes(guide, 'fontSrc: ["https://fonts.gstatic.com"]'); + assertStringIncludes(guide, "Google Fonts therefore works with no configuration"); + + // The claim the guide now makes: a project that configures nothing can + // still load what `veryfront/fonts` emits. + const csp = buildCSP(false, "n", null); + const directive = (name: string) => + csp.split("; ").find((part) => part.startsWith(`${name} `)) ?? ""; + + assertStringIncludes(directive("style-src"), "https://fonts.googleapis.com"); + assertStringIncludes(directive("font-src"), "https://fonts.gstatic.com"); + assertStringIncludes(directive("script-src"), "'nonce-n'"); + assert(!csp.includes("style-src-elem"), "no directive shadows the documented style-src"); + }); + + it("documents a third-party font service addition that actually admits it", async () => { + const guide = await readGuide("security-headers.md"); + assertStringIncludes(guide, 'styleSrc: ["https://use.typekit.net"]'); + assertStringIncludes(guide, 'fontSrc: ["https://use.typekit.net"]'); const csp = buildCSP(false, "n", { csp: { - styleSrc: ["https://fonts.googleapis.com"], - fontSrc: ["https://fonts.gstatic.com"], + styleSrc: ["https://use.typekit.net"], + fontSrc: ["https://use.typekit.net"], }, }); const directive = (name: string) => csp.split("; ").find((part) => part.startsWith(`${name} `)) ?? ""; - assertStringIncludes(directive("style-src"), "https://fonts.googleapis.com"); - assertStringIncludes(directive("font-src"), "https://fonts.gstatic.com"); + assertStringIncludes(directive("style-src"), "https://use.typekit.net"); + assertStringIncludes(directive("font-src"), "https://use.typekit.net"); // The guide promises the floor survives an addition. + assertStringIncludes(directive("style-src"), "https://fonts.googleapis.com"); assertStringIncludes(directive("script-src"), "'nonce-n'"); - assert(!csp.includes("style-src-elem"), "no directive shadows the documented style-src"); }); it("documents a null opt-out that keeps the required sources", async () => { From c79a11e51d3d20351242709e667174003ddd7b94 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Thu, 6 Aug 2026 16:46:17 +0200 Subject: [PATCH 6/6] chore: bump to 0.1.1209 main released 0.1.1208 (#3430) while this branch was open, so the bump this PR carried collided with a published version. --- deno.json | 2 +- .../hydration-script-builder/hydration-runtime.generated.ts | 2 +- src/utils/version-constant.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deno.json b/deno.json index c4c38c9609..c72b2a8e37 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "veryfront", - "version": "0.1.1208", + "version": "0.1.1209", "license": "Apache-2.0", "nodeModulesDir": "auto", "minimumDependencyAge": { diff --git a/src/html/hydration-script-builder/hydration-runtime.generated.ts b/src/html/hydration-script-builder/hydration-runtime.generated.ts index f5b0fa1e27..c5ac70d78c 100644 --- a/src/html/hydration-script-builder/hydration-runtime.generated.ts +++ b/src/html/hydration-script-builder/hydration-runtime.generated.ts @@ -8,4 +8,4 @@ */ export const HYDRATION_RUNTIME_BUNDLE: string = - '// src/html/hydration-script-builder/runtime/main.ts\nimport * as React from "react";\nimport { createRoot } from "react-dom/client";\nimport { RouterProvider, useRouter as useRouterFromModule } from "veryfront/router";\nimport * as RouterRuntime from "veryfront/router";\nimport { PageContextProvider } from "veryfront/context";\n\n// src/routing/flatten-route-params.ts\nfunction flattenRouteParams(params) {\n if (!params) return {};\n const flat = {};\n for (const [key, value] of Object.entries(params)) {\n if (value === void 0) continue;\n flat[key] = Array.isArray(value) ? value.join("/") : value;\n }\n return flat;\n}\n\n// src/html/hydration-script-builder/runtime/shared.ts\nfunction moduleServerUrl(window) {\n return window.location.origin + "/_vf_modules";\n}\nfunction createLogging(window) {\n const DEBUG = Boolean(\n window.__VERYFRONT_DEBUG__ || new URLSearchParams(window.location.search).has("vf_debug")\n );\n const log = DEBUG ? console.log.bind(console, "[Veryfront]") : () => {\n };\n const logError = console.error.bind(console, "[Veryfront]");\n function logBackgroundFetchFailure(reason, path, error) {\n const message = error?.message ?? String(error);\n log(reason + " failed:", path, message);\n }\n const perfTimers = /* @__PURE__ */ new Map();\n const perfStart = DEBUG ? (label) => {\n perfTimers.set(label, performance.now());\n } : () => {\n };\n const perfEnd = DEBUG ? (label) => {\n const start = perfTimers.get(label);\n if (start === void 0) return 0;\n const duration = performance.now() - start;\n perfTimers.delete(label);\n console.log(\n "[Veryfront Perf] %c" + label + ": %c" + duration.toFixed(2) + "ms",\n "color: #888",\n duration > 100 ? "color: #f00; font-weight: bold" : "color: #0a0"\n );\n return duration;\n } : () => 0;\n return { DEBUG, log, logError, logBackgroundFetchFailure, perfStart, perfEnd };\n}\nfunction isAbortError(error) {\n return error?.name === "AbortError";\n}\nfunction resolveDocumentNavigationUrl(target, origin) {\n try {\n const url = new URL(target, origin);\n if (url.protocol === "http:" || url.protocol === "https:") return url.href;\n } catch (_) {\n }\n return null;\n}\nfunction getDocumentNonce(document2) {\n const element = document2.querySelector("script[nonce], style[nonce], link[nonce]");\n if (!element) return void 0;\n return element.nonce || element.getAttribute("nonce") || void 0;\n}\n\n// src/html/hydration-data-element.ts\nvar HYDRATION_DATA_ELEMENT_ID = "veryfront-hydration-data";\nfunction findServerHydrationDataElement(document2) {\n try {\n const matches = [...document2.querySelectorAll(`[id="${HYDRATION_DATA_ELEMENT_ID}"]`)];\n if (matches.length !== 1) return null;\n const body = document2.body;\n if (!body) return null;\n const element = matches[0];\n if (body.firstElementChild !== element && element.parentElement !== body) return null;\n if (element.tagName?.toLowerCase() !== "script") return null;\n if (element.getAttribute("type")?.trim().toLowerCase() !== "application/json") return null;\n return element;\n } catch {\n return null;\n }\n}\n\n// src/html/hydration-script-builder/runtime/hydration-data.ts\nfunction readInitialHydrationData(document2) {\n try {\n const element = findServerHydrationDataElement(document2);\n return JSON.parse(element && element.textContent ? element.textContent : "{}") || {};\n } catch (_) {\n return {};\n }\n}\nfunction readDocumentDependencyPinningCacheKey(initialHydrationData2) {\n return typeof initialHydrationData2.dependencyPinningCacheKey === "string" && initialHydrationData2.dependencyPinningCacheKey.startsWith("on:") ? initialHydrationData2.dependencyPinningCacheKey : null;\n}\n\n// src/html/hydration-script-builder/runtime/snapshot-modules.ts\nvar RECOVERY_STATE_KEY = "__VF_DEPENDENCY_SNAPSHOT_RECOVERY_STARTED__";\nasync function isDependencySnapshotConflictResponse(response) {\n if (!response || response.status !== 409) return false;\n try {\n const clone = response.clone?.() ?? response;\n const body = (await clone.text?.() ?? "").trim();\n return body === "Unknown dependency snapshot" || body === "export default null; // Unknown dependency snapshot";\n } catch (_) {\n return false;\n }\n}\nfunction createSnapshotModuleImporter(deps) {\n async function recoverFromSnapshotBoundModuleFailure(moduleUrl, allowDocumentReload = true) {\n try {\n const parsedUrl = new URL(moduleUrl, "http://veryfront.local");\n const snapshotKeys = parsedUrl.searchParams.getAll("pins");\n const pathMatch = parsedUrl.pathname.match(\n /^\\/_vf_modules\\/_pins\\/([^/]+)(?:\\/|$)/\n );\n if (pathMatch) {\n try {\n snapshotKeys.push(decodeURIComponent(pathMatch[1]));\n } catch (_) {\n return false;\n }\n }\n if (snapshotKeys.length !== 1 || !/^on:[A-Za-z0-9._-]+$/.test(snapshotKeys[0])) return false;\n const response = await deps.fetchModule(moduleUrl, { cache: "no-store" });\n if (!await isDependencySnapshotConflictResponse(response)) return false;\n if (!allowDocumentReload) return true;\n if (deps.recoveryState[RECOVERY_STATE_KEY] === true) return true;\n deps.recoveryState[RECOVERY_STATE_KEY] = true;\n try {\n deps.reloadDocument();\n } catch (_) {\n delete deps.recoveryState[RECOVERY_STATE_KEY];\n return false;\n }\n return true;\n } catch (_) {\n return false;\n }\n }\n async function importSnapshotBoundModule(moduleUrl, allowDocumentReload = true) {\n try {\n return await deps.importModule(moduleUrl);\n } catch (error) {\n const snapshotConflict = await recoverFromSnapshotBoundModuleFailure(\n moduleUrl,\n allowDocumentReload\n );\n if (snapshotConflict && !allowDocumentReload) {\n const conflictError = new Error(\n "Dependency snapshot is unavailable during speculative module prefetch"\n );\n conflictError.name = "DependencySnapshotConflictError";\n conflictError.dependencySnapshotConflict = true;\n conflictError.cause = error;\n throw conflictError;\n }\n throw error;\n }\n }\n return { importSnapshotBoundModule, recoverFromSnapshotBoundModuleFailure };\n}\nfunction isDependencySnapshotConflict(error) {\n return Boolean(error?.dependencySnapshotConflict);\n}\n\n// src/utils/version-constant.ts\nvar VERSION = "0.1.1208";\n\n// src/html/hydration-script-builder/runtime/module-urls.ts\nfunction appendQueryParam(url, key, value) {\n return url + (url.includes("?") ? "&" : "?") + key + "=" + value;\n}\nfunction appendDependencyPinningVersion(url, moduleData) {\n const pinKey = moduleData && moduleData.dependencyPinningCacheKey;\n if (typeof pinKey !== "string" || !pinKey.startsWith("on:")) return url;\n const hashIndex = url.indexOf("#");\n const hash = hashIndex >= 0 ? url.slice(hashIndex) : "";\n const withoutHash = hashIndex >= 0 ? url.slice(0, hashIndex) : url;\n const queryIndex = withoutHash.indexOf("?");\n const base = queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash;\n const params = new URLSearchParams(queryIndex >= 0 ? withoutHash.slice(queryIndex + 1) : "");\n const modulePrefix = "/_vf_modules/";\n const prefixIndex = base.indexOf(modulePrefix);\n const origin = prefixIndex >= 0 ? base.slice(0, prefixIndex) : "";\n if (prefixIndex >= 0 && (origin === "" || /^https?:\\/\\/[^/]+$/i.test(origin))) {\n const pathStart = prefixIndex + modulePrefix.length;\n let modulePath = base.slice(pathStart);\n if (modulePath.startsWith("_pins/")) {\n const existingKeyEnd = modulePath.indexOf("/", "_pins/".length);\n const encodedExistingKey = existingKeyEnd < 0 ? modulePath.slice("_pins/".length) : modulePath.slice("_pins/".length, existingKeyEnd);\n let existingKey;\n try {\n existingKey = decodeURIComponent(encodedExistingKey);\n } catch {\n existingKey = void 0;\n }\n if (existingKey && /^on:[A-Za-z0-9._-]+$/.test(existingKey)) {\n if (existingKeyEnd < 0) return url;\n modulePath = modulePath.slice(existingKeyEnd + 1);\n }\n }\n params.delete("pins");\n const query = params.toString();\n return base.slice(0, pathStart) + "_pins/" + encodeURIComponent(pinKey) + "/" + modulePath + (query ? "?" + query : "") + hash;\n }\n params.set("pins", pinKey);\n return base + "?" + params.toString() + hash;\n}\nfunction componentCacheKey(path, moduleData) {\n const pinKey = moduleData && moduleData.dependencyPinningCacheKey;\n return typeof pinKey === "string" && pinKey.startsWith("on:") ? path + "|vf_pins|" + pinKey : path;\n}\nfunction normalizeReleaseAssetModulePath(path) {\n return String(path || "").replace(/^\\/?_vf_modules\\//, "").replace(/^\\/+/, "").replace(/[?#].*$/, "");\n}\nfunction buildPinnedRscModuleUrl(path, moduleData) {\n let moduleUrl = "/_veryfront/rsc/module?rel=" + encodeURIComponent(path);\n const pinKey = moduleData && moduleData.dependencyPinningCacheKey;\n if (typeof pinKey === "string" && pinKey.startsWith("on:")) {\n moduleUrl += "&pins=" + encodeURIComponent(pinKey);\n }\n return moduleUrl;\n}\nfunction buildPageDataEndpoint(path, origin) {\n const targetUrl = new URL(path, origin);\n const normalizedPath = targetUrl.pathname === "/" ? "" : targetUrl.pathname.replace(/^\\//, "");\n const endpointUrl = new URL(\n "/_veryfront/page-data/" + normalizedPath + ".json",\n origin\n );\n endpointUrl.search = targetUrl.search;\n return endpointUrl.pathname + endpointUrl.search;\n}\nfunction pageDataCacheIdentity(path, documentDependencyPinningCacheKey2) {\n return documentDependencyPinningCacheKey2 ? documentDependencyPinningCacheKey2 + "|path:" + path : path;\n}\nfunction assertPageDataMatchesDocumentSnapshot(path, data, documentDependencyPinningCacheKey2) {\n if (!documentDependencyPinningCacheKey2) return data;\n if (data && data.dependencyPinningCacheKey === documentDependencyPinningCacheKey2) {\n return data;\n }\n const error = new Error("Page data dependency snapshot does not match the document");\n error.status = 409;\n error.dependencySnapshotMismatch = true;\n error.path = path;\n throw error;\n}\n\n// src/html/hydration-script-builder/runtime/component-loader.ts\nvar VERYFRONT_RUNTIME_VERSION = VERSION;\nfunction createComponentLoader(deps) {\n const { window, moduleServerUrl: moduleServerUrl2 } = deps;\n const { DEBUG, log, logError } = deps.logging;\n const componentCache = /* @__PURE__ */ new Map();\n const loadingPromises = /* @__PURE__ */ new Map();\n let releaseId = null;\n let releaseAssetModules = null;\n let studioEmbed = false;\n let hmrRefreshTimestamp = null;\n function clearComponentCache(path) {\n if (!path) {\n componentCache.clear();\n loadingPromises.clear();\n log("Cleared all component caches");\n return;\n }\n for (const key of componentCache.keys()) {\n if (key === path || key.startsWith(path + "|vf_pins|")) {\n componentCache.delete(key);\n }\n }\n for (const key of loadingPromises.keys()) {\n if (key === path || key.startsWith(path + "|vf_pins|")) {\n loadingPromises.delete(key);\n }\n }\n log("Cleared component cache for:", path);\n }\n function setReleaseId(value) {\n releaseId = typeof value === "string" && value ? value : null;\n window.__veryfrontReleaseId = releaseId;\n }\n function appendReleaseModuleVersion(url) {\n if (!releaseId || url.includes("vf_release=")) return url;\n let versionedUrl = appendQueryParam(url, "vf_release", encodeURIComponent(releaseId));\n versionedUrl = appendQueryParam(\n versionedUrl,\n "vf_runtime",\n encodeURIComponent(VERYFRONT_RUNTIME_VERSION)\n );\n return versionedUrl;\n }\n function setReleaseAssetModules(value) {\n releaseAssetModules = value && typeof value === "object" && !Array.isArray(value) ? value : null;\n window.__veryfrontReleaseAssetModules = releaseAssetModules;\n }\n function resolveReleaseAssetModuleUrl(path) {\n if (!releaseAssetModules || studioEmbed || hmrRefreshTimestamp) return null;\n const key = normalizeReleaseAssetModulePath(path);\n if (releaseAssetModules[key]) return releaseAssetModules[key];\n const withoutExt = key.replace(/\\.(tsx|ts|jsx|mdx|js|mjs)$/, "");\n const extensions = [".tsx", ".ts", ".jsx", ".mdx", ".js"];\n for (const ext of extensions) {\n const candidate = withoutExt + ext;\n if (releaseAssetModules[candidate]) return releaseAssetModules[candidate];\n }\n return null;\n }\n function pathToModuleUrl(path, embedInStudio, moduleData) {\n const releaseAssetUrl = resolveReleaseAssetModuleUrl(path);\n if (releaseAssetUrl) return releaseAssetUrl;\n const pattern = /(pages|components|app|lib|layouts|shared|features)\\/(.+)\\.(tsx|ts|jsx|mdx)$/;\n const match = path.match(new RegExp("/" + pattern.source)) || path.match(new RegExp("^" + pattern.source));\n let url;\n if (match) {\n url = moduleServerUrl2 + "/" + match[1] + "/" + match[2] + ".js";\n } else {\n const hasKnownExt = /\\.(tsx|ts|jsx|mdx|js|mjs)$/.test(path);\n url = moduleServerUrl2 + "/" + (hasKnownExt ? path.replace(/\\.(tsx|ts|jsx|mdx)$/, ".js") : path + ".js");\n }\n if (embedInStudio) url = appendQueryParam(url, "studio_embed", "true");\n if (hmrRefreshTimestamp) url = appendQueryParam(url, "t", hmrRefreshTimestamp);\n if (!embedInStudio && !hmrRefreshTimestamp) url = appendReleaseModuleVersion(url);\n url = appendDependencyPinningVersion(url, moduleData);\n return url;\n }\n function setStudioEmbed(value) {\n studioEmbed = value;\n window.__veryfrontStudioEmbed = value;\n }\n function setHMRRefreshTimestamp(timestamp) {\n hmrRefreshTimestamp = timestamp;\n window.__veryfrontHMRRefreshTimestamp = timestamp;\n }\n async function loadComponent(path, moduleData, options = {}) {\n if (!path) return null;\n const cacheKey = componentCacheKey(path, moduleData);\n if (componentCache.has(cacheKey)) {\n log("Component cached:", path);\n return componentCache.get(cacheKey);\n }\n const existingPromise = loadingPromises.get(cacheKey);\n if (existingPromise) return existingPromise;\n const loadPromise = (async () => {\n try {\n const moduleUrl = pathToModuleUrl(path, studioEmbed, moduleData);\n const start = DEBUG ? performance.now() : 0;\n log("Loading component:", moduleUrl);\n const module = await deps.snapshotModules.importSnapshotBoundModule(\n moduleUrl,\n options.allowDocumentReload !== false\n );\n const component = module.MDXLayout || module.MainLayout || module.default || module;\n if (DEBUG) {\n const duration = performance.now() - start;\n console.log(\n "[Veryfront Perf] %cimport:" + path.split("/").pop() + ": %c" + duration.toFixed(2) + "ms",\n "color: #888",\n duration > 50 ? "color: #f00; font-weight: bold" : "color: #0a0"\n );\n }\n componentCache.set(cacheKey, component);\n return component;\n } catch (error) {\n if (isDependencySnapshotConflict(error)) throw error;\n logError("Failed to load component:", path, error);\n return null;\n } finally {\n loadingPromises.delete(cacheKey);\n }\n })();\n loadingPromises.set(cacheKey, loadPromise);\n return loadPromise;\n }\n return {\n loadComponent,\n pathToModuleUrl,\n clearComponentCache,\n setStudioEmbed,\n setReleaseId,\n setReleaseAssetModules,\n setHMRRefreshTimestamp\n };\n}\n\n// src/html/hydration-script-builder/runtime/route-timing.ts\nvar MAX_ROUTE_TIMINGS = 100;\nvar MAX_SERVER_TIMING_LENGTH = 1024;\nfunction routeTimingNow() {\n return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();\n}\nfunction sanitizeServerTimingMetricName(name) {\n return String(name || "").trim().replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 128);\n}\nfunction sanitizeServerTimingHeader(value) {\n if (!value) return null;\n const metrics = [];\n const printable = String(value).replace(/[^\\x20-\\x7E]/g, " ").trim();\n if (!printable) return null;\n for (const item of printable.split(",")) {\n const segments = item.split(";").map((segment) => segment.trim()).filter(Boolean);\n const name = sanitizeServerTimingMetricName(segments[0]);\n if (!name) continue;\n for (const segment of segments.slice(1)) {\n const [key, rawValue = ""] = segment.split("=");\n if ((key ?? "").trim().toLowerCase() !== "dur") continue;\n const duration = Number(rawValue.trim().replace(/^"|"$/g, ""));\n if (!Number.isFinite(duration) || duration < 0) continue;\n metrics.push(name + ";dur=" + (Math.round(duration * 100) / 100).toFixed(2));\n break;\n }\n }\n const sanitized = metrics.join(", ");\n return sanitized ? sanitized.slice(0, MAX_SERVER_TIMING_LENGTH) : null;\n}\nfunction parseServerTimingMetrics(value) {\n const header = sanitizeServerTimingHeader(value);\n if (!header) return null;\n const metrics = {};\n for (const item of header.split(",")) {\n const segments = item.split(";").map((segment) => segment.trim()).filter(Boolean);\n const name = sanitizeServerTimingMetricName(segments[0]);\n if (!name) continue;\n for (const segment of segments.slice(1)) {\n const [key, rawValue = ""] = segment.split("=");\n if ((key ?? "").trim().toLowerCase() !== "dur") continue;\n const duration = Number(rawValue.trim().replace(/^"|"$/g, ""));\n if (Number.isFinite(duration) && duration >= 0) {\n metrics[name] = Math.round(duration * 100) / 100;\n }\n }\n }\n return Object.keys(metrics).length ? metrics : null;\n}\nfunction readResponseServerTiming(response) {\n try {\n return sanitizeServerTimingHeader(response.headers?.get("server-timing"));\n } catch (_) {\n return null;\n }\n}\nfunction roundRouteTimingValue(value) {\n return Math.round(value * 100) / 100;\n}\nfunction extractResourceTiming(entry) {\n const fields = [\n "startTime",\n "requestStart",\n "responseStart",\n "responseEnd",\n "duration",\n "transferSize",\n "encodedBodySize",\n "decodedBodySize"\n ];\n const timing = {};\n for (const field of fields) {\n const value = entry?.[field];\n if (typeof value === "number" && Number.isFinite(value) && value >= 0) {\n timing[field] = roundRouteTimingValue(value);\n }\n }\n return Object.keys(timing).length ? timing : null;\n}\nfunction createRouteTimingRecorder(window, logging2) {\n const { log } = logging2;\n function emitRouteTiming(phase, path, startedAt, detail = {}) {\n const entry = {\n phase,\n path,\n duration: Math.max(0, routeTimingNow() - startedAt),\n timestamp: Date.now(),\n ...detail\n };\n const timings = Array.isArray(window.__veryfrontRouteTimings) ? window.__veryfrontRouteTimings : [];\n timings.push(entry);\n if (timings.length > MAX_ROUTE_TIMINGS) {\n timings.splice(0, timings.length - MAX_ROUTE_TIMINGS);\n }\n window.__veryfrontRouteTimings = timings;\n try {\n window.dispatchEvent(new CustomEvent("veryfront:route-timing", { detail: entry }));\n } catch (_) {\n }\n log("Route timing:", entry);\n return entry;\n }\n function getPageDataResourceTiming(endpoint, fetchStartedAt) {\n try {\n if (typeof performance === "undefined" || typeof performance.getEntriesByName !== "function") {\n return null;\n }\n const href = new URL(endpoint, window.location.href).href;\n const entries = performance.getEntriesByName(href, "resource");\n if (!entries.length) return null;\n for (let index = entries.length - 1; index >= 0; index--) {\n const entry = entries[index];\n const responseEnd = entry?.responseEnd;\n if (typeof responseEnd === "number" && Number.isFinite(responseEnd) && responseEnd + 1 >= fetchStartedAt) {\n return extractResourceTiming(entry);\n }\n }\n return null;\n } catch (_) {\n return null;\n }\n }\n function buildPageDataTimingDetail(response, endpoint, fetchStartedAt, source) {\n const detail = { source, status: response.status };\n const serverTiming = readResponseServerTiming(response);\n if (serverTiming) {\n detail.serverTiming = serverTiming;\n const serverTimingMetrics = parseServerTimingMetrics(serverTiming);\n if (serverTimingMetrics) detail.serverTimingMetrics = serverTimingMetrics;\n }\n const resourceTiming = getPageDataResourceTiming(response.url || endpoint, fetchStartedAt);\n if (resourceTiming) detail.resourceTiming = resourceTiming;\n return detail;\n }\n return { emitRouteTiming, buildPageDataTimingDetail };\n}\n\n// src/html/managed-head-protocol.ts\nvar HEAD_PROVENANCE_ATTRIBUTE = "data-vf-head";\nvar HEAD_LEGACY_MANAGED_ATTRIBUTE = "data-veryfront-managed";\nvar HEAD_CONTENT_HASH_ATTRIBUTE = "data-vf-hash";\nvar HEAD_REACT_MANAGED_ATTRIBUTE = "data-vf-react-head";\nvar HEAD_REACT_OWNER_ATTRIBUTE = "data-vf-react-head-owner";\nvar HEAD_ROUTE_MANAGED_ATTRIBUTE = "data-vf-route-head";\nvar HEAD_SERVER_COMMIT_ATTRIBUTE = "data-vf-server-head-commit";\nvar HEAD_SHELL_PROVENANCE_ATTRIBUTE = "data-vf-shell-head";\nvar HEAD_SSR_PAYLOAD_ATTRIBUTE = "data-vf-ssr-head";\nvar MAX_MANAGED_HEAD_BYTES = 2 * 1024 * 1024;\nvar MAX_MANAGED_HEAD_PAYLOAD_BYTES = MAX_MANAGED_HEAD_BYTES * 2;\nvar SINGLETON_META_KEYS = /* @__PURE__ */ new Set([\n "description",\n "robots",\n "viewport",\n "referrer",\n "color-scheme",\n "application-name",\n "generator",\n "og:title",\n "og:description",\n "og:url",\n "og:type",\n "og:site_name",\n "og:locale",\n "twitter:card",\n "twitter:site",\n "twitter:creator",\n "twitter:title",\n "twitter:description",\n "twitter:image",\n "twitter:image:alt"\n]);\nvar SINGLETON_LINK_RELS = /* @__PURE__ */ new Set([\n "canonical",\n "manifest",\n "amphtml"\n]);\nvar MAX_HEAD_ATTRIBUTE_VALUE_BYTES = 64 * 1024;\nvar MAX_HEAD_ATTRIBUTE_BYTES = 1024 * 1024;\nvar MAX_HEAD_CONTENT_BYTES = 1024 * 1024;\nvar headTextEncoder = new TextEncoder();\nvar BOOLEAN_HEAD_ATTRIBUTES = /* @__PURE__ */ new Set([\n "async",\n "defer",\n "disabled",\n "itemscope",\n "nomodule"\n]);\nfunction isHeadFrameworkAttribute(name) {\n switch (name.toLowerCase()) {\n case HEAD_PROVENANCE_ATTRIBUTE:\n case HEAD_LEGACY_MANAGED_ATTRIBUTE:\n case HEAD_CONTENT_HASH_ATTRIBUTE:\n case HEAD_REACT_MANAGED_ATTRIBUTE:\n case HEAD_REACT_OWNER_ATTRIBUTE:\n case HEAD_ROUTE_MANAGED_ATTRIBUTE:\n case HEAD_SERVER_COMMIT_ATTRIBUTE:\n case HEAD_SHELL_PROVENANCE_ATTRIBUTE:\n case HEAD_SSR_PAYLOAD_ATTRIBUTE:\n return true;\n default:\n return false;\n }\n}\nfunction normalizeHeadIdentityValue(value) {\n const normalized = value?.trim().toLowerCase();\n return normalized || void 0;\n}\nfunction readOwnString(record, key) {\n try {\n const descriptor = Reflect.getOwnPropertyDescriptor(record, key);\n return descriptor && !descriptor.get && !descriptor.set && "value" in descriptor && typeof descriptor.value === "string" ? descriptor.value : void 0;\n } catch {\n return void 0;\n }\n}\nfunction headMetaSingletonKeyFromRecord(meta) {\n if (readOwnString(meta, "charset") !== void 0) return "meta:charset";\n const key = normalizeHeadIdentityValue(\n readOwnString(meta, "property") ?? readOwnString(meta, "name")\n );\n if (!key) return void 0;\n if (key === "theme-color") {\n return `meta:theme-color:${readOwnString(meta, "media")?.trim() ?? ""}`;\n }\n return SINGLETON_META_KEYS.has(key) ? `meta:${key}` : void 0;\n}\nfunction headLinkSingletonKeyFromRecord(link) {\n const rel = normalizeHeadIdentityValue(readOwnString(link, "rel"));\n return rel && SINGLETON_LINK_RELS.has(rel) ? `link:${rel}` : void 0;\n}\n\n// src/html/client-head-manager.ts\nvar HEAD_MANAGER_STATE_SYMBOL = /* @__PURE__ */ Symbol.for(\n "veryfront.client-head-manager.v2"\n);\nvar CROSS_PAGE_PRESERVED_SINGLETON_KEYS = /* @__PURE__ */ new Set([\n "meta:viewport",\n "link:manifest"\n]);\nfunction getClientHeadManagerState() {\n const globalState = globalThis;\n return globalState[HEAD_MANAGER_STATE_SYMBOL] ?? (globalState[HEAD_MANAGER_STATE_SYMBOL] = {\n documents: /* @__PURE__ */ new WeakMap()\n });\n}\nfunction readElementAttributes(element) {\n const attributes = [];\n for (const attribute of element.attributes) {\n const name = attribute.name.toLowerCase();\n if (isHeadFrameworkAttribute(name)) continue;\n const nonce = name === "nonce" && "nonce" in element ? element.nonce : "";\n const value = BOOLEAN_HEAD_ATTRIBUTES.has(name) ? "" : nonce || attribute.value;\n attributes.push([name, value]);\n }\n return attributes.sort(([left], [right]) => left.localeCompare(right));\n}\nfunction elementSingletonKey(element) {\n const tagName = element.tagName.toLowerCase();\n if (tagName === "title") return "title";\n const attributes = Object.fromEntries(readElementAttributes(element));\n if (tagName === "meta") return headMetaSingletonKeyFromRecord(attributes);\n if (tagName === "link") return headLinkSingletonKeyFromRecord(attributes);\n return void 0;\n}\nfunction promoteToShellHeadBaseline(element) {\n for (const attribute of [...element.attributes]) {\n if (isHeadFrameworkAttribute(attribute.name)) {\n element.removeAttribute(attribute.name);\n }\n }\n element.setAttribute(HEAD_SHELL_PROVENANCE_ATTRIBUTE, "true");\n}\nfunction isCrossPagePreservedSingleton(element, singletonKey = elementSingletonKey(element)) {\n return element.parentElement !== null && singletonKey !== void 0 && CROSS_PAGE_PRESERVED_SINGLETON_KEYS.has(singletonKey);\n}\nfunction isFrameworkOwnedHeadElement(element) {\n return element.getAttribute(HEAD_PROVENANCE_ATTRIBUTE) === "true" || element.getAttribute(HEAD_REACT_MANAGED_ATTRIBUTE) === "true" || element.getAttribute(HEAD_LEGACY_MANAGED_ATTRIBUTE) === "1" || element.getAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE) === "true" || element.getAttribute(HEAD_SHELL_PROVENANCE_ATTRIBUTE) === "true";\n}\nfunction retireFrameworkHeadElement(element) {\n if (isCrossPagePreservedSingleton(element)) {\n promoteToShellHeadBaseline(element);\n return;\n }\n element.remove();\n}\nfunction retireClientHeadOwnership(targetDocument) {\n const manager = getClientHeadManagerState().documents.get(targetDocument);\n if (manager) {\n manager.retire();\n return;\n }\n for (const element of [...targetDocument.head?.children ?? []]) {\n if (isFrameworkOwnedHeadElement(element)) retireFrameworkHeadElement(element);\n }\n}\n\n// src/html/client-route-head.ts\nfunction updateRouteTitle(title, targetDocument = document) {\n if (typeof title !== "string" || !title) return;\n const titles = [...targetDocument.head.querySelectorAll("title")];\n if (titles.some((element) => element.getAttribute(HEAD_LEGACY_MANAGED_ATTRIBUTE) === "1")) {\n return;\n }\n let titleElement = titles.find(\n (element) => element.getAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE) === "true"\n );\n for (const element of titles) {\n if (element !== titleElement) element.remove();\n }\n if (!titleElement) {\n titleElement = targetDocument.createElement("title");\n titleElement.setAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE, "true");\n targetDocument.head.appendChild(titleElement);\n }\n titleElement.textContent = title;\n}\nfunction updateRouteMetaTag(targetDocument, selector, attributeName, attributeValue, content) {\n const matches = [...targetDocument.head.querySelectorAll(selector)];\n if (matches.some((element) => element.getAttribute(HEAD_LEGACY_MANAGED_ATTRIBUTE) === "1")) {\n return;\n }\n let metaTag = matches.find(\n (element) => element.getAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE) === "true"\n );\n if (!metaTag) {\n metaTag = targetDocument.createElement("meta");\n metaTag.setAttribute(attributeName, attributeValue);\n metaTag.setAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE, "true");\n targetDocument.head.appendChild(metaTag);\n }\n metaTag.setAttribute("content", content);\n}\nfunction updateRouteMetaTags(metadata, targetDocument = document) {\n if (typeof metadata.description === "string" && metadata.description) {\n updateRouteMetaTag(\n targetDocument,\n \'meta[name="description"]\',\n "name",\n "description",\n metadata.description\n );\n }\n if (typeof metadata.ogTitle === "string" && metadata.ogTitle) {\n updateRouteMetaTag(\n targetDocument,\n \'meta[property="og:title"]\',\n "property",\n "og:title",\n metadata.ogTitle\n );\n }\n}\nfunction handoffClientRouteMetadata(metadata, targetDocument = document) {\n const retainedTitle = targetDocument.title;\n retireClientHeadOwnership(targetDocument);\n updateRouteTitle(\n typeof metadata.title === "string" && metadata.title ? metadata.title : retainedTitle,\n targetDocument\n );\n updateRouteMetaTags(metadata, targetDocument);\n}\n\n// src/html/hydration-script-builder/runtime/router.ts\nvar FETCH_TIMEOUT_MS = 1e4;\nvar MAX_RETRIES = 2;\nvar MAX_CACHE_SIZE = 50;\nvar CACHE_TTL_MS = 5 * 60 * 1e3;\nvar BACKGROUND_REFRESH_INTERVAL_MS = 30 * 1e3;\nvar PREFETCH_DELAY_MS = 100;\nvar MAX_PREFETCH_PATHS = 100;\nvar IDLE_PREFETCH_DELAY_MS = 1200;\nvar IDLE_PREFETCH_MAX_LINKS = 4;\nvar VIEWPORT_PREFETCH_MAX_LINKS = 8;\nvar PAGE_DATA_PREFETCH_CONCURRENCY = 2;\nvar VIEWPORT_PREFETCH_ROOT_MARGIN = "200px";\nvar MAX_SCROLL_POSITIONS = 100;\nfunction createRouterRuntime(deps) {\n const { env: env2, logging: logging2, routeTiming: routeTiming2, componentLoader: componentLoader2, snapshotModules: snapshotModules2 } = deps;\n const { window, document: document2, React: React2, RouterProvider: RouterProvider2, PageContextProvider: PageContextProvider2 } = env2;\n const { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = env2;\n const { log, logError, logBackgroundFetchFailure, perfStart, perfEnd } = logging2;\n const { emitRouteTiming, buildPageDataTimingDetail } = routeTiming2;\n const { loadComponent } = componentLoader2;\n const documentPinKey = deps.documentDependencyPinningCacheKey;\n let hydrationResolve;\n let hydrationReject;\n const hydrationPromise = new Promise((resolve, reject) => {\n hydrationResolve = resolve;\n hydrationReject = reject;\n });\n let hydrationCompleted = false;\n let hydrationFailed = false;\n function signalHydrationComplete() {\n hydrationCompleted = true;\n hydrationResolve();\n log("Hydration complete signal received");\n }\n function signalHydrationFailed(error) {\n hydrationFailed = true;\n hydrationReject(error);\n logError("Hydration failed signal received:", error);\n }\n window.__veryfrontHydrationComplete = signalHydrationComplete;\n window.__veryfrontHydrationFailed = signalHydrationFailed;\n function pageDataCacheIdentity2(path) {\n return pageDataCacheIdentity(path, documentPinKey);\n }\n function navigateDocument(target) {\n const safeUrl = resolveDocumentNavigationUrl(target, window.location.origin);\n if (safeUrl) {\n window.location.href = safeUrl;\n return;\n }\n logError("Refusing an unsafe document navigation:", target);\n window.location.reload();\n }\n let clientBuildVersion = null;\n function checkVersionMismatch(newVersion) {\n if (!clientBuildVersion) {\n clientBuildVersion = newVersion;\n log("Build version initialized:", newVersion);\n return false;\n }\n if (newVersion.serverStart !== clientBuildVersion.serverStart) {\n log("Server restarted, reloading...", {\n old: clientBuildVersion.serverStart,\n new: newVersion.serverStart\n });\n return true;\n }\n if (newVersion.framework !== clientBuildVersion.framework) {\n log("Framework version changed, reloading...", {\n old: clientBuildVersion.framework,\n new: newVersion.framework\n });\n return true;\n }\n if (newVersion.projectUpdated && clientBuildVersion.projectUpdated && newVersion.projectUpdated !== clientBuildVersion.projectUpdated) {\n log("Project content updated, reloading...", {\n old: clientBuildVersion.projectUpdated,\n new: newVersion.projectUpdated\n });\n return true;\n }\n return false;\n }\n const pageDataCache = /* @__PURE__ */ new Map();\n const pendingPageDataFetches = /* @__PURE__ */ new Map();\n const backgroundRefreshTimestamps = /* @__PURE__ */ new Map();\n function getCachedPageData(path) {\n const cacheIdentity = pageDataCacheIdentity2(path);\n const entry = pageDataCache.get(cacheIdentity);\n if (!entry) return null;\n if (Date.now() - entry.timestamp < CACHE_TTL_MS) return entry.data;\n pageDataCache.delete(cacheIdentity);\n backgroundRefreshTimestamps.delete(cacheIdentity);\n return null;\n }\n function setCachedPageData(path, data) {\n const cacheIdentity = pageDataCacheIdentity2(path);\n if (pageDataCache.size >= MAX_CACHE_SIZE) {\n const oldest = pageDataCache.keys().next().value;\n if (oldest) {\n pageDataCache.delete(oldest);\n backgroundRefreshTimestamps.delete(oldest);\n }\n }\n pageDataCache.set(cacheIdentity, { data, timestamp: Date.now() });\n }\n const scrollPositions = /* @__PURE__ */ new Map();\n function saveScrollPosition(path) {\n if (scrollPositions.size >= MAX_SCROLL_POSITIONS) {\n const oldest = scrollPositions.keys().next().value;\n if (oldest) scrollPositions.delete(oldest);\n }\n scrollPositions.set(path, window.scrollY);\n }\n function restoreScrollPosition(path) {\n const savedY = scrollPositions.get(path);\n if (savedY === void 0) return false;\n requestAnimationFrame(() => window.scrollTo(0, savedY));\n return true;\n }\n let progressBar = null;\n let progressTimeout = null;\n function showNavigationProgress() {\n if (!progressBar) {\n progressBar = document2.createElement("div");\n progressBar.id = "vf-nav-progress";\n progressBar.style.cssText = "position:fixed;top:0;left:0;height:3px;width:0;background:linear-gradient(90deg,#0066ff,#00aaff);z-index:99999;transition:width 0.3s ease-out,opacity 0.2s;opacity:1;";\n document2.body.prepend(progressBar);\n }\n progressBar.style.opacity = "1";\n progressBar.style.width = "30%";\n progressTimeout = setTimeout2(() => {\n if (progressBar?.style) progressBar.style.width = "70%";\n }, 300);\n document2.body.setAttribute("aria-busy", "true");\n }\n function hideNavigationProgress() {\n if (progressTimeout) {\n clearTimeout2(progressTimeout);\n progressTimeout = null;\n }\n if (progressBar) {\n progressBar.style.width = "100%";\n setTimeout2(() => {\n if (!progressBar) return;\n progressBar.style.opacity = "0";\n setTimeout2(() => {\n if (progressBar) progressBar.style.width = "0";\n }, 200);\n }, 150);\n }\n document2.body.removeAttribute("aria-busy");\n }\n let currentAbortController = null;\n function sleep(ms) {\n return new Promise((resolve) => setTimeout2(resolve, ms));\n }\n async function fetchWithRetry(url, options, maxRetries = MAX_RETRIES) {\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n const controller = new AbortController();\n const callerSignal = options.signal;\n const abortFromCaller = () => controller.abort();\n if (callerSignal?.aborted) controller.abort();\n callerSignal?.addEventListener("abort", abortFromCaller, { once: true });\n const timeout = setTimeout2(() => controller.abort(), FETCH_TIMEOUT_MS);\n try {\n const response = await env2.fetch(url, { ...options, signal: controller.signal });\n clearTimeout2(timeout);\n callerSignal?.removeEventListener("abort", abortFromCaller);\n if (response.ok) return response;\n if (response.status >= 500 && attempt < maxRetries) {\n log("Server error, retrying...", response.status);\n await sleep(Math.pow(2, attempt) * 500);\n continue;\n }\n return response;\n } catch (error) {\n clearTimeout2(timeout);\n callerSignal?.removeEventListener("abort", abortFromCaller);\n if (error.name === "AbortError" && callerSignal?.aborted) throw error;\n if (attempt === maxRetries) throw error;\n log("Fetch failed, retrying...", error.message);\n await sleep(Math.pow(2, attempt) * 500);\n }\n }\n throw new Error("Failed to fetch page data");\n }\n async function fetchPageDataFresh(path, signal, options = {}) {\n const {\n triggerReloadOnVersionMismatch = false,\n recordRouteTiming = false,\n timingSource = "network"\n } = options;\n const endpoint = buildPageDataEndpoint(path, window.location.origin);\n const startedAt = recordRouteTiming ? routeTimingNow() : 0;\n log("Fetching page data:", path);\n perfStart("fetch:" + path);\n const headers = options.prefetch ? { "X-Veryfront-Prefetch": "1" } : { "X-Veryfront-Navigation": "spa" };\n if (documentPinKey) {\n headers["X-Veryfront-Dependency-Pins"] = documentPinKey;\n }\n const response = await fetchWithRetry(endpoint, {\n headers,\n signal\n }, options.prefetch ? 0 : MAX_RETRIES);\n if (!response.ok) {\n perfEnd("fetch:" + path);\n if (recordRouteTiming) {\n emitRouteTiming(\n "page-data",\n path,\n startedAt,\n buildPageDataTimingDetail(response, endpoint, startedAt, timingSource)\n );\n }\n const error = new Error("Failed to fetch page data: " + response.status);\n error.status = response.status;\n throw error;\n }\n perfStart("parse:" + path);\n const data = assertPageDataMatchesDocumentSnapshot(\n path,\n await response.json(),\n documentPinKey\n );\n perfEnd("parse:" + path);\n perfEnd("fetch:" + path);\n if (recordRouteTiming) {\n emitRouteTiming(\n "page-data",\n path,\n startedAt,\n buildPageDataTimingDetail(response, endpoint, startedAt, timingSource)\n );\n }\n if (triggerReloadOnVersionMismatch) {\n const checkedData = handlePageDataVersionMismatch(path, data);\n if (checkedData !== data) return checkedData;\n }\n setCachedPageData(path, data);\n return data;\n }\n function handlePageDataVersionMismatch(path, data) {\n if (data.buildVersion && checkVersionMismatch(data.buildVersion)) {\n log("Version mismatch detected, performing full page reload to:", path);\n navigateDocument(path);\n return new Promise(() => {\n });\n }\n return data;\n }\n function startPageDataFetch(path, signal, options = {}) {\n const cacheIdentity = pageDataCacheIdentity2(path);\n const request = fetchPageDataFresh(path, signal, options).finally(() => {\n if (options.trackPending !== false && pendingPageDataFetches.get(cacheIdentity) === request) {\n pendingPageDataFetches.delete(cacheIdentity);\n }\n });\n if (options.trackPending !== false) {\n pendingPageDataFetches.set(cacheIdentity, request);\n }\n return request;\n }\n function fetchPageDataDeduped(path) {\n const pending = pendingPageDataFetches.get(pageDataCacheIdentity2(path));\n if (pending) return pending;\n return startPageDataFetch(path, null);\n }\n function refreshPageDataInBackground(path) {\n const cacheIdentity = pageDataCacheIdentity2(path);\n const lastRefreshAt = backgroundRefreshTimestamps.get(cacheIdentity) || 0;\n const now = Date.now();\n if (now - lastRefreshAt < BACKGROUND_REFRESH_INTERVAL_MS) return;\n backgroundRefreshTimestamps.set(cacheIdentity, now);\n fetchPageDataDeduped(path).catch((error) => {\n logBackgroundFetchFailure("Stale page data refresh", path, error);\n });\n }\n async function fetchPageDataForNavigation(path, signal) {\n const startedAt = routeTimingNow();\n const cached = getCachedPageData(path);\n if (cached) {\n log("Using cached page data:", path);\n refreshPageDataInBackground(path);\n emitRouteTiming("page-data", path, startedAt, { source: "cache" });\n return cached;\n }\n const pending = pendingPageDataFetches.get(pageDataCacheIdentity2(path));\n if (pending) {\n log("Reusing pending page data fetch for navigation:", path);\n const data = await pending;\n emitRouteTiming("page-data", path, startedAt, { source: "deduped" });\n return handlePageDataVersionMismatch(path, data);\n }\n return startPageDataFetch(path, signal, {\n triggerReloadOnVersionMismatch: true,\n recordRouteTiming: true,\n timingSource: "network"\n });\n }\n function fetchPageDataForPrefetch(path, signal) {\n if (getCachedPageData(path)) return Promise.resolve();\n return startPageDataFetch(path, signal, { prefetch: true, trackPending: false }).then((data) => preloadModulesForPageData(data, path)).catch((error) => {\n if (!isAbortError(error)) {\n logBackgroundFetchFailure("Page data prefetch", path, error);\n }\n throw error;\n });\n }\n let currentPath = window.location.pathname;\n let isNavigating = false;\n async function navigateSPA(href, historyMode = "push", restoreScroll = false) {\n currentAbortController?.abort();\n if (isNavigating) return;\n isNavigating = true;\n const [navigationPath] = href.split("#");\n removeQueuedPrefetch(navigationPath || href);\n abortActiveSpeculativePrefetches();\n currentAbortController = new AbortController();\n const signal = currentAbortController.signal;\n const navigationStartedAt = routeTimingNow();\n showNavigationProgress();\n perfStart("nav:total:" + href);\n try {\n log("SPA navigating to:", href);\n saveScrollPosition(currentPath);\n const [path, hash] = href.split("#");\n const targetPath = path || currentPath;\n perfStart("nav:fetchData:" + href);\n const pageData = await fetchPageDataForNavigation(targetPath, signal);\n perfEnd("nav:fetchData:" + href);\n if (signal.aborted) return;\n if (pageData && pageData.redirect && typeof pageData.redirect.destination === "string") {\n const redirectUrl = resolveDocumentNavigationUrl(\n pageData.redirect.destination,\n window.location.origin\n );\n if (redirectUrl) {\n log("SPA navigation redirect -> " + redirectUrl);\n window.location.href = redirectUrl;\n return;\n }\n }\n if (historyMode === "push") {\n window.history.pushState({ pageData, scrollY: 0 }, "", href);\n } else if (historyMode === "replace") {\n window.history.replaceState({ pageData, scrollY: 0 }, "", href);\n }\n currentPath = targetPath;\n router.pathname = targetPath;\n router.query = Object.fromEntries(new URLSearchParams(window.location.search));\n router.params = flattenRouteParams(pageData.params);\n perfStart("nav:render:" + href);\n await renderPageFromData(pageData, targetPath);\n perfEnd("nav:render:" + href);\n if (restoreScroll) {\n restoreScrollPosition(targetPath);\n } else if (hash) {\n requestAnimationFrame(() => {\n const target = document2.getElementById(hash);\n if (target) {\n target.scrollIntoView({ behavior: "smooth" });\n return;\n }\n window.scrollTo(0, 0);\n });\n } else {\n window.scrollTo(0, 0);\n }\n hideNavigationProgress();\n perfEnd("nav:total:" + href);\n emitRouteTiming("total", targetPath, navigationStartedAt, {\n href,\n historyMode,\n restoreScroll\n });\n log("SPA navigation complete");\n } catch (error) {\n hideNavigationProgress();\n if (error.name === "AbortError") {\n log("Navigation aborted");\n return;\n }\n logError("SPA navigation failed:", error.message);\n if (error.status === 404) {\n logError("Page not found:", href);\n }\n navigateDocument(href);\n } finally {\n isNavigating = false;\n currentAbortController = null;\n processPageDataPrefetchQueue();\n }\n }\n async function loadPageDataComponent(pageData, path, options = {}) {\n if (!pageData.isolatedClientPage) return loadComponent(path, pageData, options);\n const moduleUrl = buildPinnedRscModuleUrl(path, pageData);\n const module = await snapshotModules2.importSnapshotBoundModule(\n moduleUrl,\n options.allowDocumentReload !== false\n );\n return module.MDXLayout || module.MainLayout || module.default || module;\n }\n async function renderPageFromData(pageData, targetPath) {\n if (pageData.requiresFullDocumentNavigation) {\n throw new Error("Server layout requires full document navigation");\n }\n if (window.__veryfrontSetReleaseId) {\n window.__veryfrontSetReleaseId(pageData.releaseId || null);\n }\n if (window.__veryfrontSetReleaseAssetModules) {\n window.__veryfrontSetReleaseAssetModules(pageData.releaseAssetModules || null);\n }\n perfStart("render:loadAll");\n const allPaths = getPageDataModulePaths(pageData);\n const modulesStartedAt = routeTimingNow();\n const components = await Promise.all(\n allPaths.map((path) => loadPageDataComponent(pageData, path))\n );\n emitRouteTiming("modules", targetPath, modulesStartedAt, { count: allPaths.length });\n perfEnd("render:loadAll");\n const [PageComponent, ...rest] = components;\n const ErrorComponent = pageData.errorPath ? rest.pop() : null;\n const AppComponent = pageData.appPath ? rest.pop() : null;\n const LayoutComponents = rest;\n if (!PageComponent) {\n throw new Error("Failed to load page component: " + pageData.pagePath);\n }\n handoffClientRouteMetadata(\n pageData.frontmatter ?? {},\n document2\n );\n if (pageData.css) {\n const existingStyle = document2.getElementById("veryfront-spa-css");\n if (existingStyle) {\n existingStyle.textContent = pageData.css;\n } else {\n const styleEl = document2.createElement("style");\n const nonce = getDocumentNonce(document2);\n if (nonce) styleEl.setAttribute("nonce", nonce);\n styleEl.id = "veryfront-spa-css";\n styleEl.textContent = pageData.css;\n document2.head.appendChild(styleEl);\n }\n log("Injected CSS for SPA navigation", { cssLength: pageData.css.length });\n } else if (pageData.cssAction === "clear") {\n const existingStyle = document2.getElementById("veryfront-spa-css");\n if (existingStyle) {\n existingStyle.remove();\n log("Cleared SPA CSS for release stylesheet navigation");\n }\n }\n const normalizedParams = flattenRouteParams(pageData.params);\n let tree = React2.createElement(PageComponent, {\n ...pageData.props,\n params: normalizedParams\n });\n if (pageData.layouts?.length) {\n for (let i = pageData.layouts.length - 1; i >= 0; i--) {\n const layout = pageData.layouts[i];\n const LayoutComponent = LayoutComponents[i];\n if (!LayoutComponent || !layout) continue;\n const layoutProps = pageData.layoutProps?.[layout.path] || {};\n tree = React2.createElement(LayoutComponent, { ...layoutProps, children: tree });\n }\n }\n if (AppComponent) {\n tree = React2.createElement(AppComponent, { children: tree });\n log("Wrapped with App component for SPA navigation");\n }\n if (ErrorComponent) {\n class AppRouterErrorBoundary extends React2.Component {\n constructor(props) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n static getDerivedStateFromError(error) {\n return { hasError: true, error };\n }\n render() {\n if (this.state.hasError) {\n return React2.createElement(ErrorComponent, {\n error: this.state.error,\n reset: () => this.setState({ hasError: false, error: null })\n });\n }\n return this.props.children;\n }\n }\n tree = React2.createElement(AppRouterErrorBoundary, null, tree);\n }\n const headingsArray = pageData.headings || [];\n const pageContext = {\n slug: pageData.slug || "",\n path: pageData.pagePath || targetPath,\n params: normalizedParams,\n query: Object.fromEntries(new URLSearchParams(window.location.search)),\n frontmatter: pageData.frontmatter || {},\n data: pageData.props || {},\n headings: headingsArray,\n mdxHeadings: headingsArray\n };\n tree = React2.createElement(PageContextProvider2, { pageContext, children: tree });\n tree = React2.createElement(RouterProvider2, { router, children: tree });\n const container = pageData.isolatedClientPage ? document2.getElementById("veryfront-page-island") : document2.getElementById("root");\n if (!hydrationCompleted && !hydrationFailed) {\n log("Waiting for hydration to complete before SPA render...");\n try {\n await Promise.race([\n hydrationPromise,\n new Promise(\n (_, reject) => setTimeout2(() => reject(new Error("Hydration timeout")), 1e4)\n )\n ]);\n } catch (waitError) {\n log("Hydration wait failed:", waitError.message);\n }\n }\n if (container?.__reactRoot) {\n perfStart("render:reactRender");\n container.__reactRoot.render(tree);\n perfEnd("render:reactRender");\n log("Page re-rendered via SPA");\n scheduleRoutePrefetchRefresh();\n return;\n }\n if (hydrationFailed) {\n throw new Error(\n "React root not found - hydration failed, falling back to full page navigation"\n );\n }\n throw new Error("React root not found");\n }\n let prefetchTimeout = null;\n let currentHoverLink = null;\n let routePrefetchRefreshPending = false;\n let viewportPrefetchObserver = null;\n const observedPrefetchLinks = /* @__PURE__ */ new WeakSet();\n const prefetchedPaths = /* @__PURE__ */ new Set();\n const inFlightPrefetches = /* @__PURE__ */ new Set();\n const queuedPrefetchPaths = /* @__PURE__ */ new Set();\n const pageDataPrefetchQueue = [];\n const activePageDataPrefetchControllers = /* @__PURE__ */ new Map();\n function cancelScheduledPrefetch() {\n if (prefetchTimeout) {\n clearTimeout2(prefetchTimeout);\n prefetchTimeout = null;\n }\n currentHoverLink = null;\n }\n function getPageDataModulePaths(pageData) {\n const layoutPaths = (pageData.layouts || []).map((l) => l.path).filter(Boolean);\n const allPaths = [pageData.pagePath, ...layoutPaths].filter(Boolean);\n if (pageData.appPath) allPaths.push(pageData.appPath);\n if (pageData.errorPath) allPaths.push(pageData.errorPath);\n return allPaths;\n }\n function getCurrentRouteHref() {\n return window.location.pathname + window.location.search;\n }\n function getInternalRouteHrefFromLink(link) {\n if (!link || link.target === "_blank" || link.hasAttribute("download") || link.getAttribute("data-prefetch") === "false") {\n return null;\n }\n const href = link.getAttribute("href");\n if (!href || href.startsWith("#") || href.startsWith("//") || !href.startsWith("/")) {\n return null;\n }\n try {\n const url = new URL(href, window.location.origin);\n if (url.origin !== window.location.origin) return null;\n const routeHref = url.pathname + url.search;\n return routeHref === getCurrentRouteHref() ? null : routeHref;\n } catch (_) {\n return null;\n }\n }\n function getEligiblePrefetchLinks(limit) {\n const links = [];\n const seenHrefs = /* @__PURE__ */ new Set();\n for (const link of document2.querySelectorAll("a[href]")) {\n const href = getInternalRouteHrefFromLink(link);\n if (!href || seenHrefs.has(href)) continue;\n seenHrefs.add(href);\n links.push({ link, href });\n if (links.length >= limit) break;\n }\n return links;\n }\n async function preloadModulesForPageData(pageData, path) {\n if (!pageData || pageData.requiresFullDocumentNavigation) return;\n if (pageData.releaseId && window.__veryfrontSetReleaseId) {\n window.__veryfrontSetReleaseId(pageData.releaseId);\n }\n if (pageData.releaseAssetModules && window.__veryfrontSetReleaseAssetModules) {\n window.__veryfrontSetReleaseAssetModules(pageData.releaseAssetModules);\n }\n const modulePaths = getPageDataModulePaths(pageData);\n if (modulePaths.length === 0) return;\n try {\n await Promise.all(\n modulePaths.map(\n (modulePath) => loadPageDataComponent(pageData, modulePath, { allowDocumentReload: false })\n )\n );\n } catch (error) {\n if (isDependencySnapshotConflict(error)) {\n const cacheIdentity = pageDataCacheIdentity2(path);\n pageDataCache.delete(cacheIdentity);\n backgroundRefreshTimestamps.delete(cacheIdentity);\n prefetchedPaths.delete(path);\n throw error;\n }\n logBackgroundFetchFailure("Module prefetch", path, error);\n }\n }\n function removeQueuedPrefetch(path) {\n queuedPrefetchPaths.delete(path);\n for (let i = pageDataPrefetchQueue.length - 1; i >= 0; i--) {\n if (pageDataPrefetchQueue[i] === path) pageDataPrefetchQueue.splice(i, 1);\n }\n }\n function abortActiveSpeculativePrefetches() {\n for (const controller of activePageDataPrefetchControllers.values()) {\n controller.abort();\n }\n }\n function processPageDataPrefetchQueue() {\n if (isNavigating) return;\n while (activePageDataPrefetchControllers.size < PAGE_DATA_PREFETCH_CONCURRENCY && pageDataPrefetchQueue.length > 0) {\n const href = pageDataPrefetchQueue.shift();\n queuedPrefetchPaths.delete(href);\n if (prefetchedPaths.has(href) || inFlightPrefetches.has(href) || getCachedPageData(href)) {\n continue;\n }\n if (prefetchedPaths.size >= MAX_PREFETCH_PATHS) {\n const oldest = prefetchedPaths.values().next().value;\n if (oldest) prefetchedPaths.delete(oldest);\n }\n const controller = new AbortController();\n prefetchedPaths.add(href);\n inFlightPrefetches.add(href);\n activePageDataPrefetchControllers.set(href, controller);\n fetchPageDataForPrefetch(href, controller.signal).catch((error) => {\n prefetchedPaths.delete(href);\n if (isDependencySnapshotConflict(error)) {\n logBackgroundFetchFailure("Module prefetch", href, error);\n }\n }).finally(() => {\n inFlightPrefetches.delete(href);\n activePageDataPrefetchControllers.delete(href);\n processPageDataPrefetchQueue();\n });\n }\n }\n function prefetchPage(href) {\n if (isNavigating) return;\n if (prefetchedPaths.has(href) || inFlightPrefetches.has(href) || queuedPrefetchPaths.has(href)) return;\n const cachedPageData = getCachedPageData(href);\n if (cachedPageData) {\n preloadModulesForPageData(cachedPageData, href).catch((error) => {\n logBackgroundFetchFailure("Module prefetch", href, error);\n });\n return;\n }\n queuedPrefetchPaths.add(href);\n pageDataPrefetchQueue.push(href);\n processPageDataPrefetchQueue();\n }\n function prefetchEligibleRouteLinks(limit) {\n for (const { href } of getEligiblePrefetchLinks(limit)) {\n prefetchPage(href);\n }\n }\n function ensureViewportPrefetchObserver() {\n if (viewportPrefetchObserver || typeof IntersectionObserver !== "function") {\n return viewportPrefetchObserver;\n }\n viewportPrefetchObserver = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n viewportPrefetchObserver?.unobserve(entry.target);\n const href = getInternalRouteHrefFromLink(\n entry.target\n );\n if (href) prefetchPage(href);\n }\n }, { rootMargin: VIEWPORT_PREFETCH_ROOT_MARGIN });\n return viewportPrefetchObserver;\n }\n function observeViewportPrefetchLinks() {\n const observer = ensureViewportPrefetchObserver();\n if (!observer) return;\n for (const { link } of getEligiblePrefetchLinks(VIEWPORT_PREFETCH_MAX_LINKS)) {\n if (observedPrefetchLinks.has(link)) continue;\n observedPrefetchLinks.add(link);\n observer.observe(link);\n }\n }\n function runRoutePrefetchRefresh() {\n routePrefetchRefreshPending = false;\n prefetchEligibleRouteLinks(IDLE_PREFETCH_MAX_LINKS);\n observeViewportPrefetchLinks();\n }\n function scheduleRoutePrefetchRefresh() {\n if (routePrefetchRefreshPending) return;\n routePrefetchRefreshPending = true;\n setTimeout2(() => {\n if (typeof requestIdleCallback === "function") {\n requestIdleCallback(runRoutePrefetchRefresh, { timeout: IDLE_PREFETCH_DELAY_MS });\n return;\n }\n runRoutePrefetchRefresh();\n }, IDLE_PREFETCH_DELAY_MS);\n }\n const router = {\n domain: window.location.origin,\n path: window.location.pathname,\n push: (path) => {\n void navigateSPA(path, "push");\n },\n replace: (path) => {\n void navigateSPA(path, "replace");\n },\n back: () => {\n window.history.back();\n },\n forward: () => {\n window.history.forward();\n },\n prefetch: (path) => {\n prefetchPage(path);\n },\n pathname: window.location.pathname,\n query: Object.fromEntries(new URLSearchParams(window.location.search)),\n // Seed route params from the hydration data (issue #2741). Catch-all\n // segments arrive as arrays and are joined so no path info is lost.\n params: flattenRouteParams(deps.initialHydrationData.params || {}),\n isPreview: false,\n isMounted: true,\n navigate: (path) => navigateSPA(path, "push"),\n reload: () => window.location.reload()\n };\n window.__veryfrontRouter = router;\n if (deps.navigationStoreUsesRegistryFallback) {\n log("Router runtime does not export getNavigationStore; using shared v1 registry fallback");\n }\n if (typeof deps.getNavigationStore === "function") {\n deps.getNavigationStore().setNavigator((href, options) => {\n const mode = options && options.history;\n const historyMode = mode === "replace" ? "replace" : mode === "none" ? "none" : "push";\n return navigateSPA(href, historyMode);\n });\n }\n window.addEventListener("popstate", async (e) => {\n const path = window.location.pathname;\n log("Popstate:", path);\n saveScrollPosition(currentPath);\n if (!e.state?.pageData) {\n await navigateSPA(path, "none", true);\n return;\n }\n showNavigationProgress();\n try {\n currentPath = path;\n router.pathname = path;\n router.query = Object.fromEntries(new URLSearchParams(window.location.search));\n router.params = flattenRouteParams(e.state.pageData.params);\n await renderPageFromData(e.state.pageData, path);\n restoreScrollPosition(path);\n hideNavigationProgress();\n } catch (error) {\n hideNavigationProgress();\n logError("Popstate render failed:", error.message);\n window.location.reload();\n }\n });\n document2.addEventListener("click", (e) => {\n const link = e.target?.closest("a[href]");\n if (!link) return;\n const href = link.getAttribute("href");\n if (!href) return;\n if (href.startsWith("#")) {\n const target = document2.getElementById(href.slice(1));\n if (!target) return;\n e.preventDefault();\n target.scrollIntoView({ behavior: "smooth" });\n window.history.pushState(null, "", href);\n return;\n }\n if (link.target === "_blank" || link.hasAttribute("download") || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || !href.startsWith("/") || href.startsWith("//")) {\n return;\n }\n e.preventDefault();\n cancelScheduledPrefetch();\n void navigateSPA(href, "push");\n });\n document2.addEventListener(\n "mouseenter",\n (e) => {\n if (!e.target || typeof e.target.closest !== "function") return;\n const link = e.target.closest("a[href]");\n if (!link) return;\n const href = getInternalRouteHrefFromLink(link);\n if (!href) return;\n if (currentHoverLink === link) return;\n if (prefetchTimeout) {\n clearTimeout2(prefetchTimeout);\n prefetchTimeout = null;\n }\n currentHoverLink = link;\n prefetchTimeout = setTimeout2(() => {\n prefetchPage(href);\n prefetchTimeout = null;\n }, PREFETCH_DELAY_MS);\n },\n true\n );\n document2.addEventListener(\n "mouseleave",\n (e) => {\n if (!e.target || typeof e.target.closest !== "function") return;\n const relatedTarget = e.relatedTarget;\n if (currentHoverLink && relatedTarget && currentHoverLink.contains(relatedTarget)) return;\n cancelScheduledPrefetch();\n },\n true\n );\n if (document2.readyState === "loading") {\n document2.addEventListener("DOMContentLoaded", scheduleRoutePrefetchRefresh, { once: true });\n } else {\n scheduleRoutePrefetchRefresh();\n }\n window.useRouter = () => {\n try {\n return env2.useRouterFromModule();\n } catch (_) {\n return window.__veryfrontRouter;\n }\n };\n return {\n router,\n navigateSPA,\n renderPageFromData,\n prefetchPage,\n signalHydrationComplete,\n signalHydrationFailed\n };\n}\n\n// src/html/hydration-script-builder/runtime/renderer.ts\nfunction isModuleNotFoundError(error) {\n if (!error) return false;\n if (error instanceof SyntaxError) return false;\n const message = String(error.message || error);\n return /(?:dynamically imported module|Importing a module script failed|Failed to load module script)/i.test(message);\n}\nfunction preferReachedModuleError(earlier, later) {\n if (!earlier) return later;\n if (!later) return earlier;\n if (isModuleNotFoundError(earlier) && !isModuleNotFoundError(later)) return later;\n return earlier;\n}\nasync function loadPageModuleWithIndexFallback(basePath, pageSlug, pageModuleError, importModule) {\n try {\n return await importModule(basePath + ".js");\n } catch (error) {\n const routeError = preferReachedModuleError(pageModuleError, error);\n if (pageSlug === "index" || pageSlug.endsWith("/index")) throw routeError;\n try {\n return await importModule(basePath + "/index.js");\n } catch (indexError) {\n throw preferReachedModuleError(routeError, indexError);\n }\n }\n}\nfunction isAppRouterPath(path, appRouterRoot) {\n const normalizedPath = typeof path === "string" ? path.replace(/^\\/+/, "") : "";\n return normalizedPath === appRouterRoot || normalizedPath.startsWith(appRouterRoot + "/");\n}\nfunction isRootAppLayoutPath(path, appRouterRoot) {\n const normalizedPath = typeof path === "string" ? path.replace(/^\\/+/, "") : "";\n const pathWithoutExtension = normalizedPath.replace(/\\.(?:tsx|jsx|ts|js)$/, "");\n return pathWithoutExtension === appRouterRoot + "/layout";\n}\nfunction unwrapAppRouterDocumentLayout(LayoutComponent, React2) {\n return function AppRouterDocumentLayout(props) {\n const element = LayoutComponent(props);\n const asElement = element;\n if (!React2.isValidElement(element) || asElement.type !== "html") {\n return element;\n }\n const body = React2.Children.toArray(asElement.props?.children).find(\n (child) => React2.isValidElement(child) && child.type === "body"\n );\n return body?.props?.children ?? props.children;\n };\n}\nfunction createHydrationRenderer(deps) {\n const { env: env2, logging: logging2, componentLoader: componentLoader2, snapshotModules: snapshotModules2, moduleServerUrl: moduleServerUrl2 } = deps;\n const { window, document: document2, React: React2, RouterProvider: RouterProvider2, PageContextProvider: PageContextProvider2 } = env2;\n const { DEBUG, log, logError } = logging2;\n const { loadComponent, pathToModuleUrl } = componentLoader2;\n const { importSnapshotBoundModule } = snapshotModules2;\n async function renderPage(pathname) {\n const resolvedPathname = (() => {\n const input = typeof pathname === "string" ? pathname : window.location.pathname;\n try {\n return new URL(input, window.location.origin).pathname || "/";\n } catch (_) {\n const [pathOnly] = String(input || "/").split(/[?#]/);\n return pathOnly || "/";\n }\n })();\n const dataScript = findServerHydrationDataElement(document2);\n if (!dataScript) {\n logError("Hydration data not found");\n return;\n }\n let data = {};\n try {\n data = JSON.parse(dataScript.textContent || "{}");\n } catch (parseError) {\n logError("Failed to parse hydration data:", parseError);\n return;\n }\n log("Hydration data:", data);\n if (data.studioEmbed && window.__veryfrontSetStudioEmbed) {\n window.__veryfrontSetStudioEmbed(true);\n }\n if (window.__veryfrontSetReleaseId) {\n window.__veryfrontSetReleaseId(data.releaseId || null);\n }\n if (data.releaseAssetModules && window.__veryfrontSetReleaseAssetModules) {\n window.__veryfrontSetReleaseAssetModules(data.releaseAssetModules);\n }\n try {\n let pageModule;\n const pagePath = typeof data.pagePath === "string" ? data.pagePath : "";\n const normalizedPagePath = pagePath.replace(/^\\/+/, "");\n const normalizedAppRouterRoot = typeof data.appRouterRoot === "string" && data.appRouterRoot.replace(/^\\/+|\\/+$/g, "") ? data.appRouterRoot.replace(/^\\/+|\\/+$/g, "") : "app";\n const hasReleaseAssetModules = data.releaseAssetModules && Object.keys(data.releaseAssetModules).length > 0;\n const shouldRenderRscClientPage = data.clientModuleStrategy === "rsc-module" && !hasReleaseAssetModules && isAppRouterPath(normalizedPagePath, normalizedAppRouterRoot);\n const isolatedClientPage = data.isolatedClientPage === true;\n const loadHydrationComponent = async (path, preferRscModule) => {\n const normalizedPath = typeof path === "string" ? path.replace(/^\\/+/, "") : "";\n if (preferRscModule && isAppRouterPath(normalizedPath, normalizedAppRouterRoot)) {\n const moduleUrl = buildPinnedRscModuleUrl(path, data);\n log("Loading App Router component from RSC module:", moduleUrl);\n const module = await importSnapshotBoundModule(moduleUrl);\n return module.default || module;\n }\n return loadComponent(path, data);\n };\n let pageModuleError = null;\n if (data.pagePath) {\n const moduleUrl = shouldRenderRscClientPage ? buildPinnedRscModuleUrl(data.pagePath, data) : pathToModuleUrl(data.pagePath, data.studioEmbed, data);\n log("Loading page from hydration data:", moduleUrl);\n try {\n pageModule = await importSnapshotBoundModule(moduleUrl);\n } catch (error) {\n pageModuleError = error;\n logError("Failed to load page from hydration data:", error);\n }\n }\n if (!pageModule) {\n const pageSlug = resolvedPathname === "/" ? "index" : resolvedPathname.slice(1);\n log("Falling back to Pages Router pattern:", pageSlug);\n const prefix = pageSlug.startsWith("@/") ? "" : "/pages";\n const basePath = moduleServerUrl2 + prefix + "/" + pageSlug;\n pageModule = await loadPageModuleWithIndexFallback(\n basePath,\n pageSlug,\n pageModuleError,\n (moduleUrl) => importSnapshotBoundModule(appendDependencyPinningVersion(moduleUrl, data))\n );\n }\n if (!pageModule) {\n logError("Page module failed to load");\n return;\n }\n const PageComponent = pageModule.default || pageModule;\n if (!PageComponent) {\n logError("Page component not found");\n return;\n }\n const normalizedParams = flattenRouteParams(data.params);\n const pageProps = { ...data.props || {}, params: normalizedParams };\n let tree = React2.createElement(PageComponent, pageProps);\n const layouts = data.layouts;\n if (layouts?.length) {\n for (let i = layouts.length - 1; i >= 0; i--) {\n const layout = layouts[i];\n if (!layout) continue;\n const LayoutComponent = await loadHydrationComponent(\n layout.path,\n shouldRenderRscClientPage\n );\n if (LayoutComponent) {\n const WrappedLayoutComponent = shouldRenderRscClientPage && isRootAppLayoutPath(layout.path, normalizedAppRouterRoot) ? unwrapAppRouterDocumentLayout(LayoutComponent, React2) : LayoutComponent;\n const layoutProps = data.layoutProps?.[layout.path] || {};\n tree = React2.createElement(\n WrappedLayoutComponent,\n { ...layoutProps, children: tree }\n );\n }\n }\n }\n if (data.appPath && !isolatedClientPage) {\n const AppComponent = await loadHydrationComponent(data.appPath, shouldRenderRscClientPage);\n if (AppComponent) {\n tree = React2.createElement(AppComponent, { children: tree });\n }\n }\n if (data.errorPath) {\n const ErrorComponent = await loadHydrationComponent(\n data.errorPath,\n shouldRenderRscClientPage\n );\n if (ErrorComponent) {\n class AppRouterErrorBoundary extends React2.Component {\n constructor(props) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n static getDerivedStateFromError(error) {\n return { hasError: true, error };\n }\n render() {\n if (this.state.hasError) {\n return React2.createElement(ErrorComponent, {\n error: this.state.error,\n reset: () => this.setState({ hasError: false, error: null })\n });\n }\n return this.props.children;\n }\n }\n tree = React2.createElement(AppRouterErrorBoundary, null, tree);\n }\n }\n const headings = data.headings || [];\n const pageContext = {\n slug: data.slug || "",\n path: data.pagePath || resolvedPathname,\n params: normalizedParams,\n query: Object.fromEntries(new URLSearchParams(window.location.search)),\n frontmatter: data.frontmatter || {},\n data: data.props || {},\n headings,\n mdxHeadings: headings\n // Alias for backwards compatibility\n };\n tree = React2.createElement(PageContextProvider2, { pageContext, children: tree });\n tree = React2.createElement(RouterProvider2, { router: deps.router, children: tree });\n const container = isolatedClientPage ? document2.getElementById("veryfront-page-island") : document2.getElementById("root");\n if (!container) {\n if (isolatedClientPage) {\n throw new Error("Isolated client page root not found");\n }\n return;\n }\n if (container.__reactRoot) {\n container.__reactRoot.render(tree);\n log("Page re-rendered");\n return;\n }\n if (shouldRenderRscClientPage) {\n container.__reactRoot = env2.createRoot(container);\n container.__reactRoot.render(tree);\n log("Client-side React app rendered successfully");\n } else {\n const { hydrateRoot } = await import("react-dom/client");\n const options = {\n identifierPrefix: "vf",\n onRecoverableError: (error) => {\n if (data.dev && DEBUG) {\n log("Hydration mismatch (suppressed):", error.message);\n }\n }\n };\n container.__reactRoot = hydrateRoot(container, tree, options);\n log("Client-side React app hydrated successfully");\n }\n if (window.__veryfrontHydrationComplete) {\n window.__veryfrontHydrationComplete();\n }\n } catch (error) {\n logError("Client initialization error:", error);\n if (window.__veryfrontHydrationFailed) {\n window.__veryfrontHydrationFailed(error);\n }\n }\n }\n function start() {\n window.__veryfrontRenderPage = renderPage;\n void renderPage(window.location.pathname);\n const initialDataScript = findServerHydrationDataElement(document2);\n if (initialDataScript) {\n try {\n const pageData = JSON.parse(initialDataScript.textContent || "{}");\n if (pageData.pagePath) {\n window.history.replaceState({ pageData, scrollY: 0 }, "", window.location.href);\n log("Stored initial page data in history state");\n }\n } catch (_) {\n }\n }\n }\n return { renderPage, start };\n}\n\n// src/html/hydration-script-builder/runtime/navigation-store.ts\nvar NAVIGATION_STORE_REGISTRY_KEY = "veryfront.navigation.store.v1";\nfunction resolveNavigationStore(RouterRuntime2) {\n const usesRegistryFallback2 = typeof RouterRuntime2.getNavigationStore !== "function";\n if (!usesRegistryFallback2) {\n return {\n usesRegistryFallback: usesRegistryFallback2,\n getNavigationStore: RouterRuntime2.getNavigationStore\n };\n }\n return {\n usesRegistryFallback: usesRegistryFallback2,\n getNavigationStore: () => {\n const storeKey = Symbol.for(NAVIGATION_STORE_REGISTRY_KEY);\n const registry = globalThis;\n const existing = registry[storeKey];\n if (existing) return existing;\n const listeners = /* @__PURE__ */ new Set();\n let navigator = null;\n const store = {\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n getHref() {\n const loc = globalThis.location;\n return loc ? loc.pathname + loc.search + loc.hash : "/";\n },\n notify() {\n for (const listener of [...listeners]) {\n try {\n listener();\n } catch {\n }\n }\n },\n navigate(href, options) {\n if (navigator) return navigator(href, options);\n globalThis.location?.assign(href);\n return Promise.resolve();\n },\n setNavigator(next) {\n navigator = next;\n }\n };\n registry[storeKey] = store;\n return store;\n }\n };\n}\n\n// src/html/hydration-script-builder/runtime/main.ts\nvar runtimeWindow = globalThis;\nvar runtimeDocument = globalThis.document;\nvar env = {\n window: runtimeWindow,\n document: runtimeDocument,\n fetch: (url, init) => fetch(url, init),\n React,\n RouterProvider,\n PageContextProvider,\n createRoot: (container) => createRoot(container),\n importModule: (moduleUrl) => import(moduleUrl),\n useRouterFromModule,\n setTimeout: (handler, timeout) => setTimeout(handler, timeout),\n clearTimeout: (id) => clearTimeout(id)\n};\nvar logging = createLogging(runtimeWindow);\nvar initialHydrationData = readInitialHydrationData(runtimeDocument);\nvar documentDependencyPinningCacheKey = readDocumentDependencyPinningCacheKey(\n initialHydrationData\n);\nvar routeTiming = createRouteTimingRecorder(runtimeWindow, logging);\nvar snapshotModules = createSnapshotModuleImporter({\n importModule: env.importModule,\n fetchModule: env.fetch,\n reloadDocument: () => runtimeWindow.location.reload(),\n recoveryState: runtimeWindow\n});\nvar componentLoader = createComponentLoader({\n window: runtimeWindow,\n logging,\n moduleServerUrl: moduleServerUrl(runtimeWindow),\n snapshotModules\n});\nruntimeWindow.__veryfrontClearComponentCache = componentLoader.clearComponentCache;\nruntimeWindow.__veryfrontSetStudioEmbed = componentLoader.setStudioEmbed;\nruntimeWindow.__veryfrontSetReleaseId = componentLoader.setReleaseId;\nruntimeWindow.__veryfrontSetReleaseAssetModules = componentLoader.setReleaseAssetModules;\nruntimeWindow.__veryfrontSetHMRRefreshTimestamp = componentLoader.setHMRRefreshTimestamp;\nvar { usesRegistryFallback, getNavigationStore } = resolveNavigationStore(RouterRuntime);\nvar routerRuntime = createRouterRuntime({\n env,\n logging,\n routeTiming,\n componentLoader,\n snapshotModules,\n initialHydrationData,\n documentDependencyPinningCacheKey,\n getNavigationStore,\n navigationStoreUsesRegistryFallback: usesRegistryFallback\n});\ncreateHydrationRenderer({\n env,\n logging,\n componentLoader,\n snapshotModules,\n moduleServerUrl: moduleServerUrl(runtimeWindow),\n router: routerRuntime.router\n}).start();\n'; + '// src/html/hydration-script-builder/runtime/main.ts\nimport * as React from "react";\nimport { createRoot } from "react-dom/client";\nimport { RouterProvider, useRouter as useRouterFromModule } from "veryfront/router";\nimport * as RouterRuntime from "veryfront/router";\nimport { PageContextProvider } from "veryfront/context";\n\n// src/routing/flatten-route-params.ts\nfunction flattenRouteParams(params) {\n if (!params) return {};\n const flat = {};\n for (const [key, value] of Object.entries(params)) {\n if (value === void 0) continue;\n flat[key] = Array.isArray(value) ? value.join("/") : value;\n }\n return flat;\n}\n\n// src/html/hydration-script-builder/runtime/shared.ts\nfunction moduleServerUrl(window) {\n return window.location.origin + "/_vf_modules";\n}\nfunction createLogging(window) {\n const DEBUG = Boolean(\n window.__VERYFRONT_DEBUG__ || new URLSearchParams(window.location.search).has("vf_debug")\n );\n const log = DEBUG ? console.log.bind(console, "[Veryfront]") : () => {\n };\n const logError = console.error.bind(console, "[Veryfront]");\n function logBackgroundFetchFailure(reason, path, error) {\n const message = error?.message ?? String(error);\n log(reason + " failed:", path, message);\n }\n const perfTimers = /* @__PURE__ */ new Map();\n const perfStart = DEBUG ? (label) => {\n perfTimers.set(label, performance.now());\n } : () => {\n };\n const perfEnd = DEBUG ? (label) => {\n const start = perfTimers.get(label);\n if (start === void 0) return 0;\n const duration = performance.now() - start;\n perfTimers.delete(label);\n console.log(\n "[Veryfront Perf] %c" + label + ": %c" + duration.toFixed(2) + "ms",\n "color: #888",\n duration > 100 ? "color: #f00; font-weight: bold" : "color: #0a0"\n );\n return duration;\n } : () => 0;\n return { DEBUG, log, logError, logBackgroundFetchFailure, perfStart, perfEnd };\n}\nfunction isAbortError(error) {\n return error?.name === "AbortError";\n}\nfunction resolveDocumentNavigationUrl(target, origin) {\n try {\n const url = new URL(target, origin);\n if (url.protocol === "http:" || url.protocol === "https:") return url.href;\n } catch (_) {\n }\n return null;\n}\nfunction getDocumentNonce(document2) {\n const element = document2.querySelector("script[nonce], style[nonce], link[nonce]");\n if (!element) return void 0;\n return element.nonce || element.getAttribute("nonce") || void 0;\n}\n\n// src/html/hydration-data-element.ts\nvar HYDRATION_DATA_ELEMENT_ID = "veryfront-hydration-data";\nfunction findServerHydrationDataElement(document2) {\n try {\n const matches = [...document2.querySelectorAll(`[id="${HYDRATION_DATA_ELEMENT_ID}"]`)];\n if (matches.length !== 1) return null;\n const body = document2.body;\n if (!body) return null;\n const element = matches[0];\n if (body.firstElementChild !== element && element.parentElement !== body) return null;\n if (element.tagName?.toLowerCase() !== "script") return null;\n if (element.getAttribute("type")?.trim().toLowerCase() !== "application/json") return null;\n return element;\n } catch {\n return null;\n }\n}\n\n// src/html/hydration-script-builder/runtime/hydration-data.ts\nfunction readInitialHydrationData(document2) {\n try {\n const element = findServerHydrationDataElement(document2);\n return JSON.parse(element && element.textContent ? element.textContent : "{}") || {};\n } catch (_) {\n return {};\n }\n}\nfunction readDocumentDependencyPinningCacheKey(initialHydrationData2) {\n return typeof initialHydrationData2.dependencyPinningCacheKey === "string" && initialHydrationData2.dependencyPinningCacheKey.startsWith("on:") ? initialHydrationData2.dependencyPinningCacheKey : null;\n}\n\n// src/html/hydration-script-builder/runtime/snapshot-modules.ts\nvar RECOVERY_STATE_KEY = "__VF_DEPENDENCY_SNAPSHOT_RECOVERY_STARTED__";\nasync function isDependencySnapshotConflictResponse(response) {\n if (!response || response.status !== 409) return false;\n try {\n const clone = response.clone?.() ?? response;\n const body = (await clone.text?.() ?? "").trim();\n return body === "Unknown dependency snapshot" || body === "export default null; // Unknown dependency snapshot";\n } catch (_) {\n return false;\n }\n}\nfunction createSnapshotModuleImporter(deps) {\n async function recoverFromSnapshotBoundModuleFailure(moduleUrl, allowDocumentReload = true) {\n try {\n const parsedUrl = new URL(moduleUrl, "http://veryfront.local");\n const snapshotKeys = parsedUrl.searchParams.getAll("pins");\n const pathMatch = parsedUrl.pathname.match(\n /^\\/_vf_modules\\/_pins\\/([^/]+)(?:\\/|$)/\n );\n if (pathMatch) {\n try {\n snapshotKeys.push(decodeURIComponent(pathMatch[1]));\n } catch (_) {\n return false;\n }\n }\n if (snapshotKeys.length !== 1 || !/^on:[A-Za-z0-9._-]+$/.test(snapshotKeys[0])) return false;\n const response = await deps.fetchModule(moduleUrl, { cache: "no-store" });\n if (!await isDependencySnapshotConflictResponse(response)) return false;\n if (!allowDocumentReload) return true;\n if (deps.recoveryState[RECOVERY_STATE_KEY] === true) return true;\n deps.recoveryState[RECOVERY_STATE_KEY] = true;\n try {\n deps.reloadDocument();\n } catch (_) {\n delete deps.recoveryState[RECOVERY_STATE_KEY];\n return false;\n }\n return true;\n } catch (_) {\n return false;\n }\n }\n async function importSnapshotBoundModule(moduleUrl, allowDocumentReload = true) {\n try {\n return await deps.importModule(moduleUrl);\n } catch (error) {\n const snapshotConflict = await recoverFromSnapshotBoundModuleFailure(\n moduleUrl,\n allowDocumentReload\n );\n if (snapshotConflict && !allowDocumentReload) {\n const conflictError = new Error(\n "Dependency snapshot is unavailable during speculative module prefetch"\n );\n conflictError.name = "DependencySnapshotConflictError";\n conflictError.dependencySnapshotConflict = true;\n conflictError.cause = error;\n throw conflictError;\n }\n throw error;\n }\n }\n return { importSnapshotBoundModule, recoverFromSnapshotBoundModuleFailure };\n}\nfunction isDependencySnapshotConflict(error) {\n return Boolean(error?.dependencySnapshotConflict);\n}\n\n// src/utils/version-constant.ts\nvar VERSION = "0.1.1209";\n\n// src/html/hydration-script-builder/runtime/module-urls.ts\nfunction appendQueryParam(url, key, value) {\n return url + (url.includes("?") ? "&" : "?") + key + "=" + value;\n}\nfunction appendDependencyPinningVersion(url, moduleData) {\n const pinKey = moduleData && moduleData.dependencyPinningCacheKey;\n if (typeof pinKey !== "string" || !pinKey.startsWith("on:")) return url;\n const hashIndex = url.indexOf("#");\n const hash = hashIndex >= 0 ? url.slice(hashIndex) : "";\n const withoutHash = hashIndex >= 0 ? url.slice(0, hashIndex) : url;\n const queryIndex = withoutHash.indexOf("?");\n const base = queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash;\n const params = new URLSearchParams(queryIndex >= 0 ? withoutHash.slice(queryIndex + 1) : "");\n const modulePrefix = "/_vf_modules/";\n const prefixIndex = base.indexOf(modulePrefix);\n const origin = prefixIndex >= 0 ? base.slice(0, prefixIndex) : "";\n if (prefixIndex >= 0 && (origin === "" || /^https?:\\/\\/[^/]+$/i.test(origin))) {\n const pathStart = prefixIndex + modulePrefix.length;\n let modulePath = base.slice(pathStart);\n if (modulePath.startsWith("_pins/")) {\n const existingKeyEnd = modulePath.indexOf("/", "_pins/".length);\n const encodedExistingKey = existingKeyEnd < 0 ? modulePath.slice("_pins/".length) : modulePath.slice("_pins/".length, existingKeyEnd);\n let existingKey;\n try {\n existingKey = decodeURIComponent(encodedExistingKey);\n } catch {\n existingKey = void 0;\n }\n if (existingKey && /^on:[A-Za-z0-9._-]+$/.test(existingKey)) {\n if (existingKeyEnd < 0) return url;\n modulePath = modulePath.slice(existingKeyEnd + 1);\n }\n }\n params.delete("pins");\n const query = params.toString();\n return base.slice(0, pathStart) + "_pins/" + encodeURIComponent(pinKey) + "/" + modulePath + (query ? "?" + query : "") + hash;\n }\n params.set("pins", pinKey);\n return base + "?" + params.toString() + hash;\n}\nfunction componentCacheKey(path, moduleData) {\n const pinKey = moduleData && moduleData.dependencyPinningCacheKey;\n return typeof pinKey === "string" && pinKey.startsWith("on:") ? path + "|vf_pins|" + pinKey : path;\n}\nfunction normalizeReleaseAssetModulePath(path) {\n return String(path || "").replace(/^\\/?_vf_modules\\//, "").replace(/^\\/+/, "").replace(/[?#].*$/, "");\n}\nfunction buildPinnedRscModuleUrl(path, moduleData) {\n let moduleUrl = "/_veryfront/rsc/module?rel=" + encodeURIComponent(path);\n const pinKey = moduleData && moduleData.dependencyPinningCacheKey;\n if (typeof pinKey === "string" && pinKey.startsWith("on:")) {\n moduleUrl += "&pins=" + encodeURIComponent(pinKey);\n }\n return moduleUrl;\n}\nfunction buildPageDataEndpoint(path, origin) {\n const targetUrl = new URL(path, origin);\n const normalizedPath = targetUrl.pathname === "/" ? "" : targetUrl.pathname.replace(/^\\//, "");\n const endpointUrl = new URL(\n "/_veryfront/page-data/" + normalizedPath + ".json",\n origin\n );\n endpointUrl.search = targetUrl.search;\n return endpointUrl.pathname + endpointUrl.search;\n}\nfunction pageDataCacheIdentity(path, documentDependencyPinningCacheKey2) {\n return documentDependencyPinningCacheKey2 ? documentDependencyPinningCacheKey2 + "|path:" + path : path;\n}\nfunction assertPageDataMatchesDocumentSnapshot(path, data, documentDependencyPinningCacheKey2) {\n if (!documentDependencyPinningCacheKey2) return data;\n if (data && data.dependencyPinningCacheKey === documentDependencyPinningCacheKey2) {\n return data;\n }\n const error = new Error("Page data dependency snapshot does not match the document");\n error.status = 409;\n error.dependencySnapshotMismatch = true;\n error.path = path;\n throw error;\n}\n\n// src/html/hydration-script-builder/runtime/component-loader.ts\nvar VERYFRONT_RUNTIME_VERSION = VERSION;\nfunction createComponentLoader(deps) {\n const { window, moduleServerUrl: moduleServerUrl2 } = deps;\n const { DEBUG, log, logError } = deps.logging;\n const componentCache = /* @__PURE__ */ new Map();\n const loadingPromises = /* @__PURE__ */ new Map();\n let releaseId = null;\n let releaseAssetModules = null;\n let studioEmbed = false;\n let hmrRefreshTimestamp = null;\n function clearComponentCache(path) {\n if (!path) {\n componentCache.clear();\n loadingPromises.clear();\n log("Cleared all component caches");\n return;\n }\n for (const key of componentCache.keys()) {\n if (key === path || key.startsWith(path + "|vf_pins|")) {\n componentCache.delete(key);\n }\n }\n for (const key of loadingPromises.keys()) {\n if (key === path || key.startsWith(path + "|vf_pins|")) {\n loadingPromises.delete(key);\n }\n }\n log("Cleared component cache for:", path);\n }\n function setReleaseId(value) {\n releaseId = typeof value === "string" && value ? value : null;\n window.__veryfrontReleaseId = releaseId;\n }\n function appendReleaseModuleVersion(url) {\n if (!releaseId || url.includes("vf_release=")) return url;\n let versionedUrl = appendQueryParam(url, "vf_release", encodeURIComponent(releaseId));\n versionedUrl = appendQueryParam(\n versionedUrl,\n "vf_runtime",\n encodeURIComponent(VERYFRONT_RUNTIME_VERSION)\n );\n return versionedUrl;\n }\n function setReleaseAssetModules(value) {\n releaseAssetModules = value && typeof value === "object" && !Array.isArray(value) ? value : null;\n window.__veryfrontReleaseAssetModules = releaseAssetModules;\n }\n function resolveReleaseAssetModuleUrl(path) {\n if (!releaseAssetModules || studioEmbed || hmrRefreshTimestamp) return null;\n const key = normalizeReleaseAssetModulePath(path);\n if (releaseAssetModules[key]) return releaseAssetModules[key];\n const withoutExt = key.replace(/\\.(tsx|ts|jsx|mdx|js|mjs)$/, "");\n const extensions = [".tsx", ".ts", ".jsx", ".mdx", ".js"];\n for (const ext of extensions) {\n const candidate = withoutExt + ext;\n if (releaseAssetModules[candidate]) return releaseAssetModules[candidate];\n }\n return null;\n }\n function pathToModuleUrl(path, embedInStudio, moduleData) {\n const releaseAssetUrl = resolveReleaseAssetModuleUrl(path);\n if (releaseAssetUrl) return releaseAssetUrl;\n const pattern = /(pages|components|app|lib|layouts|shared|features)\\/(.+)\\.(tsx|ts|jsx|mdx)$/;\n const match = path.match(new RegExp("/" + pattern.source)) || path.match(new RegExp("^" + pattern.source));\n let url;\n if (match) {\n url = moduleServerUrl2 + "/" + match[1] + "/" + match[2] + ".js";\n } else {\n const hasKnownExt = /\\.(tsx|ts|jsx|mdx|js|mjs)$/.test(path);\n url = moduleServerUrl2 + "/" + (hasKnownExt ? path.replace(/\\.(tsx|ts|jsx|mdx)$/, ".js") : path + ".js");\n }\n if (embedInStudio) url = appendQueryParam(url, "studio_embed", "true");\n if (hmrRefreshTimestamp) url = appendQueryParam(url, "t", hmrRefreshTimestamp);\n if (!embedInStudio && !hmrRefreshTimestamp) url = appendReleaseModuleVersion(url);\n url = appendDependencyPinningVersion(url, moduleData);\n return url;\n }\n function setStudioEmbed(value) {\n studioEmbed = value;\n window.__veryfrontStudioEmbed = value;\n }\n function setHMRRefreshTimestamp(timestamp) {\n hmrRefreshTimestamp = timestamp;\n window.__veryfrontHMRRefreshTimestamp = timestamp;\n }\n async function loadComponent(path, moduleData, options = {}) {\n if (!path) return null;\n const cacheKey = componentCacheKey(path, moduleData);\n if (componentCache.has(cacheKey)) {\n log("Component cached:", path);\n return componentCache.get(cacheKey);\n }\n const existingPromise = loadingPromises.get(cacheKey);\n if (existingPromise) return existingPromise;\n const loadPromise = (async () => {\n try {\n const moduleUrl = pathToModuleUrl(path, studioEmbed, moduleData);\n const start = DEBUG ? performance.now() : 0;\n log("Loading component:", moduleUrl);\n const module = await deps.snapshotModules.importSnapshotBoundModule(\n moduleUrl,\n options.allowDocumentReload !== false\n );\n const component = module.MDXLayout || module.MainLayout || module.default || module;\n if (DEBUG) {\n const duration = performance.now() - start;\n console.log(\n "[Veryfront Perf] %cimport:" + path.split("/").pop() + ": %c" + duration.toFixed(2) + "ms",\n "color: #888",\n duration > 50 ? "color: #f00; font-weight: bold" : "color: #0a0"\n );\n }\n componentCache.set(cacheKey, component);\n return component;\n } catch (error) {\n if (isDependencySnapshotConflict(error)) throw error;\n logError("Failed to load component:", path, error);\n return null;\n } finally {\n loadingPromises.delete(cacheKey);\n }\n })();\n loadingPromises.set(cacheKey, loadPromise);\n return loadPromise;\n }\n return {\n loadComponent,\n pathToModuleUrl,\n clearComponentCache,\n setStudioEmbed,\n setReleaseId,\n setReleaseAssetModules,\n setHMRRefreshTimestamp\n };\n}\n\n// src/html/hydration-script-builder/runtime/route-timing.ts\nvar MAX_ROUTE_TIMINGS = 100;\nvar MAX_SERVER_TIMING_LENGTH = 1024;\nfunction routeTimingNow() {\n return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();\n}\nfunction sanitizeServerTimingMetricName(name) {\n return String(name || "").trim().replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 128);\n}\nfunction sanitizeServerTimingHeader(value) {\n if (!value) return null;\n const metrics = [];\n const printable = String(value).replace(/[^\\x20-\\x7E]/g, " ").trim();\n if (!printable) return null;\n for (const item of printable.split(",")) {\n const segments = item.split(";").map((segment) => segment.trim()).filter(Boolean);\n const name = sanitizeServerTimingMetricName(segments[0]);\n if (!name) continue;\n for (const segment of segments.slice(1)) {\n const [key, rawValue = ""] = segment.split("=");\n if ((key ?? "").trim().toLowerCase() !== "dur") continue;\n const duration = Number(rawValue.trim().replace(/^"|"$/g, ""));\n if (!Number.isFinite(duration) || duration < 0) continue;\n metrics.push(name + ";dur=" + (Math.round(duration * 100) / 100).toFixed(2));\n break;\n }\n }\n const sanitized = metrics.join(", ");\n return sanitized ? sanitized.slice(0, MAX_SERVER_TIMING_LENGTH) : null;\n}\nfunction parseServerTimingMetrics(value) {\n const header = sanitizeServerTimingHeader(value);\n if (!header) return null;\n const metrics = {};\n for (const item of header.split(",")) {\n const segments = item.split(";").map((segment) => segment.trim()).filter(Boolean);\n const name = sanitizeServerTimingMetricName(segments[0]);\n if (!name) continue;\n for (const segment of segments.slice(1)) {\n const [key, rawValue = ""] = segment.split("=");\n if ((key ?? "").trim().toLowerCase() !== "dur") continue;\n const duration = Number(rawValue.trim().replace(/^"|"$/g, ""));\n if (Number.isFinite(duration) && duration >= 0) {\n metrics[name] = Math.round(duration * 100) / 100;\n }\n }\n }\n return Object.keys(metrics).length ? metrics : null;\n}\nfunction readResponseServerTiming(response) {\n try {\n return sanitizeServerTimingHeader(response.headers?.get("server-timing"));\n } catch (_) {\n return null;\n }\n}\nfunction roundRouteTimingValue(value) {\n return Math.round(value * 100) / 100;\n}\nfunction extractResourceTiming(entry) {\n const fields = [\n "startTime",\n "requestStart",\n "responseStart",\n "responseEnd",\n "duration",\n "transferSize",\n "encodedBodySize",\n "decodedBodySize"\n ];\n const timing = {};\n for (const field of fields) {\n const value = entry?.[field];\n if (typeof value === "number" && Number.isFinite(value) && value >= 0) {\n timing[field] = roundRouteTimingValue(value);\n }\n }\n return Object.keys(timing).length ? timing : null;\n}\nfunction createRouteTimingRecorder(window, logging2) {\n const { log } = logging2;\n function emitRouteTiming(phase, path, startedAt, detail = {}) {\n const entry = {\n phase,\n path,\n duration: Math.max(0, routeTimingNow() - startedAt),\n timestamp: Date.now(),\n ...detail\n };\n const timings = Array.isArray(window.__veryfrontRouteTimings) ? window.__veryfrontRouteTimings : [];\n timings.push(entry);\n if (timings.length > MAX_ROUTE_TIMINGS) {\n timings.splice(0, timings.length - MAX_ROUTE_TIMINGS);\n }\n window.__veryfrontRouteTimings = timings;\n try {\n window.dispatchEvent(new CustomEvent("veryfront:route-timing", { detail: entry }));\n } catch (_) {\n }\n log("Route timing:", entry);\n return entry;\n }\n function getPageDataResourceTiming(endpoint, fetchStartedAt) {\n try {\n if (typeof performance === "undefined" || typeof performance.getEntriesByName !== "function") {\n return null;\n }\n const href = new URL(endpoint, window.location.href).href;\n const entries = performance.getEntriesByName(href, "resource");\n if (!entries.length) return null;\n for (let index = entries.length - 1; index >= 0; index--) {\n const entry = entries[index];\n const responseEnd = entry?.responseEnd;\n if (typeof responseEnd === "number" && Number.isFinite(responseEnd) && responseEnd + 1 >= fetchStartedAt) {\n return extractResourceTiming(entry);\n }\n }\n return null;\n } catch (_) {\n return null;\n }\n }\n function buildPageDataTimingDetail(response, endpoint, fetchStartedAt, source) {\n const detail = { source, status: response.status };\n const serverTiming = readResponseServerTiming(response);\n if (serverTiming) {\n detail.serverTiming = serverTiming;\n const serverTimingMetrics = parseServerTimingMetrics(serverTiming);\n if (serverTimingMetrics) detail.serverTimingMetrics = serverTimingMetrics;\n }\n const resourceTiming = getPageDataResourceTiming(response.url || endpoint, fetchStartedAt);\n if (resourceTiming) detail.resourceTiming = resourceTiming;\n return detail;\n }\n return { emitRouteTiming, buildPageDataTimingDetail };\n}\n\n// src/html/managed-head-protocol.ts\nvar HEAD_PROVENANCE_ATTRIBUTE = "data-vf-head";\nvar HEAD_LEGACY_MANAGED_ATTRIBUTE = "data-veryfront-managed";\nvar HEAD_CONTENT_HASH_ATTRIBUTE = "data-vf-hash";\nvar HEAD_REACT_MANAGED_ATTRIBUTE = "data-vf-react-head";\nvar HEAD_REACT_OWNER_ATTRIBUTE = "data-vf-react-head-owner";\nvar HEAD_ROUTE_MANAGED_ATTRIBUTE = "data-vf-route-head";\nvar HEAD_SERVER_COMMIT_ATTRIBUTE = "data-vf-server-head-commit";\nvar HEAD_SHELL_PROVENANCE_ATTRIBUTE = "data-vf-shell-head";\nvar HEAD_SSR_PAYLOAD_ATTRIBUTE = "data-vf-ssr-head";\nvar MAX_MANAGED_HEAD_BYTES = 2 * 1024 * 1024;\nvar MAX_MANAGED_HEAD_PAYLOAD_BYTES = MAX_MANAGED_HEAD_BYTES * 2;\nvar SINGLETON_META_KEYS = /* @__PURE__ */ new Set([\n "description",\n "robots",\n "viewport",\n "referrer",\n "color-scheme",\n "application-name",\n "generator",\n "og:title",\n "og:description",\n "og:url",\n "og:type",\n "og:site_name",\n "og:locale",\n "twitter:card",\n "twitter:site",\n "twitter:creator",\n "twitter:title",\n "twitter:description",\n "twitter:image",\n "twitter:image:alt"\n]);\nvar SINGLETON_LINK_RELS = /* @__PURE__ */ new Set([\n "canonical",\n "manifest",\n "amphtml"\n]);\nvar MAX_HEAD_ATTRIBUTE_VALUE_BYTES = 64 * 1024;\nvar MAX_HEAD_ATTRIBUTE_BYTES = 1024 * 1024;\nvar MAX_HEAD_CONTENT_BYTES = 1024 * 1024;\nvar headTextEncoder = new TextEncoder();\nvar BOOLEAN_HEAD_ATTRIBUTES = /* @__PURE__ */ new Set([\n "async",\n "defer",\n "disabled",\n "itemscope",\n "nomodule"\n]);\nfunction isHeadFrameworkAttribute(name) {\n switch (name.toLowerCase()) {\n case HEAD_PROVENANCE_ATTRIBUTE:\n case HEAD_LEGACY_MANAGED_ATTRIBUTE:\n case HEAD_CONTENT_HASH_ATTRIBUTE:\n case HEAD_REACT_MANAGED_ATTRIBUTE:\n case HEAD_REACT_OWNER_ATTRIBUTE:\n case HEAD_ROUTE_MANAGED_ATTRIBUTE:\n case HEAD_SERVER_COMMIT_ATTRIBUTE:\n case HEAD_SHELL_PROVENANCE_ATTRIBUTE:\n case HEAD_SSR_PAYLOAD_ATTRIBUTE:\n return true;\n default:\n return false;\n }\n}\nfunction normalizeHeadIdentityValue(value) {\n const normalized = value?.trim().toLowerCase();\n return normalized || void 0;\n}\nfunction readOwnString(record, key) {\n try {\n const descriptor = Reflect.getOwnPropertyDescriptor(record, key);\n return descriptor && !descriptor.get && !descriptor.set && "value" in descriptor && typeof descriptor.value === "string" ? descriptor.value : void 0;\n } catch {\n return void 0;\n }\n}\nfunction headMetaSingletonKeyFromRecord(meta) {\n if (readOwnString(meta, "charset") !== void 0) return "meta:charset";\n const key = normalizeHeadIdentityValue(\n readOwnString(meta, "property") ?? readOwnString(meta, "name")\n );\n if (!key) return void 0;\n if (key === "theme-color") {\n return `meta:theme-color:${readOwnString(meta, "media")?.trim() ?? ""}`;\n }\n return SINGLETON_META_KEYS.has(key) ? `meta:${key}` : void 0;\n}\nfunction headLinkSingletonKeyFromRecord(link) {\n const rel = normalizeHeadIdentityValue(readOwnString(link, "rel"));\n return rel && SINGLETON_LINK_RELS.has(rel) ? `link:${rel}` : void 0;\n}\n\n// src/html/client-head-manager.ts\nvar HEAD_MANAGER_STATE_SYMBOL = /* @__PURE__ */ Symbol.for(\n "veryfront.client-head-manager.v2"\n);\nvar CROSS_PAGE_PRESERVED_SINGLETON_KEYS = /* @__PURE__ */ new Set([\n "meta:viewport",\n "link:manifest"\n]);\nfunction getClientHeadManagerState() {\n const globalState = globalThis;\n return globalState[HEAD_MANAGER_STATE_SYMBOL] ?? (globalState[HEAD_MANAGER_STATE_SYMBOL] = {\n documents: /* @__PURE__ */ new WeakMap()\n });\n}\nfunction readElementAttributes(element) {\n const attributes = [];\n for (const attribute of element.attributes) {\n const name = attribute.name.toLowerCase();\n if (isHeadFrameworkAttribute(name)) continue;\n const nonce = name === "nonce" && "nonce" in element ? element.nonce : "";\n const value = BOOLEAN_HEAD_ATTRIBUTES.has(name) ? "" : nonce || attribute.value;\n attributes.push([name, value]);\n }\n return attributes.sort(([left], [right]) => left.localeCompare(right));\n}\nfunction elementSingletonKey(element) {\n const tagName = element.tagName.toLowerCase();\n if (tagName === "title") return "title";\n const attributes = Object.fromEntries(readElementAttributes(element));\n if (tagName === "meta") return headMetaSingletonKeyFromRecord(attributes);\n if (tagName === "link") return headLinkSingletonKeyFromRecord(attributes);\n return void 0;\n}\nfunction promoteToShellHeadBaseline(element) {\n for (const attribute of [...element.attributes]) {\n if (isHeadFrameworkAttribute(attribute.name)) {\n element.removeAttribute(attribute.name);\n }\n }\n element.setAttribute(HEAD_SHELL_PROVENANCE_ATTRIBUTE, "true");\n}\nfunction isCrossPagePreservedSingleton(element, singletonKey = elementSingletonKey(element)) {\n return element.parentElement !== null && singletonKey !== void 0 && CROSS_PAGE_PRESERVED_SINGLETON_KEYS.has(singletonKey);\n}\nfunction isFrameworkOwnedHeadElement(element) {\n return element.getAttribute(HEAD_PROVENANCE_ATTRIBUTE) === "true" || element.getAttribute(HEAD_REACT_MANAGED_ATTRIBUTE) === "true" || element.getAttribute(HEAD_LEGACY_MANAGED_ATTRIBUTE) === "1" || element.getAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE) === "true" || element.getAttribute(HEAD_SHELL_PROVENANCE_ATTRIBUTE) === "true";\n}\nfunction retireFrameworkHeadElement(element) {\n if (isCrossPagePreservedSingleton(element)) {\n promoteToShellHeadBaseline(element);\n return;\n }\n element.remove();\n}\nfunction retireClientHeadOwnership(targetDocument) {\n const manager = getClientHeadManagerState().documents.get(targetDocument);\n if (manager) {\n manager.retire();\n return;\n }\n for (const element of [...targetDocument.head?.children ?? []]) {\n if (isFrameworkOwnedHeadElement(element)) retireFrameworkHeadElement(element);\n }\n}\n\n// src/html/client-route-head.ts\nfunction updateRouteTitle(title, targetDocument = document) {\n if (typeof title !== "string" || !title) return;\n const titles = [...targetDocument.head.querySelectorAll("title")];\n if (titles.some((element) => element.getAttribute(HEAD_LEGACY_MANAGED_ATTRIBUTE) === "1")) {\n return;\n }\n let titleElement = titles.find(\n (element) => element.getAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE) === "true"\n );\n for (const element of titles) {\n if (element !== titleElement) element.remove();\n }\n if (!titleElement) {\n titleElement = targetDocument.createElement("title");\n titleElement.setAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE, "true");\n targetDocument.head.appendChild(titleElement);\n }\n titleElement.textContent = title;\n}\nfunction updateRouteMetaTag(targetDocument, selector, attributeName, attributeValue, content) {\n const matches = [...targetDocument.head.querySelectorAll(selector)];\n if (matches.some((element) => element.getAttribute(HEAD_LEGACY_MANAGED_ATTRIBUTE) === "1")) {\n return;\n }\n let metaTag = matches.find(\n (element) => element.getAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE) === "true"\n );\n if (!metaTag) {\n metaTag = targetDocument.createElement("meta");\n metaTag.setAttribute(attributeName, attributeValue);\n metaTag.setAttribute(HEAD_ROUTE_MANAGED_ATTRIBUTE, "true");\n targetDocument.head.appendChild(metaTag);\n }\n metaTag.setAttribute("content", content);\n}\nfunction updateRouteMetaTags(metadata, targetDocument = document) {\n if (typeof metadata.description === "string" && metadata.description) {\n updateRouteMetaTag(\n targetDocument,\n \'meta[name="description"]\',\n "name",\n "description",\n metadata.description\n );\n }\n if (typeof metadata.ogTitle === "string" && metadata.ogTitle) {\n updateRouteMetaTag(\n targetDocument,\n \'meta[property="og:title"]\',\n "property",\n "og:title",\n metadata.ogTitle\n );\n }\n}\nfunction handoffClientRouteMetadata(metadata, targetDocument = document) {\n const retainedTitle = targetDocument.title;\n retireClientHeadOwnership(targetDocument);\n updateRouteTitle(\n typeof metadata.title === "string" && metadata.title ? metadata.title : retainedTitle,\n targetDocument\n );\n updateRouteMetaTags(metadata, targetDocument);\n}\n\n// src/html/hydration-script-builder/runtime/router.ts\nvar FETCH_TIMEOUT_MS = 1e4;\nvar MAX_RETRIES = 2;\nvar MAX_CACHE_SIZE = 50;\nvar CACHE_TTL_MS = 5 * 60 * 1e3;\nvar BACKGROUND_REFRESH_INTERVAL_MS = 30 * 1e3;\nvar PREFETCH_DELAY_MS = 100;\nvar MAX_PREFETCH_PATHS = 100;\nvar IDLE_PREFETCH_DELAY_MS = 1200;\nvar IDLE_PREFETCH_MAX_LINKS = 4;\nvar VIEWPORT_PREFETCH_MAX_LINKS = 8;\nvar PAGE_DATA_PREFETCH_CONCURRENCY = 2;\nvar VIEWPORT_PREFETCH_ROOT_MARGIN = "200px";\nvar MAX_SCROLL_POSITIONS = 100;\nfunction createRouterRuntime(deps) {\n const { env: env2, logging: logging2, routeTiming: routeTiming2, componentLoader: componentLoader2, snapshotModules: snapshotModules2 } = deps;\n const { window, document: document2, React: React2, RouterProvider: RouterProvider2, PageContextProvider: PageContextProvider2 } = env2;\n const { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = env2;\n const { log, logError, logBackgroundFetchFailure, perfStart, perfEnd } = logging2;\n const { emitRouteTiming, buildPageDataTimingDetail } = routeTiming2;\n const { loadComponent } = componentLoader2;\n const documentPinKey = deps.documentDependencyPinningCacheKey;\n let hydrationResolve;\n let hydrationReject;\n const hydrationPromise = new Promise((resolve, reject) => {\n hydrationResolve = resolve;\n hydrationReject = reject;\n });\n let hydrationCompleted = false;\n let hydrationFailed = false;\n function signalHydrationComplete() {\n hydrationCompleted = true;\n hydrationResolve();\n log("Hydration complete signal received");\n }\n function signalHydrationFailed(error) {\n hydrationFailed = true;\n hydrationReject(error);\n logError("Hydration failed signal received:", error);\n }\n window.__veryfrontHydrationComplete = signalHydrationComplete;\n window.__veryfrontHydrationFailed = signalHydrationFailed;\n function pageDataCacheIdentity2(path) {\n return pageDataCacheIdentity(path, documentPinKey);\n }\n function navigateDocument(target) {\n const safeUrl = resolveDocumentNavigationUrl(target, window.location.origin);\n if (safeUrl) {\n window.location.href = safeUrl;\n return;\n }\n logError("Refusing an unsafe document navigation:", target);\n window.location.reload();\n }\n let clientBuildVersion = null;\n function checkVersionMismatch(newVersion) {\n if (!clientBuildVersion) {\n clientBuildVersion = newVersion;\n log("Build version initialized:", newVersion);\n return false;\n }\n if (newVersion.serverStart !== clientBuildVersion.serverStart) {\n log("Server restarted, reloading...", {\n old: clientBuildVersion.serverStart,\n new: newVersion.serverStart\n });\n return true;\n }\n if (newVersion.framework !== clientBuildVersion.framework) {\n log("Framework version changed, reloading...", {\n old: clientBuildVersion.framework,\n new: newVersion.framework\n });\n return true;\n }\n if (newVersion.projectUpdated && clientBuildVersion.projectUpdated && newVersion.projectUpdated !== clientBuildVersion.projectUpdated) {\n log("Project content updated, reloading...", {\n old: clientBuildVersion.projectUpdated,\n new: newVersion.projectUpdated\n });\n return true;\n }\n return false;\n }\n const pageDataCache = /* @__PURE__ */ new Map();\n const pendingPageDataFetches = /* @__PURE__ */ new Map();\n const backgroundRefreshTimestamps = /* @__PURE__ */ new Map();\n function getCachedPageData(path) {\n const cacheIdentity = pageDataCacheIdentity2(path);\n const entry = pageDataCache.get(cacheIdentity);\n if (!entry) return null;\n if (Date.now() - entry.timestamp < CACHE_TTL_MS) return entry.data;\n pageDataCache.delete(cacheIdentity);\n backgroundRefreshTimestamps.delete(cacheIdentity);\n return null;\n }\n function setCachedPageData(path, data) {\n const cacheIdentity = pageDataCacheIdentity2(path);\n if (pageDataCache.size >= MAX_CACHE_SIZE) {\n const oldest = pageDataCache.keys().next().value;\n if (oldest) {\n pageDataCache.delete(oldest);\n backgroundRefreshTimestamps.delete(oldest);\n }\n }\n pageDataCache.set(cacheIdentity, { data, timestamp: Date.now() });\n }\n const scrollPositions = /* @__PURE__ */ new Map();\n function saveScrollPosition(path) {\n if (scrollPositions.size >= MAX_SCROLL_POSITIONS) {\n const oldest = scrollPositions.keys().next().value;\n if (oldest) scrollPositions.delete(oldest);\n }\n scrollPositions.set(path, window.scrollY);\n }\n function restoreScrollPosition(path) {\n const savedY = scrollPositions.get(path);\n if (savedY === void 0) return false;\n requestAnimationFrame(() => window.scrollTo(0, savedY));\n return true;\n }\n let progressBar = null;\n let progressTimeout = null;\n function showNavigationProgress() {\n if (!progressBar) {\n progressBar = document2.createElement("div");\n progressBar.id = "vf-nav-progress";\n progressBar.style.cssText = "position:fixed;top:0;left:0;height:3px;width:0;background:linear-gradient(90deg,#0066ff,#00aaff);z-index:99999;transition:width 0.3s ease-out,opacity 0.2s;opacity:1;";\n document2.body.prepend(progressBar);\n }\n progressBar.style.opacity = "1";\n progressBar.style.width = "30%";\n progressTimeout = setTimeout2(() => {\n if (progressBar?.style) progressBar.style.width = "70%";\n }, 300);\n document2.body.setAttribute("aria-busy", "true");\n }\n function hideNavigationProgress() {\n if (progressTimeout) {\n clearTimeout2(progressTimeout);\n progressTimeout = null;\n }\n if (progressBar) {\n progressBar.style.width = "100%";\n setTimeout2(() => {\n if (!progressBar) return;\n progressBar.style.opacity = "0";\n setTimeout2(() => {\n if (progressBar) progressBar.style.width = "0";\n }, 200);\n }, 150);\n }\n document2.body.removeAttribute("aria-busy");\n }\n let currentAbortController = null;\n function sleep(ms) {\n return new Promise((resolve) => setTimeout2(resolve, ms));\n }\n async function fetchWithRetry(url, options, maxRetries = MAX_RETRIES) {\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n const controller = new AbortController();\n const callerSignal = options.signal;\n const abortFromCaller = () => controller.abort();\n if (callerSignal?.aborted) controller.abort();\n callerSignal?.addEventListener("abort", abortFromCaller, { once: true });\n const timeout = setTimeout2(() => controller.abort(), FETCH_TIMEOUT_MS);\n try {\n const response = await env2.fetch(url, { ...options, signal: controller.signal });\n clearTimeout2(timeout);\n callerSignal?.removeEventListener("abort", abortFromCaller);\n if (response.ok) return response;\n if (response.status >= 500 && attempt < maxRetries) {\n log("Server error, retrying...", response.status);\n await sleep(Math.pow(2, attempt) * 500);\n continue;\n }\n return response;\n } catch (error) {\n clearTimeout2(timeout);\n callerSignal?.removeEventListener("abort", abortFromCaller);\n if (error.name === "AbortError" && callerSignal?.aborted) throw error;\n if (attempt === maxRetries) throw error;\n log("Fetch failed, retrying...", error.message);\n await sleep(Math.pow(2, attempt) * 500);\n }\n }\n throw new Error("Failed to fetch page data");\n }\n async function fetchPageDataFresh(path, signal, options = {}) {\n const {\n triggerReloadOnVersionMismatch = false,\n recordRouteTiming = false,\n timingSource = "network"\n } = options;\n const endpoint = buildPageDataEndpoint(path, window.location.origin);\n const startedAt = recordRouteTiming ? routeTimingNow() : 0;\n log("Fetching page data:", path);\n perfStart("fetch:" + path);\n const headers = options.prefetch ? { "X-Veryfront-Prefetch": "1" } : { "X-Veryfront-Navigation": "spa" };\n if (documentPinKey) {\n headers["X-Veryfront-Dependency-Pins"] = documentPinKey;\n }\n const response = await fetchWithRetry(endpoint, {\n headers,\n signal\n }, options.prefetch ? 0 : MAX_RETRIES);\n if (!response.ok) {\n perfEnd("fetch:" + path);\n if (recordRouteTiming) {\n emitRouteTiming(\n "page-data",\n path,\n startedAt,\n buildPageDataTimingDetail(response, endpoint, startedAt, timingSource)\n );\n }\n const error = new Error("Failed to fetch page data: " + response.status);\n error.status = response.status;\n throw error;\n }\n perfStart("parse:" + path);\n const data = assertPageDataMatchesDocumentSnapshot(\n path,\n await response.json(),\n documentPinKey\n );\n perfEnd("parse:" + path);\n perfEnd("fetch:" + path);\n if (recordRouteTiming) {\n emitRouteTiming(\n "page-data",\n path,\n startedAt,\n buildPageDataTimingDetail(response, endpoint, startedAt, timingSource)\n );\n }\n if (triggerReloadOnVersionMismatch) {\n const checkedData = handlePageDataVersionMismatch(path, data);\n if (checkedData !== data) return checkedData;\n }\n setCachedPageData(path, data);\n return data;\n }\n function handlePageDataVersionMismatch(path, data) {\n if (data.buildVersion && checkVersionMismatch(data.buildVersion)) {\n log("Version mismatch detected, performing full page reload to:", path);\n navigateDocument(path);\n return new Promise(() => {\n });\n }\n return data;\n }\n function startPageDataFetch(path, signal, options = {}) {\n const cacheIdentity = pageDataCacheIdentity2(path);\n const request = fetchPageDataFresh(path, signal, options).finally(() => {\n if (options.trackPending !== false && pendingPageDataFetches.get(cacheIdentity) === request) {\n pendingPageDataFetches.delete(cacheIdentity);\n }\n });\n if (options.trackPending !== false) {\n pendingPageDataFetches.set(cacheIdentity, request);\n }\n return request;\n }\n function fetchPageDataDeduped(path) {\n const pending = pendingPageDataFetches.get(pageDataCacheIdentity2(path));\n if (pending) return pending;\n return startPageDataFetch(path, null);\n }\n function refreshPageDataInBackground(path) {\n const cacheIdentity = pageDataCacheIdentity2(path);\n const lastRefreshAt = backgroundRefreshTimestamps.get(cacheIdentity) || 0;\n const now = Date.now();\n if (now - lastRefreshAt < BACKGROUND_REFRESH_INTERVAL_MS) return;\n backgroundRefreshTimestamps.set(cacheIdentity, now);\n fetchPageDataDeduped(path).catch((error) => {\n logBackgroundFetchFailure("Stale page data refresh", path, error);\n });\n }\n async function fetchPageDataForNavigation(path, signal) {\n const startedAt = routeTimingNow();\n const cached = getCachedPageData(path);\n if (cached) {\n log("Using cached page data:", path);\n refreshPageDataInBackground(path);\n emitRouteTiming("page-data", path, startedAt, { source: "cache" });\n return cached;\n }\n const pending = pendingPageDataFetches.get(pageDataCacheIdentity2(path));\n if (pending) {\n log("Reusing pending page data fetch for navigation:", path);\n const data = await pending;\n emitRouteTiming("page-data", path, startedAt, { source: "deduped" });\n return handlePageDataVersionMismatch(path, data);\n }\n return startPageDataFetch(path, signal, {\n triggerReloadOnVersionMismatch: true,\n recordRouteTiming: true,\n timingSource: "network"\n });\n }\n function fetchPageDataForPrefetch(path, signal) {\n if (getCachedPageData(path)) return Promise.resolve();\n return startPageDataFetch(path, signal, { prefetch: true, trackPending: false }).then((data) => preloadModulesForPageData(data, path)).catch((error) => {\n if (!isAbortError(error)) {\n logBackgroundFetchFailure("Page data prefetch", path, error);\n }\n throw error;\n });\n }\n let currentPath = window.location.pathname;\n let isNavigating = false;\n async function navigateSPA(href, historyMode = "push", restoreScroll = false) {\n currentAbortController?.abort();\n if (isNavigating) return;\n isNavigating = true;\n const [navigationPath] = href.split("#");\n removeQueuedPrefetch(navigationPath || href);\n abortActiveSpeculativePrefetches();\n currentAbortController = new AbortController();\n const signal = currentAbortController.signal;\n const navigationStartedAt = routeTimingNow();\n showNavigationProgress();\n perfStart("nav:total:" + href);\n try {\n log("SPA navigating to:", href);\n saveScrollPosition(currentPath);\n const [path, hash] = href.split("#");\n const targetPath = path || currentPath;\n perfStart("nav:fetchData:" + href);\n const pageData = await fetchPageDataForNavigation(targetPath, signal);\n perfEnd("nav:fetchData:" + href);\n if (signal.aborted) return;\n if (pageData && pageData.redirect && typeof pageData.redirect.destination === "string") {\n const redirectUrl = resolveDocumentNavigationUrl(\n pageData.redirect.destination,\n window.location.origin\n );\n if (redirectUrl) {\n log("SPA navigation redirect -> " + redirectUrl);\n window.location.href = redirectUrl;\n return;\n }\n }\n if (historyMode === "push") {\n window.history.pushState({ pageData, scrollY: 0 }, "", href);\n } else if (historyMode === "replace") {\n window.history.replaceState({ pageData, scrollY: 0 }, "", href);\n }\n currentPath = targetPath;\n router.pathname = targetPath;\n router.query = Object.fromEntries(new URLSearchParams(window.location.search));\n router.params = flattenRouteParams(pageData.params);\n perfStart("nav:render:" + href);\n await renderPageFromData(pageData, targetPath);\n perfEnd("nav:render:" + href);\n if (restoreScroll) {\n restoreScrollPosition(targetPath);\n } else if (hash) {\n requestAnimationFrame(() => {\n const target = document2.getElementById(hash);\n if (target) {\n target.scrollIntoView({ behavior: "smooth" });\n return;\n }\n window.scrollTo(0, 0);\n });\n } else {\n window.scrollTo(0, 0);\n }\n hideNavigationProgress();\n perfEnd("nav:total:" + href);\n emitRouteTiming("total", targetPath, navigationStartedAt, {\n href,\n historyMode,\n restoreScroll\n });\n log("SPA navigation complete");\n } catch (error) {\n hideNavigationProgress();\n if (error.name === "AbortError") {\n log("Navigation aborted");\n return;\n }\n logError("SPA navigation failed:", error.message);\n if (error.status === 404) {\n logError("Page not found:", href);\n }\n navigateDocument(href);\n } finally {\n isNavigating = false;\n currentAbortController = null;\n processPageDataPrefetchQueue();\n }\n }\n async function loadPageDataComponent(pageData, path, options = {}) {\n if (!pageData.isolatedClientPage) return loadComponent(path, pageData, options);\n const moduleUrl = buildPinnedRscModuleUrl(path, pageData);\n const module = await snapshotModules2.importSnapshotBoundModule(\n moduleUrl,\n options.allowDocumentReload !== false\n );\n return module.MDXLayout || module.MainLayout || module.default || module;\n }\n async function renderPageFromData(pageData, targetPath) {\n if (pageData.requiresFullDocumentNavigation) {\n throw new Error("Server layout requires full document navigation");\n }\n if (window.__veryfrontSetReleaseId) {\n window.__veryfrontSetReleaseId(pageData.releaseId || null);\n }\n if (window.__veryfrontSetReleaseAssetModules) {\n window.__veryfrontSetReleaseAssetModules(pageData.releaseAssetModules || null);\n }\n perfStart("render:loadAll");\n const allPaths = getPageDataModulePaths(pageData);\n const modulesStartedAt = routeTimingNow();\n const components = await Promise.all(\n allPaths.map((path) => loadPageDataComponent(pageData, path))\n );\n emitRouteTiming("modules", targetPath, modulesStartedAt, { count: allPaths.length });\n perfEnd("render:loadAll");\n const [PageComponent, ...rest] = components;\n const ErrorComponent = pageData.errorPath ? rest.pop() : null;\n const AppComponent = pageData.appPath ? rest.pop() : null;\n const LayoutComponents = rest;\n if (!PageComponent) {\n throw new Error("Failed to load page component: " + pageData.pagePath);\n }\n handoffClientRouteMetadata(\n pageData.frontmatter ?? {},\n document2\n );\n if (pageData.css) {\n const existingStyle = document2.getElementById("veryfront-spa-css");\n if (existingStyle) {\n existingStyle.textContent = pageData.css;\n } else {\n const styleEl = document2.createElement("style");\n const nonce = getDocumentNonce(document2);\n if (nonce) styleEl.setAttribute("nonce", nonce);\n styleEl.id = "veryfront-spa-css";\n styleEl.textContent = pageData.css;\n document2.head.appendChild(styleEl);\n }\n log("Injected CSS for SPA navigation", { cssLength: pageData.css.length });\n } else if (pageData.cssAction === "clear") {\n const existingStyle = document2.getElementById("veryfront-spa-css");\n if (existingStyle) {\n existingStyle.remove();\n log("Cleared SPA CSS for release stylesheet navigation");\n }\n }\n const normalizedParams = flattenRouteParams(pageData.params);\n let tree = React2.createElement(PageComponent, {\n ...pageData.props,\n params: normalizedParams\n });\n if (pageData.layouts?.length) {\n for (let i = pageData.layouts.length - 1; i >= 0; i--) {\n const layout = pageData.layouts[i];\n const LayoutComponent = LayoutComponents[i];\n if (!LayoutComponent || !layout) continue;\n const layoutProps = pageData.layoutProps?.[layout.path] || {};\n tree = React2.createElement(LayoutComponent, { ...layoutProps, children: tree });\n }\n }\n if (AppComponent) {\n tree = React2.createElement(AppComponent, { children: tree });\n log("Wrapped with App component for SPA navigation");\n }\n if (ErrorComponent) {\n class AppRouterErrorBoundary extends React2.Component {\n constructor(props) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n static getDerivedStateFromError(error) {\n return { hasError: true, error };\n }\n render() {\n if (this.state.hasError) {\n return React2.createElement(ErrorComponent, {\n error: this.state.error,\n reset: () => this.setState({ hasError: false, error: null })\n });\n }\n return this.props.children;\n }\n }\n tree = React2.createElement(AppRouterErrorBoundary, null, tree);\n }\n const headingsArray = pageData.headings || [];\n const pageContext = {\n slug: pageData.slug || "",\n path: pageData.pagePath || targetPath,\n params: normalizedParams,\n query: Object.fromEntries(new URLSearchParams(window.location.search)),\n frontmatter: pageData.frontmatter || {},\n data: pageData.props || {},\n headings: headingsArray,\n mdxHeadings: headingsArray\n };\n tree = React2.createElement(PageContextProvider2, { pageContext, children: tree });\n tree = React2.createElement(RouterProvider2, { router, children: tree });\n const container = pageData.isolatedClientPage ? document2.getElementById("veryfront-page-island") : document2.getElementById("root");\n if (!hydrationCompleted && !hydrationFailed) {\n log("Waiting for hydration to complete before SPA render...");\n try {\n await Promise.race([\n hydrationPromise,\n new Promise(\n (_, reject) => setTimeout2(() => reject(new Error("Hydration timeout")), 1e4)\n )\n ]);\n } catch (waitError) {\n log("Hydration wait failed:", waitError.message);\n }\n }\n if (container?.__reactRoot) {\n perfStart("render:reactRender");\n container.__reactRoot.render(tree);\n perfEnd("render:reactRender");\n log("Page re-rendered via SPA");\n scheduleRoutePrefetchRefresh();\n return;\n }\n if (hydrationFailed) {\n throw new Error(\n "React root not found - hydration failed, falling back to full page navigation"\n );\n }\n throw new Error("React root not found");\n }\n let prefetchTimeout = null;\n let currentHoverLink = null;\n let routePrefetchRefreshPending = false;\n let viewportPrefetchObserver = null;\n const observedPrefetchLinks = /* @__PURE__ */ new WeakSet();\n const prefetchedPaths = /* @__PURE__ */ new Set();\n const inFlightPrefetches = /* @__PURE__ */ new Set();\n const queuedPrefetchPaths = /* @__PURE__ */ new Set();\n const pageDataPrefetchQueue = [];\n const activePageDataPrefetchControllers = /* @__PURE__ */ new Map();\n function cancelScheduledPrefetch() {\n if (prefetchTimeout) {\n clearTimeout2(prefetchTimeout);\n prefetchTimeout = null;\n }\n currentHoverLink = null;\n }\n function getPageDataModulePaths(pageData) {\n const layoutPaths = (pageData.layouts || []).map((l) => l.path).filter(Boolean);\n const allPaths = [pageData.pagePath, ...layoutPaths].filter(Boolean);\n if (pageData.appPath) allPaths.push(pageData.appPath);\n if (pageData.errorPath) allPaths.push(pageData.errorPath);\n return allPaths;\n }\n function getCurrentRouteHref() {\n return window.location.pathname + window.location.search;\n }\n function getInternalRouteHrefFromLink(link) {\n if (!link || link.target === "_blank" || link.hasAttribute("download") || link.getAttribute("data-prefetch") === "false") {\n return null;\n }\n const href = link.getAttribute("href");\n if (!href || href.startsWith("#") || href.startsWith("//") || !href.startsWith("/")) {\n return null;\n }\n try {\n const url = new URL(href, window.location.origin);\n if (url.origin !== window.location.origin) return null;\n const routeHref = url.pathname + url.search;\n return routeHref === getCurrentRouteHref() ? null : routeHref;\n } catch (_) {\n return null;\n }\n }\n function getEligiblePrefetchLinks(limit) {\n const links = [];\n const seenHrefs = /* @__PURE__ */ new Set();\n for (const link of document2.querySelectorAll("a[href]")) {\n const href = getInternalRouteHrefFromLink(link);\n if (!href || seenHrefs.has(href)) continue;\n seenHrefs.add(href);\n links.push({ link, href });\n if (links.length >= limit) break;\n }\n return links;\n }\n async function preloadModulesForPageData(pageData, path) {\n if (!pageData || pageData.requiresFullDocumentNavigation) return;\n if (pageData.releaseId && window.__veryfrontSetReleaseId) {\n window.__veryfrontSetReleaseId(pageData.releaseId);\n }\n if (pageData.releaseAssetModules && window.__veryfrontSetReleaseAssetModules) {\n window.__veryfrontSetReleaseAssetModules(pageData.releaseAssetModules);\n }\n const modulePaths = getPageDataModulePaths(pageData);\n if (modulePaths.length === 0) return;\n try {\n await Promise.all(\n modulePaths.map(\n (modulePath) => loadPageDataComponent(pageData, modulePath, { allowDocumentReload: false })\n )\n );\n } catch (error) {\n if (isDependencySnapshotConflict(error)) {\n const cacheIdentity = pageDataCacheIdentity2(path);\n pageDataCache.delete(cacheIdentity);\n backgroundRefreshTimestamps.delete(cacheIdentity);\n prefetchedPaths.delete(path);\n throw error;\n }\n logBackgroundFetchFailure("Module prefetch", path, error);\n }\n }\n function removeQueuedPrefetch(path) {\n queuedPrefetchPaths.delete(path);\n for (let i = pageDataPrefetchQueue.length - 1; i >= 0; i--) {\n if (pageDataPrefetchQueue[i] === path) pageDataPrefetchQueue.splice(i, 1);\n }\n }\n function abortActiveSpeculativePrefetches() {\n for (const controller of activePageDataPrefetchControllers.values()) {\n controller.abort();\n }\n }\n function processPageDataPrefetchQueue() {\n if (isNavigating) return;\n while (activePageDataPrefetchControllers.size < PAGE_DATA_PREFETCH_CONCURRENCY && pageDataPrefetchQueue.length > 0) {\n const href = pageDataPrefetchQueue.shift();\n queuedPrefetchPaths.delete(href);\n if (prefetchedPaths.has(href) || inFlightPrefetches.has(href) || getCachedPageData(href)) {\n continue;\n }\n if (prefetchedPaths.size >= MAX_PREFETCH_PATHS) {\n const oldest = prefetchedPaths.values().next().value;\n if (oldest) prefetchedPaths.delete(oldest);\n }\n const controller = new AbortController();\n prefetchedPaths.add(href);\n inFlightPrefetches.add(href);\n activePageDataPrefetchControllers.set(href, controller);\n fetchPageDataForPrefetch(href, controller.signal).catch((error) => {\n prefetchedPaths.delete(href);\n if (isDependencySnapshotConflict(error)) {\n logBackgroundFetchFailure("Module prefetch", href, error);\n }\n }).finally(() => {\n inFlightPrefetches.delete(href);\n activePageDataPrefetchControllers.delete(href);\n processPageDataPrefetchQueue();\n });\n }\n }\n function prefetchPage(href) {\n if (isNavigating) return;\n if (prefetchedPaths.has(href) || inFlightPrefetches.has(href) || queuedPrefetchPaths.has(href)) return;\n const cachedPageData = getCachedPageData(href);\n if (cachedPageData) {\n preloadModulesForPageData(cachedPageData, href).catch((error) => {\n logBackgroundFetchFailure("Module prefetch", href, error);\n });\n return;\n }\n queuedPrefetchPaths.add(href);\n pageDataPrefetchQueue.push(href);\n processPageDataPrefetchQueue();\n }\n function prefetchEligibleRouteLinks(limit) {\n for (const { href } of getEligiblePrefetchLinks(limit)) {\n prefetchPage(href);\n }\n }\n function ensureViewportPrefetchObserver() {\n if (viewportPrefetchObserver || typeof IntersectionObserver !== "function") {\n return viewportPrefetchObserver;\n }\n viewportPrefetchObserver = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n viewportPrefetchObserver?.unobserve(entry.target);\n const href = getInternalRouteHrefFromLink(\n entry.target\n );\n if (href) prefetchPage(href);\n }\n }, { rootMargin: VIEWPORT_PREFETCH_ROOT_MARGIN });\n return viewportPrefetchObserver;\n }\n function observeViewportPrefetchLinks() {\n const observer = ensureViewportPrefetchObserver();\n if (!observer) return;\n for (const { link } of getEligiblePrefetchLinks(VIEWPORT_PREFETCH_MAX_LINKS)) {\n if (observedPrefetchLinks.has(link)) continue;\n observedPrefetchLinks.add(link);\n observer.observe(link);\n }\n }\n function runRoutePrefetchRefresh() {\n routePrefetchRefreshPending = false;\n prefetchEligibleRouteLinks(IDLE_PREFETCH_MAX_LINKS);\n observeViewportPrefetchLinks();\n }\n function scheduleRoutePrefetchRefresh() {\n if (routePrefetchRefreshPending) return;\n routePrefetchRefreshPending = true;\n setTimeout2(() => {\n if (typeof requestIdleCallback === "function") {\n requestIdleCallback(runRoutePrefetchRefresh, { timeout: IDLE_PREFETCH_DELAY_MS });\n return;\n }\n runRoutePrefetchRefresh();\n }, IDLE_PREFETCH_DELAY_MS);\n }\n const router = {\n domain: window.location.origin,\n path: window.location.pathname,\n push: (path) => {\n void navigateSPA(path, "push");\n },\n replace: (path) => {\n void navigateSPA(path, "replace");\n },\n back: () => {\n window.history.back();\n },\n forward: () => {\n window.history.forward();\n },\n prefetch: (path) => {\n prefetchPage(path);\n },\n pathname: window.location.pathname,\n query: Object.fromEntries(new URLSearchParams(window.location.search)),\n // Seed route params from the hydration data (issue #2741). Catch-all\n // segments arrive as arrays and are joined so no path info is lost.\n params: flattenRouteParams(deps.initialHydrationData.params || {}),\n isPreview: false,\n isMounted: true,\n navigate: (path) => navigateSPA(path, "push"),\n reload: () => window.location.reload()\n };\n window.__veryfrontRouter = router;\n if (deps.navigationStoreUsesRegistryFallback) {\n log("Router runtime does not export getNavigationStore; using shared v1 registry fallback");\n }\n if (typeof deps.getNavigationStore === "function") {\n deps.getNavigationStore().setNavigator((href, options) => {\n const mode = options && options.history;\n const historyMode = mode === "replace" ? "replace" : mode === "none" ? "none" : "push";\n return navigateSPA(href, historyMode);\n });\n }\n window.addEventListener("popstate", async (e) => {\n const path = window.location.pathname;\n log("Popstate:", path);\n saveScrollPosition(currentPath);\n if (!e.state?.pageData) {\n await navigateSPA(path, "none", true);\n return;\n }\n showNavigationProgress();\n try {\n currentPath = path;\n router.pathname = path;\n router.query = Object.fromEntries(new URLSearchParams(window.location.search));\n router.params = flattenRouteParams(e.state.pageData.params);\n await renderPageFromData(e.state.pageData, path);\n restoreScrollPosition(path);\n hideNavigationProgress();\n } catch (error) {\n hideNavigationProgress();\n logError("Popstate render failed:", error.message);\n window.location.reload();\n }\n });\n document2.addEventListener("click", (e) => {\n const link = e.target?.closest("a[href]");\n if (!link) return;\n const href = link.getAttribute("href");\n if (!href) return;\n if (href.startsWith("#")) {\n const target = document2.getElementById(href.slice(1));\n if (!target) return;\n e.preventDefault();\n target.scrollIntoView({ behavior: "smooth" });\n window.history.pushState(null, "", href);\n return;\n }\n if (link.target === "_blank" || link.hasAttribute("download") || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || !href.startsWith("/") || href.startsWith("//")) {\n return;\n }\n e.preventDefault();\n cancelScheduledPrefetch();\n void navigateSPA(href, "push");\n });\n document2.addEventListener(\n "mouseenter",\n (e) => {\n if (!e.target || typeof e.target.closest !== "function") return;\n const link = e.target.closest("a[href]");\n if (!link) return;\n const href = getInternalRouteHrefFromLink(link);\n if (!href) return;\n if (currentHoverLink === link) return;\n if (prefetchTimeout) {\n clearTimeout2(prefetchTimeout);\n prefetchTimeout = null;\n }\n currentHoverLink = link;\n prefetchTimeout = setTimeout2(() => {\n prefetchPage(href);\n prefetchTimeout = null;\n }, PREFETCH_DELAY_MS);\n },\n true\n );\n document2.addEventListener(\n "mouseleave",\n (e) => {\n if (!e.target || typeof e.target.closest !== "function") return;\n const relatedTarget = e.relatedTarget;\n if (currentHoverLink && relatedTarget && currentHoverLink.contains(relatedTarget)) return;\n cancelScheduledPrefetch();\n },\n true\n );\n if (document2.readyState === "loading") {\n document2.addEventListener("DOMContentLoaded", scheduleRoutePrefetchRefresh, { once: true });\n } else {\n scheduleRoutePrefetchRefresh();\n }\n window.useRouter = () => {\n try {\n return env2.useRouterFromModule();\n } catch (_) {\n return window.__veryfrontRouter;\n }\n };\n return {\n router,\n navigateSPA,\n renderPageFromData,\n prefetchPage,\n signalHydrationComplete,\n signalHydrationFailed\n };\n}\n\n// src/html/hydration-script-builder/runtime/renderer.ts\nfunction isModuleNotFoundError(error) {\n if (!error) return false;\n if (error instanceof SyntaxError) return false;\n const message = String(error.message || error);\n return /(?:dynamically imported module|Importing a module script failed|Failed to load module script)/i.test(message);\n}\nfunction preferReachedModuleError(earlier, later) {\n if (!earlier) return later;\n if (!later) return earlier;\n if (isModuleNotFoundError(earlier) && !isModuleNotFoundError(later)) return later;\n return earlier;\n}\nasync function loadPageModuleWithIndexFallback(basePath, pageSlug, pageModuleError, importModule) {\n try {\n return await importModule(basePath + ".js");\n } catch (error) {\n const routeError = preferReachedModuleError(pageModuleError, error);\n if (pageSlug === "index" || pageSlug.endsWith("/index")) throw routeError;\n try {\n return await importModule(basePath + "/index.js");\n } catch (indexError) {\n throw preferReachedModuleError(routeError, indexError);\n }\n }\n}\nfunction isAppRouterPath(path, appRouterRoot) {\n const normalizedPath = typeof path === "string" ? path.replace(/^\\/+/, "") : "";\n return normalizedPath === appRouterRoot || normalizedPath.startsWith(appRouterRoot + "/");\n}\nfunction isRootAppLayoutPath(path, appRouterRoot) {\n const normalizedPath = typeof path === "string" ? path.replace(/^\\/+/, "") : "";\n const pathWithoutExtension = normalizedPath.replace(/\\.(?:tsx|jsx|ts|js)$/, "");\n return pathWithoutExtension === appRouterRoot + "/layout";\n}\nfunction unwrapAppRouterDocumentLayout(LayoutComponent, React2) {\n return function AppRouterDocumentLayout(props) {\n const element = LayoutComponent(props);\n const asElement = element;\n if (!React2.isValidElement(element) || asElement.type !== "html") {\n return element;\n }\n const body = React2.Children.toArray(asElement.props?.children).find(\n (child) => React2.isValidElement(child) && child.type === "body"\n );\n return body?.props?.children ?? props.children;\n };\n}\nfunction createHydrationRenderer(deps) {\n const { env: env2, logging: logging2, componentLoader: componentLoader2, snapshotModules: snapshotModules2, moduleServerUrl: moduleServerUrl2 } = deps;\n const { window, document: document2, React: React2, RouterProvider: RouterProvider2, PageContextProvider: PageContextProvider2 } = env2;\n const { DEBUG, log, logError } = logging2;\n const { loadComponent, pathToModuleUrl } = componentLoader2;\n const { importSnapshotBoundModule } = snapshotModules2;\n async function renderPage(pathname) {\n const resolvedPathname = (() => {\n const input = typeof pathname === "string" ? pathname : window.location.pathname;\n try {\n return new URL(input, window.location.origin).pathname || "/";\n } catch (_) {\n const [pathOnly] = String(input || "/").split(/[?#]/);\n return pathOnly || "/";\n }\n })();\n const dataScript = findServerHydrationDataElement(document2);\n if (!dataScript) {\n logError("Hydration data not found");\n return;\n }\n let data = {};\n try {\n data = JSON.parse(dataScript.textContent || "{}");\n } catch (parseError) {\n logError("Failed to parse hydration data:", parseError);\n return;\n }\n log("Hydration data:", data);\n if (data.studioEmbed && window.__veryfrontSetStudioEmbed) {\n window.__veryfrontSetStudioEmbed(true);\n }\n if (window.__veryfrontSetReleaseId) {\n window.__veryfrontSetReleaseId(data.releaseId || null);\n }\n if (data.releaseAssetModules && window.__veryfrontSetReleaseAssetModules) {\n window.__veryfrontSetReleaseAssetModules(data.releaseAssetModules);\n }\n try {\n let pageModule;\n const pagePath = typeof data.pagePath === "string" ? data.pagePath : "";\n const normalizedPagePath = pagePath.replace(/^\\/+/, "");\n const normalizedAppRouterRoot = typeof data.appRouterRoot === "string" && data.appRouterRoot.replace(/^\\/+|\\/+$/g, "") ? data.appRouterRoot.replace(/^\\/+|\\/+$/g, "") : "app";\n const hasReleaseAssetModules = data.releaseAssetModules && Object.keys(data.releaseAssetModules).length > 0;\n const shouldRenderRscClientPage = data.clientModuleStrategy === "rsc-module" && !hasReleaseAssetModules && isAppRouterPath(normalizedPagePath, normalizedAppRouterRoot);\n const isolatedClientPage = data.isolatedClientPage === true;\n const loadHydrationComponent = async (path, preferRscModule) => {\n const normalizedPath = typeof path === "string" ? path.replace(/^\\/+/, "") : "";\n if (preferRscModule && isAppRouterPath(normalizedPath, normalizedAppRouterRoot)) {\n const moduleUrl = buildPinnedRscModuleUrl(path, data);\n log("Loading App Router component from RSC module:", moduleUrl);\n const module = await importSnapshotBoundModule(moduleUrl);\n return module.default || module;\n }\n return loadComponent(path, data);\n };\n let pageModuleError = null;\n if (data.pagePath) {\n const moduleUrl = shouldRenderRscClientPage ? buildPinnedRscModuleUrl(data.pagePath, data) : pathToModuleUrl(data.pagePath, data.studioEmbed, data);\n log("Loading page from hydration data:", moduleUrl);\n try {\n pageModule = await importSnapshotBoundModule(moduleUrl);\n } catch (error) {\n pageModuleError = error;\n logError("Failed to load page from hydration data:", error);\n }\n }\n if (!pageModule) {\n const pageSlug = resolvedPathname === "/" ? "index" : resolvedPathname.slice(1);\n log("Falling back to Pages Router pattern:", pageSlug);\n const prefix = pageSlug.startsWith("@/") ? "" : "/pages";\n const basePath = moduleServerUrl2 + prefix + "/" + pageSlug;\n pageModule = await loadPageModuleWithIndexFallback(\n basePath,\n pageSlug,\n pageModuleError,\n (moduleUrl) => importSnapshotBoundModule(appendDependencyPinningVersion(moduleUrl, data))\n );\n }\n if (!pageModule) {\n logError("Page module failed to load");\n return;\n }\n const PageComponent = pageModule.default || pageModule;\n if (!PageComponent) {\n logError("Page component not found");\n return;\n }\n const normalizedParams = flattenRouteParams(data.params);\n const pageProps = { ...data.props || {}, params: normalizedParams };\n let tree = React2.createElement(PageComponent, pageProps);\n const layouts = data.layouts;\n if (layouts?.length) {\n for (let i = layouts.length - 1; i >= 0; i--) {\n const layout = layouts[i];\n if (!layout) continue;\n const LayoutComponent = await loadHydrationComponent(\n layout.path,\n shouldRenderRscClientPage\n );\n if (LayoutComponent) {\n const WrappedLayoutComponent = shouldRenderRscClientPage && isRootAppLayoutPath(layout.path, normalizedAppRouterRoot) ? unwrapAppRouterDocumentLayout(LayoutComponent, React2) : LayoutComponent;\n const layoutProps = data.layoutProps?.[layout.path] || {};\n tree = React2.createElement(\n WrappedLayoutComponent,\n { ...layoutProps, children: tree }\n );\n }\n }\n }\n if (data.appPath && !isolatedClientPage) {\n const AppComponent = await loadHydrationComponent(data.appPath, shouldRenderRscClientPage);\n if (AppComponent) {\n tree = React2.createElement(AppComponent, { children: tree });\n }\n }\n if (data.errorPath) {\n const ErrorComponent = await loadHydrationComponent(\n data.errorPath,\n shouldRenderRscClientPage\n );\n if (ErrorComponent) {\n class AppRouterErrorBoundary extends React2.Component {\n constructor(props) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n static getDerivedStateFromError(error) {\n return { hasError: true, error };\n }\n render() {\n if (this.state.hasError) {\n return React2.createElement(ErrorComponent, {\n error: this.state.error,\n reset: () => this.setState({ hasError: false, error: null })\n });\n }\n return this.props.children;\n }\n }\n tree = React2.createElement(AppRouterErrorBoundary, null, tree);\n }\n }\n const headings = data.headings || [];\n const pageContext = {\n slug: data.slug || "",\n path: data.pagePath || resolvedPathname,\n params: normalizedParams,\n query: Object.fromEntries(new URLSearchParams(window.location.search)),\n frontmatter: data.frontmatter || {},\n data: data.props || {},\n headings,\n mdxHeadings: headings\n // Alias for backwards compatibility\n };\n tree = React2.createElement(PageContextProvider2, { pageContext, children: tree });\n tree = React2.createElement(RouterProvider2, { router: deps.router, children: tree });\n const container = isolatedClientPage ? document2.getElementById("veryfront-page-island") : document2.getElementById("root");\n if (!container) {\n if (isolatedClientPage) {\n throw new Error("Isolated client page root not found");\n }\n return;\n }\n if (container.__reactRoot) {\n container.__reactRoot.render(tree);\n log("Page re-rendered");\n return;\n }\n if (shouldRenderRscClientPage) {\n container.__reactRoot = env2.createRoot(container);\n container.__reactRoot.render(tree);\n log("Client-side React app rendered successfully");\n } else {\n const { hydrateRoot } = await import("react-dom/client");\n const options = {\n identifierPrefix: "vf",\n onRecoverableError: (error) => {\n if (data.dev && DEBUG) {\n log("Hydration mismatch (suppressed):", error.message);\n }\n }\n };\n container.__reactRoot = hydrateRoot(container, tree, options);\n log("Client-side React app hydrated successfully");\n }\n if (window.__veryfrontHydrationComplete) {\n window.__veryfrontHydrationComplete();\n }\n } catch (error) {\n logError("Client initialization error:", error);\n if (window.__veryfrontHydrationFailed) {\n window.__veryfrontHydrationFailed(error);\n }\n }\n }\n function start() {\n window.__veryfrontRenderPage = renderPage;\n void renderPage(window.location.pathname);\n const initialDataScript = findServerHydrationDataElement(document2);\n if (initialDataScript) {\n try {\n const pageData = JSON.parse(initialDataScript.textContent || "{}");\n if (pageData.pagePath) {\n window.history.replaceState({ pageData, scrollY: 0 }, "", window.location.href);\n log("Stored initial page data in history state");\n }\n } catch (_) {\n }\n }\n }\n return { renderPage, start };\n}\n\n// src/html/hydration-script-builder/runtime/navigation-store.ts\nvar NAVIGATION_STORE_REGISTRY_KEY = "veryfront.navigation.store.v1";\nfunction resolveNavigationStore(RouterRuntime2) {\n const usesRegistryFallback2 = typeof RouterRuntime2.getNavigationStore !== "function";\n if (!usesRegistryFallback2) {\n return {\n usesRegistryFallback: usesRegistryFallback2,\n getNavigationStore: RouterRuntime2.getNavigationStore\n };\n }\n return {\n usesRegistryFallback: usesRegistryFallback2,\n getNavigationStore: () => {\n const storeKey = Symbol.for(NAVIGATION_STORE_REGISTRY_KEY);\n const registry = globalThis;\n const existing = registry[storeKey];\n if (existing) return existing;\n const listeners = /* @__PURE__ */ new Set();\n let navigator = null;\n const store = {\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n getHref() {\n const loc = globalThis.location;\n return loc ? loc.pathname + loc.search + loc.hash : "/";\n },\n notify() {\n for (const listener of [...listeners]) {\n try {\n listener();\n } catch {\n }\n }\n },\n navigate(href, options) {\n if (navigator) return navigator(href, options);\n globalThis.location?.assign(href);\n return Promise.resolve();\n },\n setNavigator(next) {\n navigator = next;\n }\n };\n registry[storeKey] = store;\n return store;\n }\n };\n}\n\n// src/html/hydration-script-builder/runtime/main.ts\nvar runtimeWindow = globalThis;\nvar runtimeDocument = globalThis.document;\nvar env = {\n window: runtimeWindow,\n document: runtimeDocument,\n fetch: (url, init) => fetch(url, init),\n React,\n RouterProvider,\n PageContextProvider,\n createRoot: (container) => createRoot(container),\n importModule: (moduleUrl) => import(moduleUrl),\n useRouterFromModule,\n setTimeout: (handler, timeout) => setTimeout(handler, timeout),\n clearTimeout: (id) => clearTimeout(id)\n};\nvar logging = createLogging(runtimeWindow);\nvar initialHydrationData = readInitialHydrationData(runtimeDocument);\nvar documentDependencyPinningCacheKey = readDocumentDependencyPinningCacheKey(\n initialHydrationData\n);\nvar routeTiming = createRouteTimingRecorder(runtimeWindow, logging);\nvar snapshotModules = createSnapshotModuleImporter({\n importModule: env.importModule,\n fetchModule: env.fetch,\n reloadDocument: () => runtimeWindow.location.reload(),\n recoveryState: runtimeWindow\n});\nvar componentLoader = createComponentLoader({\n window: runtimeWindow,\n logging,\n moduleServerUrl: moduleServerUrl(runtimeWindow),\n snapshotModules\n});\nruntimeWindow.__veryfrontClearComponentCache = componentLoader.clearComponentCache;\nruntimeWindow.__veryfrontSetStudioEmbed = componentLoader.setStudioEmbed;\nruntimeWindow.__veryfrontSetReleaseId = componentLoader.setReleaseId;\nruntimeWindow.__veryfrontSetReleaseAssetModules = componentLoader.setReleaseAssetModules;\nruntimeWindow.__veryfrontSetHMRRefreshTimestamp = componentLoader.setHMRRefreshTimestamp;\nvar { usesRegistryFallback, getNavigationStore } = resolveNavigationStore(RouterRuntime);\nvar routerRuntime = createRouterRuntime({\n env,\n logging,\n routeTiming,\n componentLoader,\n snapshotModules,\n initialHydrationData,\n documentDependencyPinningCacheKey,\n getNavigationStore,\n navigationStoreUsesRegistryFallback: usesRegistryFallback\n});\ncreateHydrationRenderer({\n env,\n logging,\n componentLoader,\n snapshotModules,\n moduleServerUrl: moduleServerUrl(runtimeWindow),\n router: routerRuntime.router\n}).start();\n'; diff --git a/src/utils/version-constant.ts b/src/utils/version-constant.ts index f7b7c58a68..359edbb726 100644 --- a/src/utils/version-constant.ts +++ b/src/utils/version-constant.ts @@ -1,4 +1,4 @@ // Keep in sync with deno.json version. // scripts/release.ts updates this constant during releases. /** Shared version value. */ -export const VERSION = "0.1.1208"; +export const VERSION = "0.1.1209";