@elurjs/kit 2.4.10 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +313 -0
- package/README.md +248 -15
- package/dist/lib/action/index.cjs +1 -1
- package/dist/lib/action/index.js +1 -1
- package/dist/lib/{action-DZHihU3Z.cjs → action-BiE4tEty.cjs} +2 -2
- package/dist/lib/{action-DZHihU3Z.cjs.map → action-BiE4tEty.cjs.map} +1 -1
- package/dist/lib/{action-BB6N5yD-.js → action-CXVk8rWt.js} +3 -3
- package/dist/lib/{action-BB6N5yD-.js.map → action-CXVk8rWt.js.map} +1 -1
- package/dist/lib/adapter-DNsjXvq5.js +160 -0
- package/dist/lib/adapter-DNsjXvq5.js.map +1 -0
- package/dist/lib/adapter-Dz2hzR36.cjs +3 -0
- package/dist/lib/adapter-Dz2hzR36.cjs.map +1 -0
- package/dist/lib/adapters/bun.cjs +2 -2
- package/dist/lib/adapters/bun.cjs.map +1 -1
- package/dist/lib/adapters/bun.js +17 -9
- package/dist/lib/adapters/bun.js.map +1 -1
- package/dist/lib/adapters/index.d.cts +34 -0
- package/dist/lib/adapters/index.d.ts +34 -0
- package/dist/lib/adapters/netlify.cjs +1 -1
- package/dist/lib/adapters/netlify.js +7 -7
- package/dist/lib/adapters/node.cjs +5 -5
- package/dist/lib/adapters/node.cjs.map +1 -1
- package/dist/lib/adapters/node.js +20 -12
- package/dist/lib/adapters/node.js.map +1 -1
- package/dist/lib/adapters/vercel.cjs +1 -1
- package/dist/lib/adapters/vercel.js +8 -8
- package/dist/lib/build/build.d.cts +40 -0
- package/dist/lib/build/build.d.ts +40 -0
- package/dist/lib/build/document-shell.d.cts +47 -1
- package/dist/lib/build/document-shell.d.ts +47 -1
- package/dist/lib/build/vite-build.d.cts +25 -2
- package/dist/lib/build/vite-build.d.ts +25 -2
- package/dist/lib/cache/adapter.d.cts +41 -0
- package/dist/lib/cache/adapter.d.ts +41 -0
- package/dist/lib/cache/index.cjs +2 -0
- package/dist/lib/cache/index.cjs.map +1 -0
- package/dist/lib/cache/index.d.cts +5 -0
- package/dist/lib/cache/index.d.ts +5 -0
- package/dist/lib/cache/index.js +113 -0
- package/dist/lib/cache/index.js.map +1 -0
- package/dist/lib/cache/invalidation.d.cts +36 -0
- package/dist/lib/cache/invalidation.d.ts +36 -0
- package/dist/lib/cache/redis-adapter.d.cts +49 -0
- package/dist/lib/cache/redis-adapter.d.ts +49 -0
- package/dist/lib/cache-DGhTYyqh.js +50 -0
- package/dist/lib/cache-DGhTYyqh.js.map +1 -0
- package/dist/lib/cache-X1n4Ossu.cjs +2 -0
- package/dist/lib/cache-X1n4Ossu.cjs.map +1 -0
- package/dist/lib/cli/output.d.cts +58 -0
- package/dist/lib/cli/output.d.ts +58 -0
- package/dist/lib/cli/ports.d.cts +20 -0
- package/dist/lib/cli/ports.d.ts +20 -0
- package/dist/lib/cli.cjs +2559 -1488
- package/dist/lib/cli.cjs.map +1 -1
- package/dist/lib/cli.d.cts +21 -0
- package/dist/lib/cli.d.ts +21 -0
- package/dist/lib/cli.js +2558 -1488
- package/dist/lib/cli.js.map +1 -1
- package/dist/lib/config/index.cjs +1 -1
- package/dist/lib/config/index.cjs.map +1 -1
- package/dist/lib/config/index.d.cts +79 -0
- package/dist/lib/config/index.d.ts +79 -0
- package/dist/lib/config/index.js +21 -14
- package/dist/lib/config/index.js.map +1 -1
- package/dist/lib/{errors-ePYxDlhU.js → errors-j4cg9VN3.js} +24 -30
- package/dist/lib/{errors-ePYxDlhU.js.map → errors-j4cg9VN3.js.map} +1 -1
- package/dist/lib/errors-kSl9yLkg.cjs +2 -0
- package/dist/lib/{errors-0X7pCFNU.cjs.map → errors-kSl9yLkg.cjs.map} +1 -1
- package/dist/lib/hydrate-BSLuI0Nh.cjs +2 -0
- package/dist/lib/hydrate-BSLuI0Nh.cjs.map +1 -0
- package/dist/lib/hydrate-BaS7-d8C.js +133 -0
- package/dist/lib/hydrate-BaS7-d8C.js.map +1 -0
- package/dist/lib/idiomorph.esm-BOOTOg1r.cjs +2 -0
- package/dist/lib/idiomorph.esm-BOOTOg1r.cjs.map +1 -0
- package/dist/lib/idiomorph.esm-CXuY-EHB.js +371 -0
- package/dist/lib/idiomorph.esm-CXuY-EHB.js.map +1 -0
- package/dist/lib/index.cjs +1 -1
- package/dist/lib/index.cjs.map +1 -1
- package/dist/lib/index.d.cts +8 -6
- package/dist/lib/index.d.ts +8 -6
- package/dist/lib/index.js +1 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/interpolation-plugin-C0dTd69H.cjs +58 -0
- package/dist/lib/interpolation-plugin-C0dTd69H.cjs.map +1 -0
- package/dist/lib/{interpolation-plugin-Wgb1j4pT.js → interpolation-plugin-K2qE69tF.js} +95 -126
- package/dist/lib/interpolation-plugin-K2qE69tF.js.map +1 -0
- package/dist/lib/invalidation-CFpNQUv6.cjs +2 -0
- package/dist/lib/invalidation-CFpNQUv6.cjs.map +1 -0
- package/dist/lib/invalidation-DXhdE9wj.js +64 -0
- package/dist/lib/invalidation-DXhdE9wj.js.map +1 -0
- package/dist/lib/island/generate-entry.d.cts +40 -2
- package/dist/lib/island/generate-entry.d.ts +40 -2
- package/dist/lib/island/hydrate.d.cts +19 -6
- package/dist/lib/island/hydrate.d.ts +19 -6
- package/dist/lib/island/index.cjs +1 -1
- package/dist/lib/island/index.d.cts +2 -1
- package/dist/lib/island/index.d.ts +2 -1
- package/dist/lib/island/index.js +1 -1
- package/dist/lib/island/island.d.cts +15 -0
- package/dist/lib/island/island.d.ts +15 -0
- package/dist/lib/island-CTBnsOhI.js +59 -0
- package/dist/lib/island-CTBnsOhI.js.map +1 -0
- package/dist/lib/island-kfQEKRe_.cjs +2 -0
- package/dist/lib/island-kfQEKRe_.cjs.map +1 -0
- package/dist/lib/node-http-DVFcYeBS.js +723 -0
- package/dist/lib/node-http-DVFcYeBS.js.map +1 -0
- package/dist/lib/node-http-Uct7DhmN.cjs +13 -0
- package/dist/lib/node-http-Uct7DhmN.cjs.map +1 -0
- package/dist/lib/rolldown-runtime-BIInC43l.cjs +1 -0
- package/dist/lib/rolldown-runtime-Dy4uBu1J.js +11 -0
- package/dist/lib/router/client.cjs +1 -1
- package/dist/lib/router/client.cjs.map +1 -1
- package/dist/lib/router/client.d.cts +55 -8
- package/dist/lib/router/client.d.ts +55 -8
- package/dist/lib/router/client.js +298 -97
- package/dist/lib/router/client.js.map +1 -1
- package/dist/lib/router/redirects.d.cts +34 -0
- package/dist/lib/router/redirects.d.ts +34 -0
- package/dist/lib/runtime/handler.d.cts +59 -0
- package/dist/lib/runtime/handler.d.ts +59 -0
- package/dist/lib/runtime/index.cjs +1 -1
- package/dist/lib/runtime/index.d.cts +2 -1
- package/dist/lib/runtime/index.d.ts +2 -1
- package/dist/lib/runtime/index.js +4 -4
- package/dist/lib/runtime/logger.d.cts +47 -0
- package/dist/lib/runtime/logger.d.ts +47 -0
- package/dist/lib/runtime/node-http.d.cts +11 -1
- package/dist/lib/runtime/node-http.d.ts +11 -1
- package/dist/lib/runtime-ChE4flX3.cjs +2 -0
- package/dist/lib/runtime-ChE4flX3.cjs.map +1 -0
- package/dist/lib/runtime-OMyz5yE-.js +905 -0
- package/dist/lib/runtime-OMyz5yE-.js.map +1 -0
- package/dist/lib/seo/index.cjs +1 -10
- package/dist/lib/seo/index.d.cts +1 -0
- package/dist/lib/seo/index.d.ts +1 -0
- package/dist/lib/seo/index.js +2 -41
- package/dist/lib/seo/sitemap-from-routes.d.cts +24 -0
- package/dist/lib/seo/sitemap-from-routes.d.ts +24 -0
- package/dist/lib/{shared-CfqOLsKj.cjs → shared-CP0Dc3cp.cjs} +72 -47
- package/dist/lib/shared-CP0Dc3cp.cjs.map +1 -0
- package/dist/lib/{shared-DU3iQgr7.js → shared-D8w-neq2.js} +87 -58
- package/dist/lib/shared-D8w-neq2.js.map +1 -0
- package/dist/lib/sitemap-from-routes-CaBoiGvE.js +77 -0
- package/dist/lib/sitemap-from-routes-CaBoiGvE.js.map +1 -0
- package/dist/lib/sitemap-from-routes-DbcLLL4G.cjs +13 -0
- package/dist/lib/sitemap-from-routes-DbcLLL4G.cjs.map +1 -0
- package/dist/lib/ssr/render.d.cts +8 -2
- package/dist/lib/ssr/render.d.ts +8 -2
- package/dist/lib/ssr/server.d.cts +7 -0
- package/dist/lib/ssr/server.d.ts +7 -0
- package/dist/lib/ssr/stream-response.d.cts +30 -0
- package/dist/lib/ssr/stream-response.d.ts +30 -0
- package/dist/lib/ssr/stream.d.cts +16 -1
- package/dist/lib/ssr/stream.d.ts +16 -1
- package/dist/lib/{stream-DmZjYqHm.cjs → stream-Cg-xYG82.cjs} +3 -3
- package/dist/lib/stream-Cg-xYG82.cjs.map +1 -0
- package/dist/lib/stream-D-ytogdu.js +106 -0
- package/dist/lib/stream-D-ytogdu.js.map +1 -0
- package/dist/lib/vite/index.cjs +1 -1
- package/dist/lib/vite/index.cjs.map +1 -1
- package/dist/lib/vite/index.d.cts +11 -0
- package/dist/lib/vite/index.d.ts +11 -0
- package/dist/lib/vite/index.js +101 -94
- package/dist/lib/vite/index.js.map +1 -1
- package/package.json +25 -8
- package/dist/lib/errors-0X7pCFNU.cjs +0 -2
- package/dist/lib/hydrate-CJbUWdXu.js +0 -126
- package/dist/lib/hydrate-CJbUWdXu.js.map +0 -1
- package/dist/lib/hydrate-D6MR24mz.cjs +0 -2
- package/dist/lib/hydrate-D6MR24mz.cjs.map +0 -1
- package/dist/lib/interpolation-plugin-TWDch8nn.cjs +0 -39
- package/dist/lib/interpolation-plugin-TWDch8nn.cjs.map +0 -1
- package/dist/lib/interpolation-plugin-Wgb1j4pT.js.map +0 -1
- package/dist/lib/node-http-DRAUhO0c.js +0 -617
- package/dist/lib/node-http-DRAUhO0c.js.map +0 -1
- package/dist/lib/node-http-DhxguYyz.cjs +0 -13
- package/dist/lib/node-http-DhxguYyz.cjs.map +0 -1
- package/dist/lib/runtime-CrH5X-Es.cjs +0 -2
- package/dist/lib/runtime-CrH5X-Es.cjs.map +0 -1
- package/dist/lib/runtime-Ds2-wH6P.js +0 -520
- package/dist/lib/runtime-Ds2-wH6P.js.map +0 -1
- package/dist/lib/seo/index.cjs.map +0 -1
- package/dist/lib/seo/index.js.map +0 -1
- package/dist/lib/shared-CfqOLsKj.cjs.map +0 -1
- package/dist/lib/shared-DU3iQgr7.js.map +0 -1
- package/dist/lib/stream-CpOG052V.js +0 -104
- package/dist/lib/stream-CpOG052V.js.map +0 -1
- package/dist/lib/stream-DmZjYqHm.cjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../../../src/router/client.ts"],"sourcesContent":["/**\n * Client-side router for Elur Kit.\n *\n * Intercepts clicks on internal links, fetches the rendered page body from\n * `/__elur-js/render`, swaps the `#app` content and updates the history state.\n * This is loaded as part of the client bundle instead of being inlined in\n * every HTML page.\n *\n * Features:\n * - SPA navigation with head merge (title, meta, OG tags)\n * - Scroll restoration on back/forward\n * - Prefetch on hover/focus (Astro-style), with opt-in viewport prefetch\n * - View Transitions API with `prefers-reduced-motion` respect\n */\n\ninterface RenderPayload {\n title?: string;\n body: string;\n /** Set-Cookie value relayed by the server to clear a consumed action error. */\n clearActionErrorCookie?: string;\n /** `<head>` tags (title, meta, OG, twitter) to merge on navigation. */\n head?: string;\n}\n\n/**\n * Whether the `/__elur-js/render` endpoint has been detected. Static builds emit\n * `<meta name=\"elur:render-endpoint\" content=\"off\">` so this starts as\n * `false` with zero probe requests. For older builds, a single shared probe\n * determines availability so concurrent prefetches never storm the endpoint.\n */\nlet renderEndpointAvailable = true;\n\n/** Shared in-flight probe promise; at most one request hits the endpoint. */\nlet endpointProbe: Promise<boolean> | null = null;\n\n/** Resolves endpoint availability, caching the result for the page lifetime. */\nfunction resolveEndpointAvailability(): Promise<boolean> {\n if (!renderEndpointAvailable) return Promise.resolve(false);\n if (!endpointProbe) {\n endpointProbe = (async () => {\n const url = new URL(\"/__elur-js/render\", location.origin);\n url.searchParams.set(\"page\", \"/\");\n try {\n const response = await fetch(url.toString(), {\n headers: { Accept: \"application/json\", \"X-Elur-Probe\": \"1\" },\n });\n renderEndpointAvailable = response.ok;\n return response.ok;\n } catch {\n renderEndpointAvailable = false;\n return false;\n }\n })();\n }\n return endpointProbe;\n}\n\nfunction isInternalLink(link: HTMLAnchorElement): boolean {\n return (\n link.tagName === \"A\" &&\n link.hostname === location.hostname &&\n link.target === \"\" &&\n !link.getAttribute(\"download\") &&\n !link.hasAttribute(\"data-no-router\")\n );\n}\n\nfunction hasModifier(event: MouseEvent): boolean {\n return event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;\n}\n\n// --- Prefetch cache ---\n\nconst PREFETCH_TTL_MS = 30_000; // 30 seconds\n\ninterface CacheEntry {\n payload: RenderPayload;\n ts: number;\n}\n\nconst prefetchCache = new Map<string, CacheEntry>();\n\n/** Builds the cache key from pathname + search. */\nfunction cacheKey(pathname: string, search: string): string {\n return pathname + search;\n}\n\n/** Returns a cached payload if fresh, otherwise undefined. */\nfunction getCached(key: string): RenderPayload | undefined {\n const entry = prefetchCache.get(key);\n if (!entry) return undefined;\n if (Date.now() - entry.ts > PREFETCH_TTL_MS) {\n prefetchCache.delete(key);\n return undefined;\n }\n return entry.payload;\n}\n\n/** Stores a payload in the prefetch cache. */\nfunction setCached(key: string, payload: RenderPayload): void {\n prefetchCache.set(key, { payload, ts: Date.now() });\n}\n\n/**\n * Fetches the render payload for a path. Uses the prefetch cache when fresh.\n * Stores the result in the cache for subsequent navigations.\n *\n * On static deployments (no `/__elur-js/render` endpoint), falls back to\n * fetching the full HTML page and extracting `#app` + `<head>` tags.\n */\nasync function fetchPayload(pathname: string, search: string, signal?: AbortSignal): Promise<RenderPayload | undefined> {\n const key = cacheKey(pathname, search);\n const cached = getCached(key);\n if (cached) return cached;\n\n // Wait on the shared probe so concurrent prefetches generate at most ONE\n // request against the endpoint (the rest go straight to the HTML fallback).\n if (renderEndpointAvailable) {\n if (await resolveEndpointAvailability()) {\n const payload = await fetchFromRenderEndpoint(pathname, search, signal);\n if (payload) {\n setCached(key, payload);\n return payload;\n }\n // The endpoint exists but couldn't render this specific page — fall\n // through to the HTML-based fetch without disabling it globally.\n }\n }\n\n // Static fallback: fetch the full HTML page and extract #app + head.\n const payload = await fetchFromHtml(pathname, search, signal);\n if (payload) {\n setCached(key, payload);\n }\n return payload;\n}\n\n/** Attempts to fetch from the `/__elur-js/render` JSON endpoint. */\nasync function fetchFromRenderEndpoint(pathname: string, search: string, signal?: AbortSignal): Promise<RenderPayload | undefined> {\n const url = new URL(\"/__elur-js/render\", location.origin);\n url.searchParams.set(\"page\", pathname);\n const current = new URL(location.href);\n url.searchParams.set(\"search\", search || current.search);\n\n let response: Response;\n try {\n response = await fetch(url.toString(), { headers: { Accept: \"application/json\" }, signal });\n } catch (err) {\n if (err instanceof DOMException && err.name === \"AbortError\") return undefined;\n return undefined;\n }\n if (!response.ok) return undefined;\n\n let payload: RenderPayload;\n try {\n payload = await response.json();\n } catch {\n return undefined;\n }\n return payload;\n}\n\n/**\n * Static-mode fallback: fetches the full HTML page for the path and extracts\n * the `#app` innerHTML plus managed `<head>` tags (`[data-elur-head]`).\n * Also extracts `<title>`, stylesheets, and headLinks for SPA navigation.\n */\nasync function fetchFromHtml(pathname: string, search: string, signal?: AbortSignal): Promise<RenderPayload | undefined> {\n const fullUrl = pathname + (search || \"\");\n let response: Response;\n try {\n response = await fetch(fullUrl, { headers: { Accept: \"text/html\" }, signal });\n } catch (err) {\n if (err instanceof DOMException && err.name === \"AbortError\") return undefined;\n return undefined;\n }\n if (!response.ok) return undefined;\n\n let html: string;\n try {\n html = await response.text();\n } catch {\n return undefined;\n }\n\n // Parse the full HTML document.\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, \"text/html\");\n\n // Extract #app innerHTML — this is the page body.\n const appEl = doc.getElementById(\"app\");\n if (!appEl) return undefined;\n const body = appEl.innerHTML;\n\n // Extract managed head tags (data-elur-head) for mergeHead.\n const headTags = doc.querySelectorAll(\"[data-elur-head]\");\n let head = \"\";\n for (const tag of headTags) {\n head += tag.outerHTML;\n }\n\n // Also extract headLinks (favicons, manifest, theme-color) so they persist.\n // These don't have data-elur-head, so we grab them separately.\n const linkTags = doc.head.querySelectorAll(\"link[rel='icon'], link[rel='apple-touch-icon'], link[rel='manifest'], meta[name='theme-color']\");\n for (const tag of linkTags) {\n // Skip if already in the current document head\n const href = tag.getAttribute(\"href\");\n if (href && document.head.querySelector(`link[href=\"${href}\"]`)) continue;\n head += tag.outerHTML;\n }\n\n const title = doc.querySelector(\"title\")?.textContent ?? undefined;\n\n return { body, head, title };\n}\n\n/**\n * Prefetches a path without navigating. Called by the IntersectionObserver\n * when a link enters the viewport, and on hover/focus.\n */\nexport async function prefetch(pathname: string, search = \"\"): Promise<void> {\n const key = cacheKey(pathname, search);\n if (prefetchCache.has(key)) {\n // Already cached or in-flight — skip.\n const entry = prefetchCache.get(key)!;\n if (Date.now() - entry.ts <= PREFETCH_TTL_MS) return;\n }\n await fetchPayload(pathname, search);\n}\n\n// --- View Transitions ---\n\n/** Returns true if the user prefers reduced motion. */\nfunction prefersReducedMotion(): boolean {\n return window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches ?? false;\n}\n\n/** Returns true if the View Transitions API is available. */\nfunction supportsViewTransitions(): boolean {\n return typeof (document as any).startViewTransition === \"function\";\n}\n\n// --- Navigation ---\n\n/**\n * Guard against concurrent navigations. When a navigation is in-flight, a new\n * request cancels the previous one (abort + ignore its result). This prevents\n * race conditions where two rapid clicks could swap content out of order.\n */\nlet inFlightNavigation: {\n controller: AbortController;\n pathname: string;\n} | null = null;\n\n/**\n * Cancels any in-flight navigation so a new one can proceed cleanly.\n */\nfunction cancelInFlightNavigation(): void {\n if (inFlightNavigation) {\n inFlightNavigation.controller.abort();\n inFlightNavigation = null;\n }\n}\n\n/**\n * Hoists `<link rel=\"stylesheet\">` and `<style>` tags from inside `#app` into\n * `<head>` so they persist across SPA navigations (prevents FOUC/flashing).\n * Deduplicates by `href` for links and by text content for styles.\n */\nexport function hoistStyles(container: ParentNode): void {\n const links = container.querySelectorAll<HTMLLinkElement>('link[rel=\"stylesheet\"]');\n for (const link of links) {\n const href = link.getAttribute(\"href\");\n if (!href) continue;\n // Already in <head>?\n const existing = document.head.querySelector(`link[rel=\"stylesheet\"][href=\"${href}\"]`);\n if (existing) {\n link.remove();\n continue;\n }\n // Mark as hoisted so we can clean up later if needed\n link.setAttribute(\"data-elur-hoisted\", \"\");\n document.head.appendChild(link);\n }\n\n const styles = container.querySelectorAll<HTMLStyleElement>(\"style\");\n for (const style of styles) {\n const text = style.textContent?.trim();\n if (!text) continue;\n // Check if an identical style already exists in <head>\n const existing = Array.from(document.head.querySelectorAll(\"style\")).find(\n (s) => s.textContent?.trim() === text,\n );\n if (existing) {\n style.remove();\n continue;\n }\n style.setAttribute(\"data-elur-hoisted\", \"\");\n document.head.appendChild(style);\n }\n}\n\n/**\n * Announces a route change to assistive technology via an aria-live region.\n * This is critical for screen reader users who need to know the page content\n * has changed after a SPA navigation.\n */\nfunction announceNavigation(pathname: string): void {\n let liveRegion = document.getElementById(\"elur-route-announcer\");\n if (!liveRegion) {\n liveRegion = document.createElement(\"div\");\n liveRegion.id = \"elur-route-announcer\";\n liveRegion.setAttribute(\"aria-live\", \"assertive\");\n liveRegion.setAttribute(\"aria-atomic\", \"true\");\n liveRegion.setAttribute(\"role\", \"status\");\n // Visually hidden but available to screen readers.\n liveRegion.setAttribute(\"style\", \"position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden;\");\n document.body.appendChild(liveRegion);\n }\n // Clear and re-set so screen readers announce the change.\n liveRegion.textContent = \"\";\n // Use a microtask delay so the DOM update is picked up by AT.\n // Guard against the document being torn down (e.g. in tests).\n const region = liveRegion;\n const timer = setTimeout(() => {\n if (typeof document !== \"undefined\" && region) {\n const title = document.title || pathname;\n region.textContent = title;\n }\n }, 50);\n // Don't keep the process alive just for the announcer.\n if (typeof timer === \"object\" && timer && \"unref\" in timer) {\n (timer as { unref: () => void }).unref();\n }\n}\n\n/**\n * Moves focus to the main content area after a SPA navigation. This follows\n * the WAI-ARIA pattern for route changes: if the #app has a tabindex=-1, focus\n * it; otherwise create a temporary focus target.\n */\nfunction moveFocusToContent(): void {\n const app = document.getElementById(\"app\");\n if (!app) return;\n // Ensure the container is focusable.\n if (!app.hasAttribute(\"tabindex\")) {\n app.setAttribute(\"tabindex\", \"-1\");\n }\n // Remove outline only for mouse users; keyboard users keep it.\n app.focus({ preventScroll: false });\n}\n\n/**\n * Updates the canonical URL and og:url meta tags after navigation.\n */\nfunction updateCanonicalUrl(pathname: string, search: string): void {\n const fullUrl = location.origin + pathname + (search || \"\");\n // Update or create canonical link.\n let canonical = document.querySelector<HTMLLinkElement>('link[rel=\"canonical\"]');\n if (!canonical) {\n canonical = document.createElement(\"link\");\n canonical.rel = \"canonical\";\n document.head.appendChild(canonical);\n }\n canonical.href = fullUrl;\n // Update og:url meta.\n let ogUrl = document.querySelector<HTMLMetaElement>('meta[property=\"og:url\"]');\n if (!ogUrl) {\n ogUrl = document.createElement(\"meta\");\n ogUrl.setAttribute(\"property\", \"og:url\");\n document.head.appendChild(ogUrl);\n }\n ogUrl.content = fullUrl;\n}\n\n/**\n * Navigates to a page without a full reload: fetches the fresh body from the\n * `/__elur-js/render` endpoint, swaps `#app`, updates the document title and\n * dispatches `elur:rendered` so islands re-hydrate. Used by the router on\n * clicks and available for programmatic navigation (e.g. after a server\n * action returns a redirect, so the target page shows fresh server data).\n *\n * Concurrent navigations are handled: a new navigateTo() cancels any\n * in-flight navigation to prevent out-of-order content swaps.\n *\n * @param pathname Path without query, e.g. \"/movies/inception\".\n * @param search Query string, e.g. \"?reviewed=1\" (optional).\n * @param push Whether to push a history entry (default true).\n * @returns true on success, false if the render failed.\n */\nexport async function navigateTo(pathname: string, search = \"\", push = true): Promise<boolean> {\n // Cancel any previous in-flight navigation to prevent race conditions.\n cancelInFlightNavigation();\n\n const controller = new AbortController();\n inFlightNavigation = { controller, pathname };\n\n let payload: RenderPayload | undefined;\n try {\n payload = await fetchPayload(pathname, search, controller.signal);\n } catch (err) {\n if (controller.signal.aborted) return false; // superseded by a newer nav\n throw err;\n }\n\n // If a newer navigation started while we were fetching, bail out.\n if (inFlightNavigation && inFlightNavigation.controller !== controller) {\n if (controller.signal.aborted) return false;\n }\n inFlightNavigation = null;\n\n if (!payload) return false;\n\n const app = document.getElementById(\"app\");\n if (!app) return false;\n\n // Save scroll position in the current history entry before navigating.\n if (push) {\n history.replaceState(\n { n: location.pathname, scroll: window.scrollY },\n \"\",\n location.href,\n );\n }\n\n const current = new URL(location.href);\n const doSwap = () => {\n // Save scroll positions of scrollable elements (e.g. sidebar) before swap\n const scrollables: { el: Element; top: number }[] = [];\n app.querySelectorAll(\"[data-scroll-preserve]\").forEach((el) => {\n scrollables.push({ el, top: el.scrollTop });\n });\n\n // Hoist any stylesheets from the current #app content to <head> before\n // the swap, so they persist and don't cause a flash.\n hoistStyles(app);\n\n // Parse the new body and hoist its styles before injecting, so the\n // browser never sees a frame without styles.\n const temp = document.createElement(\"template\");\n temp.innerHTML = payload.body;\n hoistStyles(temp.content as unknown as HTMLElement);\n\n // Inject the remaining body (styles already moved to <head>)\n app.innerHTML = temp.innerHTML;\n mergeHead(payload.head, payload.title);\n if (payload.clearActionErrorCookie) {\n document.cookie = payload.clearActionErrorCookie;\n }\n if (push) {\n history.pushState({ n: pathname, scroll: 0 }, \"\", pathname + (search || current.search));\n }\n const savedScroll = push ? 0 : (history.state?.scroll ?? 0);\n window.scrollTo(0, savedScroll);\n\n // Restore scroll positions of preserved elements\n for (const s of scrollables) {\n const newEl = app.querySelector(`[data-scroll-preserve=\"${s.el.getAttribute(\"data-scroll-preserve\")}\"]`);\n if (newEl) newEl.scrollTop = s.top;\n }\n\n // Update canonical URL and OG tags for the new route.\n updateCanonicalUrl(pathname, search);\n\n // Announce the navigation to screen readers.\n announceNavigation(pathname);\n\n // Move focus to the main content for keyboard/screen reader users.\n // Only on push (forward navigation), not on back/forward (popstate).\n if (push) moveFocusToContent();\n\n document.dispatchEvent(new CustomEvent(\"elur:rendered\"));\n };\n\n // Use View Transitions when available and the user hasn't opted out.\n const useTransition = supportsViewTransitions() && !prefersReducedMotion();\n if (useTransition) {\n (document as any).startViewTransition(() => doSwap());\n } else {\n doSwap();\n }\n\n return true;\n}\n\n/**\n * Replaces all `<head>` tags marked with `data-elur-head` with the new ones\n * from the server payload. Also updates `document.title` when a title tag is\n * present in the new head.\n */\nfunction mergeHead(head: string | undefined, fallbackTitle: string | undefined): void {\n // Remove existing managed tags.\n const existing = document.querySelectorAll(\"[data-elur-head]\");\n existing.forEach((el) => el.remove());\n\n if (head && head.trim().length > 0) {\n // Parse the head tags from the server and insert them into <head>.\n const parser = document.createElement(\"template\");\n parser.innerHTML = head;\n const fragment = parser.content;\n // Extract the <title> if present and set document.title directly.\n const titleEl = fragment.querySelector(\"title\");\n if (titleEl) {\n document.title = titleEl.textContent ?? \"\";\n titleEl.remove();\n }\n document.head.appendChild(fragment);\n } else if (fallbackTitle) {\n document.title = fallbackTitle;\n }\n}\n\n// --- Link prefetch observers ---\n\n/** Set of links currently being observed for prefetch. */\nconst observedLinks = new WeakSet<HTMLAnchorElement>();\n\n/**\n * Sets up prefetch on internal links. Default is interaction-only (hover or\n * focus) — the same behavior as Astro — so a page load never fires a burst of\n * fetches for every link in the viewport. Links can opt into viewport\n * prefetching with `data-prefetch=\"viewport\"`.\n */\nfunction setupLinkPrefetch(): void {\n const linkInfo = (link: HTMLAnchorElement) => {\n const href = link.getAttribute(\"href\");\n if (!href || href.startsWith(\"#\") || href.startsWith(\"mailto:\") || href.startsWith(\"javascript:\")) {\n return null;\n }\n const qIndex = href.indexOf(\"?\");\n return {\n path: qIndex === -1 ? href : href.slice(0, qIndex),\n search: qIndex === -1 ? \"\" : href.slice(qIndex),\n };\n };\n\n const observeLink = (link: HTMLAnchorElement) => {\n if (observedLinks.has(link)) return;\n if (!isInternalLink(link) || link.hasAttribute(\"data-no-prefetch\")) return;\n observedLinks.add(link);\n\n // Interaction prefetch (default): hover or focus.\n const onInteract = () => {\n const info = linkInfo(link);\n if (info) void prefetch(info.path, info.search);\n };\n link.addEventListener(\"pointerenter\", onInteract, { once: true });\n link.addEventListener(\"focus\", onInteract, { once: true });\n\n // Opt-in viewport prefetch via data-prefetch=\"viewport\".\n if (link.dataset.prefetch === \"viewport\" && \"IntersectionObserver\" in window) {\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n const info = linkInfo(entry.target as HTMLAnchorElement);\n if (info) void prefetch(info.path, info.search);\n observer.disconnect();\n }\n },\n { rootMargin: \"200px\", threshold: 0 },\n );\n observer.observe(link);\n }\n };\n\n const observeLinks = () => {\n const links = document.querySelectorAll<HTMLAnchorElement>(\"a[href]\");\n for (const link of links) observeLink(link);\n };\n\n observeLinks();\n\n // Re-scan when the DOM changes (e.g. after SPA navigation).\n const mutationObserver = new MutationObserver(() => observeLinks());\n mutationObserver.observe(document.body, { childList: true, subtree: true });\n\n // Re-scan after each SPA navigation.\n document.addEventListener(\"elur:rendered\", observeLinks);\n}\n\n// --- Router bootstrap ---\n\nexport function startClientRouter(): void {\n // Static builds emit this marker, so the client never probes the render\n // endpoint (zero 404s on fully static deployments). The meta lives in the\n // initial HTML head and persists across SPA navigations.\n const endpointMeta = document.querySelector<HTMLMetaElement>(\n 'meta[name=\"elur:render-endpoint\"]',\n );\n if (endpointMeta?.getAttribute(\"content\") === \"off\") {\n renderEndpointAvailable = false;\n }\n\n // Hoist styles from #app to <head> immediately on page load.\n // This prevents FOUC on the first SPA navigation.\n const app = document.getElementById(\"app\");\n if (app) hoistStyles(app);\n\n document.addEventListener(\"click\", async (event) => {\n if (!(event instanceof MouseEvent) || hasModifier(event)) return;\n if (event.defaultPrevented) return;\n const link = (event.target as HTMLElement).closest(\"a\");\n if (!link || !isInternalLink(link as HTMLAnchorElement)) return;\n\n const href = link.getAttribute(\"href\");\n if (!href || href.startsWith(\"mailto:\") || href.startsWith(\"javascript:\")) return;\n\n // Handle hash links: scroll to the element if it exists on the page\n if (href.startsWith(\"#\")) {\n if (href.length > 1) {\n const target = document.getElementById(href.slice(1));\n if (target) {\n event.preventDefault();\n target.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n history.replaceState(null, \"\", href);\n }\n }\n return;\n }\n\n event.preventDefault();\n const qIndex = href.indexOf(\"?\");\n const path = qIndex === -1 ? href : href.slice(0, qIndex);\n const search = qIndex === -1 ? \"\" : href.slice(qIndex);\n if (!(await navigateTo(path, search))) {\n location.assign(href);\n }\n });\n\n window.addEventListener(\"popstate\", (event) => {\n const state = event.state as { n?: string; scroll?: number } | null;\n const target = state?.n ?? location.pathname;\n void navigateTo(target, location.search, false);\n });\n\n setupLinkPrefetch();\n}\n\n// --- Test helpers (not part of the public API) ---\n\n/**\n * Resets all internal router state. Intended for test isolation only.\n * @internal\n */\nexport function __resetRouterState(): void {\n prefetchCache.clear();\n inFlightNavigation = null;\n renderEndpointAvailable = true;\n endpointProbe = null;\n}\n\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../../../src/router/client.ts"],"sourcesContent":["/**\n * Client-side router for Elur Kit.\n *\n * Intercepts clicks on internal links, fetches the rendered page body from\n * `/__elur-js/render`, swaps the `#app` content and updates the history state.\n * This is loaded as part of the client bundle instead of being inlined in\n * every HTML page.\n *\n * Features:\n * - SPA navigation with head merge (title, meta, OG tags)\n * - Lifecycle events: `elur:navigate-start` / `elur:navigate-end` /\n * `elur:navigate-error`, plus `elur:before-render` (before the DOM swap) and\n * `elur:rendered` (after it) so entries can clean up and re-hydrate islands\n * - Element persistence via `data-elur-persist=\"key\"` (Astro/Turbo-style)\n * - Optional DOM morphing (idiomorph) behind `router.morph`\n * - Scroll restoration on back/forward (`history.scrollRestoration = \"manual\"`)\n * - Network-aware prefetch on hover/focus/pointerdown (Save-Data and\n * `effectiveType` 2g/slow-2g opt out; `data-prefetch=\"always\"` forces)\n * - Inline `<script>` re-execution and `#elur-data`/`#elur-actions` refresh\n * - View Transitions API with `prefers-reduced-motion` respect\n * - Optional loading indicator (`router.loadingIndicator`)\n */\n\n/**\n * Attribute marking an element whose live DOM node is moved — not re-rendered —\n * across SPA navigations (Astro `transition:persist` / Turbo `permanent`).\n * Matched between the old page and the new payload by the attribute value.\n * Kept as a literal like the other `data-elur-*` hooks below so this module\n * stays import-free for the standalone router chunk.\n */\nconst PERSIST_ATTR = \"data-elur-persist\";\n\ninterface RenderPayload {\n title?: string | null;\n body: string;\n /** Set-Cookie value relayed by the server to clear a consumed action error. */\n clearActionErrorCookie?: string | null;\n /** `<head>` tags (title, meta, OG, twitter) to merge on navigation. */\n head?: string | null;\n /** Serialized contents of `<script id=\"elur-data\">` for the new page. */\n data?: string | null;\n /** Serialized contents of `<script id=\"elur-actions\">` for the new page. */\n actions?: string | null;\n}\n\n/** Detail payload for the `elur:navigate-*` lifecycle events. */\nexport interface NavigationEventDetail {\n /** Pathname being navigated to (no query string). */\n pathname: string;\n /** Query string including `?`, when present. */\n search: string;\n /** True when the payload came from the prefetch cache instead of the network. */\n fromCache: boolean;\n /** True when the navigation was triggered by back/forward (popstate). */\n popstate: boolean;\n}\n\nfunction dispatchNavigateEvent(\n name: \"elur:navigate-start\" | \"elur:navigate-end\" | \"elur:navigate-error\",\n detail: NavigationEventDetail,\n): void {\n document.dispatchEvent(new CustomEvent(name, { detail }));\n}\n\n/**\n * Whether the `/__elur-js/render` endpoint has been detected. Static builds emit\n * `<meta name=\"elur:render-endpoint\" content=\"off\">` so this starts as\n * `false` with zero probe requests. For older builds, a single shared probe\n * determines availability so concurrent prefetches never storm the endpoint.\n */\nlet renderEndpointAvailable = true;\n\n/** Shared in-flight probe promise; at most one request hits the endpoint. */\nlet endpointProbe: Promise<boolean> | null = null;\n\n/** Resolves endpoint availability, caching the result for the page lifetime. */\nfunction resolveEndpointAvailability(): Promise<boolean> {\n if (!renderEndpointAvailable) return Promise.resolve(false);\n if (!endpointProbe) {\n endpointProbe = (async () => {\n const url = new URL(\"/__elur-js/render\", location.origin);\n url.searchParams.set(\"page\", \"/\");\n try {\n const response = await fetch(url.toString(), {\n headers: { Accept: \"application/json\", \"X-Elur-Probe\": \"1\" },\n });\n renderEndpointAvailable = response.ok;\n return response.ok;\n } catch {\n renderEndpointAvailable = false;\n return false;\n }\n })();\n }\n return endpointProbe;\n}\n\nfunction isInternalLink(link: HTMLAnchorElement): boolean {\n return (\n link.tagName === \"A\" &&\n link.hostname === location.hostname &&\n link.target === \"\" &&\n !link.getAttribute(\"download\") &&\n !link.hasAttribute(\"data-no-router\")\n );\n}\n\nfunction hasModifier(event: MouseEvent): boolean {\n return event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;\n}\n\n// --- Prefetch cache ---\n\nconst PREFETCH_TTL_MS = 30_000; // 30 seconds\nconst PREFETCH_CACHE_MAX = 32; // LRU cap — bound memory on long sessions\n\ninterface CacheEntry {\n payload: RenderPayload;\n ts: number;\n}\n\nconst prefetchCache = new Map<string, CacheEntry>();\n\n/** Builds the cache key from pathname + search. */\nfunction cacheKey(pathname: string, search: string): string {\n return pathname + search;\n}\n\n/** Returns a cached payload if fresh, refreshing its LRU recency. */\nfunction getCached(key: string): RenderPayload | undefined {\n const entry = prefetchCache.get(key);\n if (!entry) return undefined;\n if (Date.now() - entry.ts > PREFETCH_TTL_MS) {\n prefetchCache.delete(key);\n return undefined;\n }\n // Refresh recency: re-insert so the Map's insertion order reflects use.\n prefetchCache.delete(key);\n prefetchCache.set(key, entry);\n return entry.payload;\n}\n\n/** Stores a payload in the prefetch cache, evicting the oldest entry at cap. */\nfunction setCached(key: string, payload: RenderPayload): void {\n prefetchCache.delete(key);\n prefetchCache.set(key, { payload, ts: Date.now() });\n while (prefetchCache.size > PREFETCH_CACHE_MAX) {\n const oldest = prefetchCache.keys().next().value;\n if (oldest === undefined) break;\n prefetchCache.delete(oldest);\n }\n}\n\n/**\n * True when prefetching should be skipped to respect constrained networks —\n * quicklink-style guards on Save-Data and slow effective connection types.\n * A link with `data-prefetch=\"always\"` (or a `force` call) bypasses the check.\n */\nfunction prefetchAllowedByNetwork(): boolean {\n const conn = (globalThis.navigator as\n | { connection?: { saveData?: boolean; effectiveType?: string } }\n | undefined)?.connection;\n if (conn?.saveData) return false;\n if (conn?.effectiveType === \"slow-2g\" || conn?.effectiveType === \"2g\") return false;\n return true;\n}\n\n/**\n * Fetches the render payload for a path. Uses the prefetch cache when fresh.\n * Stores the result in the cache for subsequent navigations.\n *\n * On static deployments (no `/__elur-js/render` endpoint), falls back to\n * fetching the full HTML page and extracting `#app` + `<head>` tags.\n *\n * Returns `{ payload, fromCache }` so navigation events can report whether the\n * content came from the prefetch cache or the network.\n */\nasync function fetchPayload(\n pathname: string,\n search: string,\n signal?: AbortSignal,\n): Promise<{ payload: RenderPayload; fromCache: boolean } | undefined> {\n const key = cacheKey(pathname, search);\n const cached = getCached(key);\n if (cached) return { payload: cached, fromCache: true };\n\n // Wait on the shared probe so concurrent prefetches generate at most ONE\n // request against the endpoint (the rest go straight to the HTML fallback).\n if (renderEndpointAvailable) {\n if (await resolveEndpointAvailability()) {\n const payload = await fetchFromRenderEndpoint(pathname, search, signal);\n if (payload) {\n setCached(key, payload);\n return { payload, fromCache: false };\n }\n // The endpoint exists but couldn't render this specific page — fall\n // through to the HTML-based fetch without disabling it globally.\n }\n }\n\n // Static fallback: fetch the full HTML page and extract #app + head.\n const payload = await fetchFromHtml(pathname, search, signal);\n if (payload) {\n setCached(key, payload);\n return { payload, fromCache: false };\n }\n return undefined;\n}\n\n/** Attempts to fetch from the `/__elur-js/render` JSON endpoint. */\nasync function fetchFromRenderEndpoint(pathname: string, search: string, signal?: AbortSignal): Promise<RenderPayload | undefined> {\n const url = new URL(\"/__elur-js/render\", location.origin);\n url.searchParams.set(\"page\", pathname);\n const current = new URL(location.href);\n url.searchParams.set(\"search\", search || current.search);\n\n let response: Response;\n try {\n response = await fetch(url.toString(), { headers: { Accept: \"application/json\" }, signal });\n } catch (err) {\n if (err instanceof DOMException && err.name === \"AbortError\") return undefined;\n return undefined;\n }\n if (!response.ok) return undefined;\n\n let payload: RenderPayload;\n try {\n payload = await response.json();\n } catch {\n return undefined;\n }\n return payload;\n}\n\n/**\n * Static-mode fallback: fetches the full HTML page for the path and extracts\n * the `#app` innerHTML plus managed `<head>` tags (`[data-elur-head]`).\n * Also extracts `<title>`, stylesheets, and headLinks for SPA navigation.\n */\nasync function fetchFromHtml(pathname: string, search: string, signal?: AbortSignal): Promise<RenderPayload | undefined> {\n const fullUrl = pathname + (search || \"\");\n let response: Response;\n try {\n response = await fetch(fullUrl, { headers: { Accept: \"text/html\" }, signal });\n } catch (err) {\n if (err instanceof DOMException && err.name === \"AbortError\") return undefined;\n return undefined;\n }\n if (!response.ok) return undefined;\n\n let html: string;\n try {\n html = await response.text();\n } catch {\n return undefined;\n }\n\n // Parse the full HTML document.\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, \"text/html\");\n\n // Extract #app innerHTML — this is the page body.\n const appEl = doc.getElementById(\"app\");\n if (!appEl) return undefined;\n const body = appEl.innerHTML;\n\n // Extract managed head tags (data-elur-head) for mergeHead.\n const headTags = doc.querySelectorAll(\"[data-elur-head]\");\n let head = \"\";\n for (const tag of headTags) {\n head += tag.outerHTML;\n }\n\n // Also extract headLinks (favicons, manifest, theme-color) so they persist.\n // These don't have data-elur-head, so we grab them separately.\n const linkTags = doc.head.querySelectorAll(\"link[rel='icon'], link[rel='apple-touch-icon'], link[rel='manifest'], meta[name='theme-color']\");\n for (const tag of linkTags) {\n // Skip if already in the current document head\n const href = tag.getAttribute(\"href\");\n if (href && document.head.querySelector(`link[href=\"${href}\"]`)) continue;\n head += tag.outerHTML;\n }\n\n const title = doc.querySelector(\"title\")?.textContent ?? undefined;\n\n // Keep the inert JSON scripts in sync across navigations (same fields the\n // render endpoint returns): without this they stay frozen with the initial\n // page's loader data and action registry.\n const dataEl = doc.getElementById(\"elur-data\");\n const actionsEl = doc.getElementById(\"elur-actions\");\n\n return {\n body,\n head,\n title,\n data: dataEl?.textContent ?? undefined,\n actions: actionsEl?.textContent ?? undefined,\n };\n}\n\n/**\n * Prefetches a path without navigating. Called by the IntersectionObserver\n * when a link enters the viewport, and on hover/focus/pointerdown.\n *\n * Skipped on constrained networks (Save-Data or a 2g-class effective type)\n * unless `force` is set — e.g. `data-prefetch=\"always\"` links.\n */\nexport async function prefetch(\n pathname: string,\n search = \"\",\n options?: { force?: boolean },\n): Promise<void> {\n if (options?.force !== true && !prefetchAllowedByNetwork()) return;\n const key = cacheKey(pathname, search);\n if (prefetchCache.has(key)) {\n // Already cached or in-flight — skip.\n const entry = prefetchCache.get(key)!;\n if (Date.now() - entry.ts <= PREFETCH_TTL_MS) return;\n }\n await fetchPayload(pathname, search);\n}\n\n// --- View Transitions ---\n\n/** Returns true if the user prefers reduced motion. */\nfunction prefersReducedMotion(): boolean {\n return window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches ?? false;\n}\n\n/** Returns true if the View Transitions API is available. */\nfunction supportsViewTransitions(): boolean {\n return typeof (document as any).startViewTransition === \"function\";\n}\n\n// --- Navigation ---\n\n/**\n * Guard against concurrent navigations. When a navigation is in-flight, a new\n * request cancels the previous one (abort + ignore its result). This prevents\n * race conditions where two rapid clicks could swap content out of order.\n */\nlet inFlightNavigation: {\n controller: AbortController;\n pathname: string;\n} | null = null;\n\n/**\n * Cancels any in-flight navigation so a new one can proceed cleanly.\n */\nfunction cancelInFlightNavigation(): void {\n if (inFlightNavigation) {\n inFlightNavigation.controller.abort();\n inFlightNavigation = null;\n }\n}\n\n/**\n * Hoists `<link rel=\"stylesheet\">` and `<style>` tags from inside `#app` into\n * `<head>` so they persist across SPA navigations (prevents FOUC/flashing).\n * Deduplicates by `href` for links and by text content for styles.\n */\nexport function hoistStyles(container: ParentNode): void {\n const links = container.querySelectorAll<HTMLLinkElement>('link[rel=\"stylesheet\"]');\n for (const link of links) {\n const href = link.getAttribute(\"href\");\n if (!href) continue;\n // Already in <head>?\n const existing = document.head.querySelector(`link[rel=\"stylesheet\"][href=\"${href}\"]`);\n if (existing) {\n link.remove();\n continue;\n }\n // Mark as hoisted so we can clean up later if needed\n link.setAttribute(\"data-elur-hoisted\", \"\");\n document.head.appendChild(link);\n }\n\n const styles = container.querySelectorAll<HTMLStyleElement>(\"style\");\n for (const style of styles) {\n const text = style.textContent?.trim();\n if (!text) continue;\n // Check if an identical style already exists in <head>\n const existing = Array.from(document.head.querySelectorAll(\"style\")).find(\n (s) => s.textContent?.trim() === text,\n );\n if (existing) {\n style.remove();\n continue;\n }\n style.setAttribute(\"data-elur-hoisted\", \"\");\n document.head.appendChild(style);\n }\n}\n\n/**\n * Announces a route change to assistive technology via an aria-live region.\n * This is critical for screen reader users who need to know the page content\n * has changed after a SPA navigation.\n */\nfunction announceNavigation(pathname: string): void {\n let liveRegion = document.getElementById(\"elur-route-announcer\");\n if (!liveRegion) {\n liveRegion = document.createElement(\"div\");\n liveRegion.id = \"elur-route-announcer\";\n liveRegion.setAttribute(\"aria-live\", \"assertive\");\n liveRegion.setAttribute(\"aria-atomic\", \"true\");\n liveRegion.setAttribute(\"role\", \"status\");\n // Visually hidden but available to screen readers.\n liveRegion.setAttribute(\"style\", \"position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden;\");\n document.body.appendChild(liveRegion);\n }\n // Clear and re-set so screen readers announce the change.\n liveRegion.textContent = \"\";\n // Use a microtask delay so the DOM update is picked up by AT.\n // Guard against the document being torn down (e.g. in tests).\n const region = liveRegion;\n const timer = setTimeout(() => {\n if (typeof document !== \"undefined\" && region) {\n const title = document.title || pathname;\n region.textContent = title;\n }\n }, 50);\n // Don't keep the process alive just for the announcer.\n if (typeof timer === \"object\" && timer && \"unref\" in timer) {\n (timer as { unref: () => void }).unref();\n }\n}\n\n/**\n * Moves focus to the main content area after a SPA navigation. This follows\n * the WAI-ARIA pattern for route changes: if the #app has a tabindex=-1, focus\n * it; otherwise create a temporary focus target.\n */\nfunction moveFocusToContent(): void {\n const app = document.getElementById(\"app\");\n if (!app) return;\n // Ensure the container is focusable.\n if (!app.hasAttribute(\"tabindex\")) {\n app.setAttribute(\"tabindex\", \"-1\");\n }\n // Remove outline only for mouse users; keyboard users keep it.\n app.focus({ preventScroll: false });\n}\n\n/**\n * Updates the canonical URL and og:url meta tags after navigation.\n */\nfunction updateCanonicalUrl(pathname: string, search: string): void {\n const fullUrl = location.origin + pathname + (search || \"\");\n // Update or create canonical link.\n let canonical = document.querySelector<HTMLLinkElement>('link[rel=\"canonical\"]');\n if (!canonical) {\n canonical = document.createElement(\"link\");\n canonical.rel = \"canonical\";\n document.head.appendChild(canonical);\n }\n canonical.href = fullUrl;\n // Update og:url meta.\n let ogUrl = document.querySelector<HTMLMetaElement>('meta[property=\"og:url\"]');\n if (!ogUrl) {\n ogUrl = document.createElement(\"meta\");\n ogUrl.setAttribute(\"property\", \"og:url\");\n document.head.appendChild(ogUrl);\n }\n ogUrl.content = fullUrl;\n}\n\n// --- Element persistence (data-elur-persist) ---\n\n/**\n * Computes which `[data-elur-persist]` nodes of the current page have a\n * counterpart — same attribute value — in the incoming fragment. The plan is\n * computed before any mutation so listeners of `elur:before-render` know\n * exactly which subtrees will survive the swap (islands inside them are\n * excluded from cleanup).\n */\nfunction planPersistedNodes(app: HTMLElement, fragment: DocumentFragment): { oldEl: Element; newEl: Element }[] {\n const oldNodes = app.querySelectorAll(`[${PERSIST_ATTR}]`);\n if (oldNodes.length === 0) return [];\n const plan: { oldEl: Element; newEl: Element }[] = [];\n for (const oldEl of oldNodes) {\n const key = oldEl.getAttribute(PERSIST_ATTR);\n if (!key) continue;\n // A persisted ancestor already carries this subtree with it.\n if (plan.some((p) => p.oldEl.contains(oldEl))) continue;\n const newEl = fragment.querySelector(`[${PERSIST_ATTR}=\"${cssEscape(key)}\"]`);\n if (!newEl) continue;\n plan.push({ oldEl, newEl });\n }\n return plan;\n}\n\n/** CSS.escape with a minimal fallback for environments lacking it. */\nfunction cssEscape(value: string): string {\n const esc = (globalThis as { CSS?: { escape?: (v: string) => string } }).CSS?.escape;\n return esc ? esc(value) : value.replace(/[\"\\\\\\]]/g, \"\\\\$&\");\n}\n\n/**\n * Moves each persisted node's LIVE element into the position of its match in\n * the new content. `Element.moveBefore()` (Chrome 133+) preserves the state of\n * iframes/video/canvas during the move; `replaceWith` is the fallback.\n *\n * Props conflicts are resolved in favor of the persisted node (v1): the old\n * subtree keeps its own props/state. When an island inside a persisted node\n * would receive different props on the new page, an `elur:persist-props-changed`\n * event bubbles from the island marker so apps can react if they choose to.\n */\nfunction movePersistedNodes(plan: { oldEl: Element; newEl: Element }[]): void {\n for (const { oldEl, newEl } of plan) {\n reportPersistPropChanges(oldEl, newEl);\n const parent = newEl.parentNode as (Node & { moveBefore?: (node: Node, ref: Node) => void }) | null;\n if (parent && typeof parent.moveBefore === \"function\") {\n parent.moveBefore(oldEl, newEl);\n newEl.remove();\n } else {\n newEl.replaceWith(oldEl);\n }\n }\n}\n\n/**\n * Dispatches `elur:persist-props-changed` for islands whose props changed.\n *\n * Known limitation (v1): islands are matched by NAME inside each persisted\n * subtree — two islands with the same registry name inside the same\n * `data-elur-persist` node collide in the props map (last one wins).\n * Persisted nodes are typically singletons (player, sidebar), so this is\n * acceptable for v1.\n */\nfunction reportPersistPropChanges(oldEl: Element, newEl: Element): void {\n const islandsOf = (root: Element) => {\n const list = (root.matches?.(\"[data-elur-island]\") ? [root] : []) as Element[];\n return list.concat(Array.from(root.querySelectorAll(\"[data-elur-island]\")));\n };\n const newPropsByName = new Map<string, string | null>();\n for (const el of islandsOf(newEl)) {\n const name = el.getAttribute(\"data-elur-island\");\n if (name) newPropsByName.set(name, el.getAttribute(\"data-props\"));\n }\n for (const el of islandsOf(oldEl)) {\n const name = el.getAttribute(\"data-elur-island\");\n if (!name) continue;\n const next = newPropsByName.get(name);\n const prev = el.getAttribute(\"data-props\");\n if (next !== undefined && next !== prev) {\n el.dispatchEvent(\n new CustomEvent(\"elur:persist-props-changed\", {\n bubbles: true,\n detail: { name, props: next, previousProps: prev },\n }),\n );\n }\n }\n}\n\n// --- Inline <script> re-execution ---\n\n/**\n * `src` values already executed during this session. Seeded lazily with every\n * script present in the document so a page's own scripts never re-run and the\n * shell's module entries are never re-fetched.\n */\nlet executedScriptSrcs: Set<string> | null = null;\n\nconst EXECUTABLE_SCRIPT_TYPES = new Set([\"\", \"text/javascript\", \"application/javascript\", \"module\"]);\n\n/**\n * Re-executes `<script>` elements inside the swapped content. Scripts injected\n * via innerHTML are inert, so each one is cloned and re-inserted — the same\n * trick Astro uses. External scripts are deduplicated by absolute `src` across\n * navigations; inline scripts run on every navigation. Opt out per script with\n * `data-elur-no-reload`.\n */\nfunction reexecuteScripts(container: ParentNode): void {\n if (!executedScriptSrcs) {\n executedScriptSrcs = new Set(\n Array.from(document.querySelectorAll(\"script[src]\"))\n .map((s) => s.getAttribute(\"src\"))\n .filter((s): s is string => !!s)\n .map((s) => new URL(s, location.href).href),\n );\n }\n for (const script of Array.from(container.querySelectorAll(\"script\"))) {\n if (script.hasAttribute(\"data-elur-no-reload\")) continue;\n if (!EXECUTABLE_SCRIPT_TYPES.has(script.type)) continue;\n const src = script.getAttribute(\"src\");\n if (src) {\n const abs = new URL(src, location.href).href;\n if (executedScriptSrcs.has(abs)) continue;\n executedScriptSrcs.add(abs);\n }\n const clone = document.createElement(\"script\");\n for (const attr of Array.from(script.attributes)) {\n clone.setAttribute(attr.name, attr.value);\n }\n clone.textContent = script.textContent;\n script.replaceWith(clone);\n }\n}\n\n/**\n * Replaces the contents of the inert `<script id=\"...\">` JSON payloads so they\n * reflect the current page. When the new payload lacks the field, a stale\n * element is removed rather than left frozen.\n */\nfunction syncJsonScript(id: string, content: string | undefined): void {\n const existing = document.getElementById(id);\n if (content === undefined) {\n existing?.remove();\n return;\n }\n if (existing) {\n existing.textContent = content;\n return;\n }\n const el = document.createElement(\"script\");\n el.type = \"application/json\";\n el.id = id;\n el.textContent = content;\n document.body.appendChild(el);\n}\n\n// --- DOM morphing (opt-in, idiomorph) ---\n\nlet morphEnabled = false;\n\n/**\n * True for nodes the morpher must treat as opaque: hydrated islands (they own\n * their DOM through signals) and persisted subtrees (moved verbatim by the\n * persistence pass — re-morphing them would defeat the point).\n */\nfunction isOpaqueToMorph(node: Node): boolean {\n if (node instanceof Element) {\n if (node.hasAttribute(PERSIST_ATTR)) return true;\n if (typeof (node as { __elur_js_island_dispose?: unknown }).__elur_js_island_dispose === \"function\") {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Morphs `app` children toward the new content using idiomorph instead of a\n * wholesale `replaceChildren`. Only reached when `router.morph` is enabled.\n * On any failure the caller falls back to the plain swap.\n */\nasync function morphApp(app: HTMLElement, fragment: DocumentFragment): Promise<void> {\n const { Idiomorph } = await import(\"idiomorph\");\n // Wrap the fragment in a dummy parent: idiomorph's innerHTML style morphs\n // the element's children against another parent's children.\n const wrapper = document.createElement(\"div\");\n wrapper.appendChild(fragment);\n Idiomorph.morph(app, wrapper, {\n morphStyle: \"innerHTML\",\n callbacks: {\n beforeNodeMorphed: (node) => (isOpaqueToMorph(node) ? false : undefined),\n },\n });\n}\n\n/**\n * Navigates to a page without a full reload: fetches the fresh body from the\n * `/__elur-js/render` endpoint, swaps `#app`, updates the document title and\n * dispatches `elur:before-render` + `elur:rendered` so islands are cleaned up\n * and re-hydrated. Used by the router on clicks and available for programmatic\n * navigation (e.g. after a server action returns a redirect, so the target\n * page shows fresh server data).\n *\n * Concurrent navigations are handled: a new navigateTo() cancels any\n * in-flight navigation to prevent out-of-order content swaps.\n *\n * Lifecycle events (`elur:navigate-start` / `elur:navigate-end` /\n * `elur:navigate-error`) carry `{ pathname, search, fromCache, popstate }` —\n * enough for a loading indicator in a few lines of user code.\n *\n * @param pathname Path without query, e.g. \"/movies/inception\".\n * @param search Query string, e.g. \"?reviewed=1\" (optional).\n * @param push Whether to push a history entry (default true).\n * @returns true on success, false if the render failed.\n */\nexport async function navigateTo(pathname: string, search = \"\", push = true): Promise<boolean> {\n const popstate = !push;\n // Cancel any previous in-flight navigation to prevent race conditions.\n cancelInFlightNavigation();\n\n const controller = new AbortController();\n inFlightNavigation = { controller, pathname };\n\n const detailBase = { pathname, search, popstate };\n dispatchNavigateEvent(\"elur:navigate-start\", {\n ...detailBase,\n fromCache: getCached(cacheKey(pathname, search)) !== undefined,\n });\n\n let result: { payload: RenderPayload; fromCache: boolean } | undefined;\n try {\n result = await fetchPayload(pathname, search, controller.signal);\n } catch (err) {\n // Superseded navigations stay silent: the newer navigation already\n // emitted its own navigate-start and owns the lifecycle events — an error\n // here would wrongly resolve it (e.g. hide a loading indicator mid-nav).\n if (controller.signal.aborted) return false;\n dispatchNavigateEvent(\"elur:navigate-error\", { ...detailBase, fromCache: false });\n throw err;\n }\n\n // If a newer navigation started while we were fetching, bail out silently.\n if (inFlightNavigation && inFlightNavigation.controller !== controller) {\n if (controller.signal.aborted) return false;\n }\n inFlightNavigation = null;\n\n if (!result) {\n dispatchNavigateEvent(\"elur:navigate-error\", { ...detailBase, fromCache: false });\n return false;\n }\n const { payload, fromCache } = result;\n\n const app = document.getElementById(\"app\");\n if (!app) {\n dispatchNavigateEvent(\"elur:navigate-error\", { ...detailBase, fromCache });\n return false;\n }\n\n // Save scroll position in the current history entry before navigating.\n if (push) {\n history.replaceState(\n { n: location.pathname, scroll: window.scrollY },\n \"\",\n location.href,\n );\n }\n\n const current = new URL(location.href);\n const doSwap = async () => {\n // Save scroll positions of scrollable elements (e.g. sidebar) before swap\n const scrollables: { el: Element; top: number }[] = [];\n app.querySelectorAll(\"[data-scroll-preserve]\").forEach((el) => {\n scrollables.push({ el, top: el.scrollTop });\n });\n\n // Hoist any stylesheets from the current #app content to <head> before\n // the swap, so they persist and don't cause a flash.\n hoistStyles(app);\n\n // Parse the new body and hoist its styles before injecting, so the\n // browser never sees a frame without styles.\n const buildFragment = () => {\n const t = document.createElement(\"template\");\n t.innerHTML = payload.body;\n hoistStyles(t.content as unknown as HTMLElement);\n return t.content;\n };\n const tempContent = buildFragment();\n\n // Persistence pass: decide which live nodes move into the new content.\n const persistPlan = planPersistedNodes(app, tempContent);\n\n // Notify listeners BEFORE touching the DOM: this is where the client\n // entry disposes islands (still attached, so cleanup can read live DOM)\n // except the ones inside persisted nodes.\n document.dispatchEvent(\n new CustomEvent(\"elur:before-render\", {\n detail: { ...detailBase, persisted: persistPlan.map((p) => p.oldEl) },\n }),\n );\n\n movePersistedNodes(persistPlan);\n\n if (morphEnabled) {\n try {\n await morphApp(app, tempContent);\n } catch {\n // morphApp consumes the fragment (persisted nodes may already be\n // inside it): rebuild fresh content and rescue the live persisted\n // nodes from the plan — wherever the failed morph left them.\n const fresh = buildFragment();\n const rescue: { oldEl: Element; newEl: Element }[] = [];\n for (const newEl of fresh.querySelectorAll(`[${PERSIST_ATTR}]`)) {\n const key = newEl.getAttribute(PERSIST_ATTR);\n const oldEl = persistPlan.find(\n (p) => p.oldEl.getAttribute(PERSIST_ATTR) === key,\n )?.oldEl;\n if (oldEl) rescue.push({ oldEl, newEl });\n }\n movePersistedNodes(rescue);\n app.replaceChildren(fresh);\n }\n } else {\n app.replaceChildren(tempContent);\n }\n mergeHead(payload.head, payload.title);\n if (payload.clearActionErrorCookie) {\n document.cookie = payload.clearActionErrorCookie;\n }\n if (push) {\n history.pushState({ n: pathname, scroll: 0 }, \"\", pathname + (search || current.search));\n }\n const savedScroll = push ? 0 : (history.state?.scroll ?? 0);\n window.scrollTo(0, savedScroll);\n\n // Restore scroll positions of preserved elements\n for (const s of scrollables) {\n const newEl = app.querySelector(`[data-scroll-preserve=\"${s.el.getAttribute(\"data-scroll-preserve\")}\"]`);\n if (newEl) newEl.scrollTop = s.top;\n }\n\n // Update canonical URL and OG tags for the new route.\n updateCanonicalUrl(pathname, search);\n\n // Refresh the inert JSON payloads (#elur-data / #elur-actions).\n syncJsonScript(\"elur-data\", payload.data ?? undefined);\n syncJsonScript(\"elur-actions\", payload.actions ?? undefined);\n\n // Re-execute page scripts that innerHTML left inert.\n reexecuteScripts(app);\n\n // Announce the navigation to screen readers.\n announceNavigation(pathname);\n\n // Move focus to the main content for keyboard/screen reader users.\n // Only on push (forward navigation), not on back/forward (popstate).\n if (push) moveFocusToContent();\n\n document.dispatchEvent(new CustomEvent(\"elur:rendered\", { detail: detailBase }));\n };\n\n // Use View Transitions when available and the user hasn't opted out.\n const useTransition = supportsViewTransitions() && !prefersReducedMotion();\n if (useTransition) {\n const vt = (document as any).startViewTransition(() => doSwap());\n // navigate-end must fire after the DOM update, not when the animation\n // finishes — updateCallbackDone resolves once the swap callback ran.\n // It rejects when the transition is skipped (e.g. hidden tab): the swap\n // still happened, so the rejection is not a navigation error.\n try {\n await vt.updateCallbackDone;\n } catch {\n // skipped transition — the update callback already ran or was skipped\n }\n } else {\n await doSwap();\n }\n\n dispatchNavigateEvent(\"elur:navigate-end\", { ...detailBase, fromCache });\n return true;\n}\n\n/**\n * Replaces all `<head>` tags marked with `data-elur-head` with the new ones\n * from the server payload. Also updates `document.title` when a title tag is\n * present in the new head.\n */\nfunction mergeHead(head: string | null | undefined, fallbackTitle: string | null | undefined): void {\n // Remove existing managed tags.\n const existing = document.querySelectorAll(\"[data-elur-head]\");\n existing.forEach((el) => el.remove());\n\n if (head && head.trim().length > 0) {\n // Parse the head tags from the server and insert them into <head>.\n const parser = document.createElement(\"template\");\n parser.innerHTML = head;\n const fragment = parser.content;\n // Extract the <title> if present and set document.title directly.\n const titleEl = fragment.querySelector(\"title\");\n if (titleEl) {\n document.title = titleEl.textContent ?? \"\";\n titleEl.remove();\n }\n document.head.appendChild(fragment);\n } else if (fallbackTitle) {\n document.title = fallbackTitle;\n }\n}\n\n// --- Link prefetch observers ---\n\n/** Set of links currently being observed for prefetch. */\nconst observedLinks = new WeakSet<HTMLAnchorElement>();\n\n/**\n * Sets up prefetch on internal links. Default is interaction-only (hover or\n * focus) — the same behavior as Astro — so a page load never fires a burst of\n * fetches for every link in the viewport. Links can opt into viewport\n * prefetching with `data-prefetch=\"viewport\"`.\n */\nfunction setupLinkPrefetch(): void {\n const linkInfo = (link: HTMLAnchorElement) => {\n const href = link.getAttribute(\"href\");\n if (!href || href.startsWith(\"#\") || href.startsWith(\"mailto:\") || href.startsWith(\"javascript:\")) {\n return null;\n }\n const qIndex = href.indexOf(\"?\");\n return {\n path: qIndex === -1 ? href : href.slice(0, qIndex),\n search: qIndex === -1 ? \"\" : href.slice(qIndex),\n };\n };\n\n const observeLink = (link: HTMLAnchorElement) => {\n if (observedLinks.has(link)) return;\n if (!isInternalLink(link) || link.hasAttribute(\"data-no-prefetch\")) return;\n observedLinks.add(link);\n\n // Interaction prefetch (default): hover, focus or pointerdown. The\n // pointerdown listener is the \"tap\" strategy — it starts the fetch\n // earlier than the click on touch devices.\n const onInteract = () => {\n const info = linkInfo(link);\n if (info) void prefetch(info.path, info.search, { force: link.dataset.prefetch === \"always\" });\n };\n link.addEventListener(\"pointerenter\", onInteract, { once: true });\n link.addEventListener(\"focus\", onInteract, { once: true });\n link.addEventListener(\"pointerdown\", onInteract, { once: true });\n\n // Opt-in viewport prefetch via data-prefetch=\"viewport\".\n if (link.dataset.prefetch === \"viewport\" && \"IntersectionObserver\" in window) {\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n const info = linkInfo(entry.target as HTMLAnchorElement);\n if (info) void prefetch(info.path, info.search);\n observer.disconnect();\n }\n },\n { rootMargin: \"200px\", threshold: 0 },\n );\n observer.observe(link);\n }\n };\n\n const observeLinks = () => {\n const links = document.querySelectorAll<HTMLAnchorElement>(\"a[href]\");\n for (const link of links) observeLink(link);\n };\n\n observeLinks();\n\n // Re-scan when the DOM changes (e.g. after SPA navigation).\n const mutationObserver = new MutationObserver(() => observeLinks());\n mutationObserver.observe(document.body, { childList: true, subtree: true });\n\n // Re-scan after each SPA navigation.\n document.addEventListener(\"elur:rendered\", observeLinks);\n}\n\n// --- Loading indicator (opt-in) ---\n\nconst LOADING_INDICATOR_DELAY_MS = 200;\nconst LOADING_INDICATOR_ID = \"elur-loading-indicator\";\n\n/**\n * Progress-bar style loading indicator driven by the `elur:navigate-*`\n * lifecycle events. Hidden by default; appears only when a navigation takes\n * longer than ~200 ms (fast navigations never flash it) and is skipped for\n * cache hits. Honors `prefers-reduced-motion` by showing a static bar\n * instead of a trickle animation.\n */\nfunction startLoadingIndicator(): void {\n if (typeof document === \"undefined\") return;\n let bar: HTMLDivElement | null = null;\n let showTimer: ReturnType<typeof setTimeout> | null = null;\n let trickleTimer: ReturnType<typeof setInterval> | null = null;\n let progress = 0;\n\n const reducedMotion = () => prefersReducedMotion();\n\n const paint = () => {\n if (!bar) return;\n bar.style.transform = `scaleX(${Math.min(progress, 100) / 100})`;\n };\n\n const show = () => {\n if (bar) return;\n bar = document.createElement(\"div\");\n bar.id = LOADING_INDICATOR_ID;\n // Decorative only — AT users get the aria-live route announcer instead,\n // so a role would be redundant and aria-hidden would contradict it.\n bar.setAttribute(\"aria-hidden\", \"true\");\n Object.assign(bar.style, {\n position: \"fixed\",\n top: \"0\",\n left: \"0\",\n right: \"0\",\n height: \"2px\",\n background: \"currentColor\",\n color: \"var(--elur-loading-color, #4f7cff)\",\n transform: \"scaleX(0)\",\n transformOrigin: \"0 50%\",\n transition: reducedMotion() ? \"none\" : \"transform 150ms ease-out, opacity 200ms ease-in\",\n zIndex: \"2147483647\",\n pointerEvents: \"none\",\n opacity: \"1\",\n } satisfies Partial<CSSStyleDeclaration>);\n document.body.appendChild(bar);\n progress = 10;\n paint();\n if (!reducedMotion()) {\n // NProgress-style trickle: approach ~80% asymptotically while the\n // navigation is still in flight.\n trickleTimer = setInterval(() => {\n progress += (80 - progress) * 0.12;\n paint();\n }, 150);\n }\n };\n\n const finish = () => {\n if (showTimer) {\n clearTimeout(showTimer);\n showTimer = null;\n }\n if (trickleTimer) {\n clearInterval(trickleTimer);\n trickleTimer = null;\n }\n if (!bar) return;\n progress = 100;\n paint();\n const el = bar;\n bar = null;\n setTimeout(() => {\n el.style.opacity = \"0\";\n setTimeout(() => el.remove(), reducedMotion() ? 0 : 200);\n }, 120);\n };\n\n document.addEventListener(\"elur:navigate-start\", (event) => {\n const detail = (event as CustomEvent).detail as NavigationEventDetail | undefined;\n if (detail?.fromCache) return; // cache hits are instant — no indicator\n finish(); // reset any previous pending state\n showTimer = setTimeout(show, LOADING_INDICATOR_DELAY_MS);\n });\n document.addEventListener(\"elur:navigate-end\", finish);\n document.addEventListener(\"elur:navigate-error\", finish);\n}\n\n// --- Router bootstrap ---\n\nexport interface ClientRouterOptions {\n /**\n * Link prefetching on hover/focus/pointerdown (and opt-in viewport).\n * Default: `true`. Pass `false` to disable all prefetch traffic.\n */\n prefetch?: boolean;\n /**\n * Swap `#app` with idiomorph-based DOM morphing instead of a wholesale\n * replace. Experimental — hydrated islands and `data-elur-persist` nodes\n * are treated as opaque. Default: `false`.\n */\n morph?: boolean;\n /**\n * Show a minimal top progress bar on navigations slower than ~200 ms.\n * Default: `false`.\n */\n loadingIndicator?: boolean;\n}\n\n/** Guard: the router is a singleton — starting it twice must not double-bind listeners. */\nlet routerStarted = false;\n\nexport function startClientRouter(options: ClientRouterOptions = {}): void {\n // Option-derived flags are re-applied on every call — after Vite HMR\n // re-runs the generated entry, changed flags (e.g. router.morph) take\n // effect without a full reload. Listener-bound behavior (prefetch,\n // loading indicator) is registered once and still requires a reload.\n morphEnabled = options.morph === true;\n if (routerStarted) return;\n routerStarted = true;\n\n // We manage scroll ourselves (save/restore per history entry); tell the\n // browser not to interfere on back/forward.\n if (\"scrollRestoration\" in history) {\n history.scrollRestoration = \"manual\";\n }\n\n // Static builds emit this marker, so the client never probes the render\n // endpoint (zero 404s on fully static deployments). The meta lives in the\n // initial HTML head and persists across SPA navigations.\n const endpointMeta = document.querySelector<HTMLMetaElement>(\n 'meta[name=\"elur:render-endpoint\"]',\n );\n if (endpointMeta?.getAttribute(\"content\") === \"off\") {\n renderEndpointAvailable = false;\n }\n\n // Hoist styles from #app to <head> immediately on page load.\n // This prevents FOUC on the first SPA navigation.\n const app = document.getElementById(\"app\");\n if (app) hoistStyles(app);\n\n document.addEventListener(\"click\", async (event) => {\n if (!(event instanceof MouseEvent) || hasModifier(event)) return;\n if (event.defaultPrevented) return;\n const link = (event.target as HTMLElement).closest(\"a\");\n if (!link || !isInternalLink(link as HTMLAnchorElement)) return;\n\n const href = link.getAttribute(\"href\");\n if (!href || href.startsWith(\"mailto:\") || href.startsWith(\"javascript:\")) return;\n\n // Handle hash links: scroll to the element if it exists on the page\n if (href.startsWith(\"#\")) {\n if (href.length > 1) {\n const target = document.getElementById(href.slice(1));\n if (target) {\n event.preventDefault();\n target.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n history.replaceState(null, \"\", href);\n }\n }\n return;\n }\n\n event.preventDefault();\n const qIndex = href.indexOf(\"?\");\n const path = qIndex === -1 ? href : href.slice(0, qIndex);\n const search = qIndex === -1 ? \"\" : href.slice(qIndex);\n if (!(await navigateTo(path, search))) {\n location.assign(href);\n }\n });\n\n window.addEventListener(\"popstate\", (event) => {\n const state = event.state as { n?: string; scroll?: number } | null;\n const target = state?.n ?? location.pathname;\n void navigateTo(target, location.search, false);\n });\n\n if (options.prefetch !== false) setupLinkPrefetch();\n if (options.loadingIndicator === true) startLoadingIndicator();\n}\n\n// --- Test helpers (not part of the public API) ---\n\n/**\n * Resets all internal router state. Intended for test isolation only.\n * @internal\n */\nexport function __resetRouterState(): void {\n prefetchCache.clear();\n inFlightNavigation = null;\n renderEndpointAvailable = true;\n endpointProbe = null;\n executedScriptSrcs = null;\n morphEnabled = false;\n routerStarted = false;\n}\n\n"],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface RedirectRule {
|
|
2
|
+
/** Source path pattern (supports :param and *). */
|
|
3
|
+
from: string;
|
|
4
|
+
/** Destination path (supports :param interpolation). */
|
|
5
|
+
to: string;
|
|
6
|
+
/** HTTP status code (301, 302, 307, 308). Default: 308. */
|
|
7
|
+
status?: 301 | 302 | 307 | 308;
|
|
8
|
+
}
|
|
9
|
+
export interface RewriteRule {
|
|
10
|
+
/** Source path pattern (supports :param and *). */
|
|
11
|
+
from: string;
|
|
12
|
+
/** Destination path (supports :param interpolation). */
|
|
13
|
+
to: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RouteHeadersRule {
|
|
16
|
+
/** Path pattern to match (supports :param and *). */
|
|
17
|
+
path: string;
|
|
18
|
+
/** Headers to apply to matching responses. */
|
|
19
|
+
headers: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Checks if a pathname matches a redirect rule and returns the redirect
|
|
23
|
+
* Response if so.
|
|
24
|
+
*/
|
|
25
|
+
export declare function matchRedirect(pathname: string, rules: RedirectRule[]): Response | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Checks if a pathname matches a rewrite rule and returns the rewritten
|
|
28
|
+
* pathname if so.
|
|
29
|
+
*/
|
|
30
|
+
export declare function matchRewrite(pathname: string, rules: RewriteRule[]): string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Returns headers that should be applied to a response for the given pathname.
|
|
33
|
+
*/
|
|
34
|
+
export declare function matchRouteHeaders(pathname: string, rules: RouteHeadersRule[]): Record<string, string> | undefined;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface RedirectRule {
|
|
2
|
+
/** Source path pattern (supports :param and *). */
|
|
3
|
+
from: string;
|
|
4
|
+
/** Destination path (supports :param interpolation). */
|
|
5
|
+
to: string;
|
|
6
|
+
/** HTTP status code (301, 302, 307, 308). Default: 308. */
|
|
7
|
+
status?: 301 | 302 | 307 | 308;
|
|
8
|
+
}
|
|
9
|
+
export interface RewriteRule {
|
|
10
|
+
/** Source path pattern (supports :param and *). */
|
|
11
|
+
from: string;
|
|
12
|
+
/** Destination path (supports :param interpolation). */
|
|
13
|
+
to: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RouteHeadersRule {
|
|
16
|
+
/** Path pattern to match (supports :param and *). */
|
|
17
|
+
path: string;
|
|
18
|
+
/** Headers to apply to matching responses. */
|
|
19
|
+
headers: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Checks if a pathname matches a redirect rule and returns the redirect
|
|
23
|
+
* Response if so.
|
|
24
|
+
*/
|
|
25
|
+
export declare function matchRedirect(pathname: string, rules: RedirectRule[]): Response | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Checks if a pathname matches a rewrite rule and returns the rewritten
|
|
28
|
+
* pathname if so.
|
|
29
|
+
*/
|
|
30
|
+
export declare function matchRewrite(pathname: string, rules: RewriteRule[]): string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Returns headers that should be applied to a response for the given pathname.
|
|
33
|
+
*/
|
|
34
|
+
export declare function matchRouteHeaders(pathname: string, rules: RouteHeadersRule[]): Record<string, string> | undefined;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { type CacheAdapter } from "../cache/adapter.js";
|
|
2
|
+
import { type LogLevel } from "./logger.js";
|
|
3
|
+
import { type RedirectRule, type RewriteRule, type RouteHeadersRule } from "../router/redirects.js";
|
|
4
|
+
import { type LoadedMiddleware } from "../middleware/index.js";
|
|
5
|
+
import { type AdapterCapabilities } from "./capabilities.js";
|
|
1
6
|
import type { SecurityHeadersConfig } from "../config/index.js";
|
|
2
7
|
export interface WebHandlerOptions {
|
|
3
8
|
/** Static file root (absolute path). Usually the build output directory. */
|
|
@@ -18,6 +23,60 @@ export interface WebHandlerOptions {
|
|
|
18
23
|
renderEndpoint?: boolean;
|
|
19
24
|
/** Security headers config (runtime-security §14). `false` disables. */
|
|
20
25
|
securityHeaders?: SecurityHeadersConfig | false;
|
|
26
|
+
/** Minimum log level for the per-request structured logger. */
|
|
27
|
+
logLevel?: LogLevel;
|
|
28
|
+
/**
|
|
29
|
+
* Pluggable ISR cache adapter. When omitted and `cacheDir` is set, a
|
|
30
|
+
* filesystem adapter is created and shared per `cacheDir` for the process.
|
|
31
|
+
*/
|
|
32
|
+
cacheAdapter?: CacheAdapter;
|
|
33
|
+
/** Redirect rules evaluated before any routing (first match wins). */
|
|
34
|
+
redirects?: RedirectRule[];
|
|
35
|
+
/** Rewrite rules: transparently change the pathname used for routing. */
|
|
36
|
+
rewrites?: RewriteRule[];
|
|
37
|
+
/** Extra response headers applied to matching request paths. */
|
|
38
|
+
routeHeaders?: RouteHeadersRule[];
|
|
39
|
+
/**
|
|
40
|
+
* Opt-in streaming SSR (experimental). When `true`, dynamic routes with a
|
|
41
|
+
* `loading` boundary are served as a real stream: the document shell plus
|
|
42
|
+
* the loading fallback go out immediately, and the resolved content arrives
|
|
43
|
+
* as a follow-up chunk that swaps the boundary in-place. Streamed responses
|
|
44
|
+
* bypass the ISR cache (they always render live) and send
|
|
45
|
+
* `Cache-Control: no-store` + `X-Accel-Buffering: no`. Routes without a
|
|
46
|
+
* loading boundary render buffered exactly as before.
|
|
47
|
+
*/
|
|
48
|
+
streaming?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Host capabilities used to gate streaming. Defaults to
|
|
51
|
+
* `DEFAULT_CAPABILITIES` (a full Node/Bun process). Adapters for hosts
|
|
52
|
+
* without streaming support should pass their own capabilities so
|
|
53
|
+
* `streaming: true` degrades to buffered rendering instead of breaking.
|
|
54
|
+
*/
|
|
55
|
+
capabilities?: AdapterCapabilities;
|
|
56
|
+
/**
|
|
57
|
+
* User middleware (the project's `src/middleware.ts`, loaded by the caller
|
|
58
|
+
* with `loadMiddleware`). Runs after redirects/rewrites and the internal
|
|
59
|
+
* endpoints, before API/static/SSR routing. A returned Response
|
|
60
|
+
* short-circuits the pipeline; `next({ headers, locals })` merges headers
|
|
61
|
+
* into the downstream request and exposes `locals` to API routes.
|
|
62
|
+
*/
|
|
63
|
+
middleware?: LoadedMiddleware;
|
|
64
|
+
/**
|
|
65
|
+
* Client router options affecting SSR output: `enabled` controls the
|
|
66
|
+
* render-endpoint marker and whether a page without islands ships any JS;
|
|
67
|
+
* `entry` is the public URL of the split router chunk (e.g.
|
|
68
|
+
* `/_elur/router.js`) when the client bundle was built with separate
|
|
69
|
+
* entry/router inputs.
|
|
70
|
+
*/
|
|
71
|
+
router?: {
|
|
72
|
+
enabled?: boolean;
|
|
73
|
+
entry?: string;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Client JS mode. `"legacy"` restores the pre-0%-JS behavior: the combined
|
|
77
|
+
* client entry is emitted unconditionally on every page.
|
|
78
|
+
*/
|
|
79
|
+
js?: "modern" | "legacy";
|
|
21
80
|
}
|
|
22
81
|
export interface WebHandlerRouteTable {
|
|
23
82
|
pages: import("../router/route-scanner.js").PageRoute[];
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { type CacheAdapter } from "../cache/adapter.js";
|
|
2
|
+
import { type LogLevel } from "./logger.js";
|
|
3
|
+
import { type RedirectRule, type RewriteRule, type RouteHeadersRule } from "../router/redirects.js";
|
|
4
|
+
import { type LoadedMiddleware } from "../middleware/index.js";
|
|
5
|
+
import { type AdapterCapabilities } from "./capabilities.js";
|
|
1
6
|
import type { SecurityHeadersConfig } from "../config/index.js";
|
|
2
7
|
export interface WebHandlerOptions {
|
|
3
8
|
/** Static file root (absolute path). Usually the build output directory. */
|
|
@@ -18,6 +23,60 @@ export interface WebHandlerOptions {
|
|
|
18
23
|
renderEndpoint?: boolean;
|
|
19
24
|
/** Security headers config (runtime-security §14). `false` disables. */
|
|
20
25
|
securityHeaders?: SecurityHeadersConfig | false;
|
|
26
|
+
/** Minimum log level for the per-request structured logger. */
|
|
27
|
+
logLevel?: LogLevel;
|
|
28
|
+
/**
|
|
29
|
+
* Pluggable ISR cache adapter. When omitted and `cacheDir` is set, a
|
|
30
|
+
* filesystem adapter is created and shared per `cacheDir` for the process.
|
|
31
|
+
*/
|
|
32
|
+
cacheAdapter?: CacheAdapter;
|
|
33
|
+
/** Redirect rules evaluated before any routing (first match wins). */
|
|
34
|
+
redirects?: RedirectRule[];
|
|
35
|
+
/** Rewrite rules: transparently change the pathname used for routing. */
|
|
36
|
+
rewrites?: RewriteRule[];
|
|
37
|
+
/** Extra response headers applied to matching request paths. */
|
|
38
|
+
routeHeaders?: RouteHeadersRule[];
|
|
39
|
+
/**
|
|
40
|
+
* Opt-in streaming SSR (experimental). When `true`, dynamic routes with a
|
|
41
|
+
* `loading` boundary are served as a real stream: the document shell plus
|
|
42
|
+
* the loading fallback go out immediately, and the resolved content arrives
|
|
43
|
+
* as a follow-up chunk that swaps the boundary in-place. Streamed responses
|
|
44
|
+
* bypass the ISR cache (they always render live) and send
|
|
45
|
+
* `Cache-Control: no-store` + `X-Accel-Buffering: no`. Routes without a
|
|
46
|
+
* loading boundary render buffered exactly as before.
|
|
47
|
+
*/
|
|
48
|
+
streaming?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Host capabilities used to gate streaming. Defaults to
|
|
51
|
+
* `DEFAULT_CAPABILITIES` (a full Node/Bun process). Adapters for hosts
|
|
52
|
+
* without streaming support should pass their own capabilities so
|
|
53
|
+
* `streaming: true` degrades to buffered rendering instead of breaking.
|
|
54
|
+
*/
|
|
55
|
+
capabilities?: AdapterCapabilities;
|
|
56
|
+
/**
|
|
57
|
+
* User middleware (the project's `src/middleware.ts`, loaded by the caller
|
|
58
|
+
* with `loadMiddleware`). Runs after redirects/rewrites and the internal
|
|
59
|
+
* endpoints, before API/static/SSR routing. A returned Response
|
|
60
|
+
* short-circuits the pipeline; `next({ headers, locals })` merges headers
|
|
61
|
+
* into the downstream request and exposes `locals` to API routes.
|
|
62
|
+
*/
|
|
63
|
+
middleware?: LoadedMiddleware;
|
|
64
|
+
/**
|
|
65
|
+
* Client router options affecting SSR output: `enabled` controls the
|
|
66
|
+
* render-endpoint marker and whether a page without islands ships any JS;
|
|
67
|
+
* `entry` is the public URL of the split router chunk (e.g.
|
|
68
|
+
* `/_elur/router.js`) when the client bundle was built with separate
|
|
69
|
+
* entry/router inputs.
|
|
70
|
+
*/
|
|
71
|
+
router?: {
|
|
72
|
+
enabled?: boolean;
|
|
73
|
+
entry?: string;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Client JS mode. `"legacy"` restores the pre-0%-JS behavior: the combined
|
|
77
|
+
* client entry is emitted unconditionally on every page.
|
|
78
|
+
*/
|
|
79
|
+
js?: "modern" | "legacy";
|
|
21
80
|
}
|
|
22
81
|
export interface WebHandlerRouteTable {
|
|
23
82
|
pages: import("../router/route-scanner.js").PageRoute[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../node-http-
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../node-http-Uct7DhmN.cjs"),t=require("../runtime-ChE4flX3.cjs"),n=require("../capabilities-C0TDQoMY.cjs");exports.DEFAULT_CAPABILITIES=n.t,exports.DEFAULT_SECURITY_HEADERS=t.i,exports.EDGE_CAPABILITIES=n.n,exports.RequestContext=t.s,exports.SERVERLESS_CAPABILITIES=n.r,exports.StructuredLogger=t.n,exports.applySecurityHeaders=t.a,exports.buildSecurityHeaders=t.o,exports.createCapabilities=n.i,exports.createRequestLogger=t.r,exports.createWebHandler=t.t,exports.guessContentType=t.c,exports.htmlResponse=t.l,exports.incomingMessageToRequest=e.t,exports.jsonResponse=t.u,exports.methodNotAllowed=t.d,exports.notFound=t.f,exports.resolveStaticFile=t.g,exports.sendWebResponse=e.n,exports.serveStaticFile=t.p,exports.serverError=t.m,exports.supportsPersistentStorage=n.a,exports.supportsStreaming=n.o,exports.supportsWritableFilesystem=n.s,exports.textResponse=t.h,exports.validateCapabilities=n.c;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { RequestContext, type RequestContextOptions, type RouteTable, type CookieJar, type ResponseCookieJar, type ResponseState, type CookieOptions, htmlResponse, jsonResponse, textResponse, notFound, methodNotAllowed, serverError, guessContentType, serveStaticFile, } from "./context.js";
|
|
2
2
|
export { createWebHandler, type WebHandlerOptions, type WebHandlerRouteTable, type WebHandlerActionRegistry, type CreateWebHandlerResult, } from "./handler.js";
|
|
3
3
|
export { resolveStaticFile } from "./static.js";
|
|
4
|
-
export { incomingMessageToRequest } from "./node-http.js";
|
|
4
|
+
export { incomingMessageToRequest, sendWebResponse } from "./node-http.js";
|
|
5
5
|
export { buildSecurityHeaders, applySecurityHeaders, DEFAULT_SECURITY_HEADERS, } from "./security-headers.js";
|
|
6
|
+
export { StructuredLogger, createRequestLogger, type LogLevel, type LogEntry, type ServerTimingMetric, } from "./logger.js";
|
|
6
7
|
export { DEFAULT_CAPABILITIES, SERVERLESS_CAPABILITIES, EDGE_CAPABILITIES, createCapabilities, supportsStreaming, supportsPersistentStorage, supportsWritableFilesystem, validateCapabilities, type AdapterCapabilities, type FilesystemCapability, type CapabilityOptions, type CapabilityDiagnostics, } from "./capabilities.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { RequestContext, type RequestContextOptions, type RouteTable, type CookieJar, type ResponseCookieJar, type ResponseState, type CookieOptions, htmlResponse, jsonResponse, textResponse, notFound, methodNotAllowed, serverError, guessContentType, serveStaticFile, } from "./context.js";
|
|
2
2
|
export { createWebHandler, type WebHandlerOptions, type WebHandlerRouteTable, type WebHandlerActionRegistry, type CreateWebHandlerResult, } from "./handler.js";
|
|
3
3
|
export { resolveStaticFile } from "./static.js";
|
|
4
|
-
export { incomingMessageToRequest } from "./node-http.js";
|
|
4
|
+
export { incomingMessageToRequest, sendWebResponse } from "./node-http.js";
|
|
5
5
|
export { buildSecurityHeaders, applySecurityHeaders, DEFAULT_SECURITY_HEADERS, } from "./security-headers.js";
|
|
6
|
+
export { StructuredLogger, createRequestLogger, type LogLevel, type LogEntry, type ServerTimingMetric, } from "./logger.js";
|
|
6
7
|
export { DEFAULT_CAPABILITIES, SERVERLESS_CAPABILITIES, EDGE_CAPABILITIES, createCapabilities, supportsStreaming, supportsPersistentStorage, supportsWritableFilesystem, validateCapabilities, type AdapterCapabilities, type FilesystemCapability, type CapabilityOptions, type CapabilityDiagnostics, } from "./capabilities.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
3
|
-
import { a as
|
|
4
|
-
export {
|
|
1
|
+
import { n as e, t } from "../node-http-DVFcYeBS.js";
|
|
2
|
+
import { a as n, c as r, d as i, f as a, g as o, h as s, i as c, l, m as u, n as d, o as f, p, r as m, s as h, t as g, u as _ } from "../runtime-OMyz5yE-.js";
|
|
3
|
+
import { a as v, c as y, i as b, n as x, o as S, r as C, s as w, t as T } from "../capabilities-C7txofmY.js";
|
|
4
|
+
export { T as DEFAULT_CAPABILITIES, c as DEFAULT_SECURITY_HEADERS, x as EDGE_CAPABILITIES, h as RequestContext, C as SERVERLESS_CAPABILITIES, d as StructuredLogger, n as applySecurityHeaders, f as buildSecurityHeaders, b as createCapabilities, m as createRequestLogger, g as createWebHandler, r as guessContentType, l as htmlResponse, t as incomingMessageToRequest, _ as jsonResponse, i as methodNotAllowed, a as notFound, o as resolveStaticFile, e as sendWebResponse, p as serveStaticFile, u as serverError, v as supportsPersistentStorage, S as supportsStreaming, w as supportsWritableFilesystem, s as textResponse, y as validateCapabilities };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
export interface LogEntry {
|
|
3
|
+
level: LogLevel;
|
|
4
|
+
message: string;
|
|
5
|
+
requestId?: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
fields?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface ServerTimingMetric {
|
|
10
|
+
name: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
durationMs: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class StructuredLogger {
|
|
15
|
+
private minLevel;
|
|
16
|
+
private requestId;
|
|
17
|
+
private timings;
|
|
18
|
+
private entries;
|
|
19
|
+
constructor(options?: {
|
|
20
|
+
minLevel?: LogLevel;
|
|
21
|
+
requestId?: string;
|
|
22
|
+
});
|
|
23
|
+
/** Returns the request ID for this logger instance. */
|
|
24
|
+
getRequestId(): string;
|
|
25
|
+
/** Logs a debug message. */
|
|
26
|
+
debug(message: string, fields?: Record<string, unknown>): void;
|
|
27
|
+
/** Logs an info message. */
|
|
28
|
+
info(message: string, fields?: Record<string, unknown>): void;
|
|
29
|
+
/** Logs a warning. */
|
|
30
|
+
warn(message: string, fields?: Record<string, unknown>): void;
|
|
31
|
+
/** Logs an error. */
|
|
32
|
+
error(message: string, fields?: Record<string, unknown>): void;
|
|
33
|
+
/** Records a Server-Timing metric. */
|
|
34
|
+
timing(name: string, durationMs: number, description?: string): void;
|
|
35
|
+
/** Starts a timer and returns a function to stop it and record the timing. */
|
|
36
|
+
startTimer(name: string, description?: string): () => void;
|
|
37
|
+
/** Returns the Server-Timing header value. */
|
|
38
|
+
getServerTimingHeader(): string;
|
|
39
|
+
/** Returns all log entries collected so far. */
|
|
40
|
+
getEntries(): readonly LogEntry[];
|
|
41
|
+
private log;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Creates a logger for a request, optionally using the request's
|
|
45
|
+
* X-Request-ID header if present.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createRequestLogger(request?: Request, minLevel?: LogLevel): StructuredLogger;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
export interface LogEntry {
|
|
3
|
+
level: LogLevel;
|
|
4
|
+
message: string;
|
|
5
|
+
requestId?: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
fields?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface ServerTimingMetric {
|
|
10
|
+
name: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
durationMs: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class StructuredLogger {
|
|
15
|
+
private minLevel;
|
|
16
|
+
private requestId;
|
|
17
|
+
private timings;
|
|
18
|
+
private entries;
|
|
19
|
+
constructor(options?: {
|
|
20
|
+
minLevel?: LogLevel;
|
|
21
|
+
requestId?: string;
|
|
22
|
+
});
|
|
23
|
+
/** Returns the request ID for this logger instance. */
|
|
24
|
+
getRequestId(): string;
|
|
25
|
+
/** Logs a debug message. */
|
|
26
|
+
debug(message: string, fields?: Record<string, unknown>): void;
|
|
27
|
+
/** Logs an info message. */
|
|
28
|
+
info(message: string, fields?: Record<string, unknown>): void;
|
|
29
|
+
/** Logs a warning. */
|
|
30
|
+
warn(message: string, fields?: Record<string, unknown>): void;
|
|
31
|
+
/** Logs an error. */
|
|
32
|
+
error(message: string, fields?: Record<string, unknown>): void;
|
|
33
|
+
/** Records a Server-Timing metric. */
|
|
34
|
+
timing(name: string, durationMs: number, description?: string): void;
|
|
35
|
+
/** Starts a timer and returns a function to stop it and record the timing. */
|
|
36
|
+
startTimer(name: string, description?: string): () => void;
|
|
37
|
+
/** Returns the Server-Timing header value. */
|
|
38
|
+
getServerTimingHeader(): string;
|
|
39
|
+
/** Returns all log entries collected so far. */
|
|
40
|
+
getEntries(): readonly LogEntry[];
|
|
41
|
+
private log;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Creates a logger for a request, optionally using the request's
|
|
45
|
+
* X-Request-ID header if present.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createRequestLogger(request?: Request, minLevel?: LogLevel): StructuredLogger;
|
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
import type { IncomingMessage } from "node:http";
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
export declare function incomingMessageToRequest(req: IncomingMessage, body?: BodyInit | null): Request;
|
|
3
|
+
/**
|
|
4
|
+
* Writes a Web `Response` to a Node `ServerResponse`, streaming the body.
|
|
5
|
+
*
|
|
6
|
+
* Unlike `res.end(Buffer.from(await response.arrayBuffer()))`, this forwards
|
|
7
|
+
* chunks as they are produced — required for streaming SSR responses to reach
|
|
8
|
+
* the client progressively. Buffered (non-stream) bodies behave exactly as
|
|
9
|
+
* before. Honors backpressure (`drain`) and cancels the upstream stream when
|
|
10
|
+
* the client disconnects (`close` before `finish`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function sendWebResponse(res: ServerResponse, response: Response): Promise<void>;
|
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
import type { IncomingMessage } from "node:http";
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
export declare function incomingMessageToRequest(req: IncomingMessage, body?: BodyInit | null): Request;
|
|
3
|
+
/**
|
|
4
|
+
* Writes a Web `Response` to a Node `ServerResponse`, streaming the body.
|
|
5
|
+
*
|
|
6
|
+
* Unlike `res.end(Buffer.from(await response.arrayBuffer()))`, this forwards
|
|
7
|
+
* chunks as they are produced — required for streaming SSR responses to reach
|
|
8
|
+
* the client progressively. Buffered (non-stream) bodies behave exactly as
|
|
9
|
+
* before. Honors backpressure (`drain`) and cancels the upstream stream when
|
|
10
|
+
* the client disconnects (`close` before `finish`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function sendWebResponse(res: ServerResponse, response: Response): Promise<void>;
|