@elurjs/kit 2.4.4

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.
Files changed (255) hide show
  1. package/CHANGELOG.md +874 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1330 -0
  4. package/bin/elur-kit.js +54 -0
  5. package/dist/lib/action/define.d.cts +72 -0
  6. package/dist/lib/action/define.d.ts +72 -0
  7. package/dist/lib/action/error-store.d.cts +29 -0
  8. package/dist/lib/action/error-store.d.ts +29 -0
  9. package/dist/lib/action/index.cjs +1 -0
  10. package/dist/lib/action/index.d.cts +64 -0
  11. package/dist/lib/action/index.d.ts +64 -0
  12. package/dist/lib/action/index.js +2 -0
  13. package/dist/lib/action/origin.d.cts +20 -0
  14. package/dist/lib/action/origin.d.ts +20 -0
  15. package/dist/lib/action/scan.d.cts +28 -0
  16. package/dist/lib/action/scan.d.ts +28 -0
  17. package/dist/lib/action/server.d.cts +30 -0
  18. package/dist/lib/action/server.d.ts +30 -0
  19. package/dist/lib/action-BB6N5yD-.js +111 -0
  20. package/dist/lib/action-BB6N5yD-.js.map +1 -0
  21. package/dist/lib/action-DZHihU3Z.cjs +2 -0
  22. package/dist/lib/action-DZHihU3Z.cjs.map +1 -0
  23. package/dist/lib/adapters/bun.cjs +30 -0
  24. package/dist/lib/adapters/bun.cjs.map +1 -0
  25. package/dist/lib/adapters/bun.d.cts +13 -0
  26. package/dist/lib/adapters/bun.d.ts +13 -0
  27. package/dist/lib/adapters/bun.js +65 -0
  28. package/dist/lib/adapters/bun.js.map +1 -0
  29. package/dist/lib/adapters/index.cjs +0 -0
  30. package/dist/lib/adapters/index.d.cts +30 -0
  31. package/dist/lib/adapters/index.d.ts +30 -0
  32. package/dist/lib/adapters/index.js +0 -0
  33. package/dist/lib/adapters/netlify.cjs +10 -0
  34. package/dist/lib/adapters/netlify.cjs.map +1 -0
  35. package/dist/lib/adapters/netlify.d.cts +13 -0
  36. package/dist/lib/adapters/netlify.d.ts +13 -0
  37. package/dist/lib/adapters/netlify.js +51 -0
  38. package/dist/lib/adapters/netlify.js.map +1 -0
  39. package/dist/lib/adapters/node.cjs +46 -0
  40. package/dist/lib/adapters/node.cjs.map +1 -0
  41. package/dist/lib/adapters/node.d.cts +13 -0
  42. package/dist/lib/adapters/node.d.ts +13 -0
  43. package/dist/lib/adapters/node.js +105 -0
  44. package/dist/lib/adapters/node.js.map +1 -0
  45. package/dist/lib/adapters/shared.d.cts +17 -0
  46. package/dist/lib/adapters/shared.d.ts +17 -0
  47. package/dist/lib/adapters/vercel.cjs +2 -0
  48. package/dist/lib/adapters/vercel.cjs.map +1 -0
  49. package/dist/lib/adapters/vercel.d.cts +9 -0
  50. package/dist/lib/adapters/vercel.d.ts +9 -0
  51. package/dist/lib/adapters/vercel.js +62 -0
  52. package/dist/lib/adapters/vercel.js.map +1 -0
  53. package/dist/lib/build/build.d.cts +83 -0
  54. package/dist/lib/build/build.d.ts +83 -0
  55. package/dist/lib/build/document-shell.d.cts +48 -0
  56. package/dist/lib/build/document-shell.d.ts +48 -0
  57. package/dist/lib/build/transform-source.d.cts +26 -0
  58. package/dist/lib/build/transform-source.d.ts +26 -0
  59. package/dist/lib/build/vite-build.d.cts +78 -0
  60. package/dist/lib/build/vite-build.d.ts +78 -0
  61. package/dist/lib/cache/policy.d.cts +25 -0
  62. package/dist/lib/cache/policy.d.ts +25 -0
  63. package/dist/lib/cache.d.cts +13 -0
  64. package/dist/lib/cache.d.ts +13 -0
  65. package/dist/lib/capabilities-C0TDQoMY.cjs +2 -0
  66. package/dist/lib/capabilities-C0TDQoMY.cjs.map +1 -0
  67. package/dist/lib/capabilities-C7txofmY.js +45 -0
  68. package/dist/lib/capabilities-C7txofmY.js.map +1 -0
  69. package/dist/lib/cli/commands.d.cts +15 -0
  70. package/dist/lib/cli/commands.d.ts +15 -0
  71. package/dist/lib/cli.cjs +5132 -0
  72. package/dist/lib/cli.cjs.map +1 -0
  73. package/dist/lib/cli.d.cts +30 -0
  74. package/dist/lib/cli.d.ts +30 -0
  75. package/dist/lib/cli.js +5131 -0
  76. package/dist/lib/cli.js.map +1 -0
  77. package/dist/lib/collections-Dbc8aRLf.js +233 -0
  78. package/dist/lib/collections-Dbc8aRLf.js.map +1 -0
  79. package/dist/lib/collections-VDy5g9p8.cjs +13 -0
  80. package/dist/lib/collections-VDy5g9p8.cjs.map +1 -0
  81. package/dist/lib/config/index.cjs +2 -0
  82. package/dist/lib/config/index.cjs.map +1 -0
  83. package/dist/lib/config/index.d.cts +100 -0
  84. package/dist/lib/config/index.d.ts +100 -0
  85. package/dist/lib/config/index.js +122 -0
  86. package/dist/lib/config/index.js.map +1 -0
  87. package/dist/lib/content/collections.d.cts +69 -0
  88. package/dist/lib/content/collections.d.ts +69 -0
  89. package/dist/lib/content/frontmatter.d.cts +20 -0
  90. package/dist/lib/content/frontmatter.d.ts +20 -0
  91. package/dist/lib/content/index.cjs +2 -0
  92. package/dist/lib/content/index.cjs.map +1 -0
  93. package/dist/lib/content/index.d.cts +5 -0
  94. package/dist/lib/content/index.d.ts +5 -0
  95. package/dist/lib/content/index.js +28 -0
  96. package/dist/lib/content/index.js.map +1 -0
  97. package/dist/lib/content/markdown.d.cts +6 -0
  98. package/dist/lib/content/markdown.d.ts +6 -0
  99. package/dist/lib/content/raw.d.cts +9 -0
  100. package/dist/lib/content/raw.d.ts +9 -0
  101. package/dist/lib/content/schema.d.cts +14 -0
  102. package/dist/lib/content/schema.d.ts +14 -0
  103. package/dist/lib/errors-0X7pCFNU.cjs +2 -0
  104. package/dist/lib/errors-0X7pCFNU.cjs.map +1 -0
  105. package/dist/lib/errors-ePYxDlhU.js +70 -0
  106. package/dist/lib/errors-ePYxDlhU.js.map +1 -0
  107. package/dist/lib/errors.d.cts +84 -0
  108. package/dist/lib/errors.d.ts +84 -0
  109. package/dist/lib/hydrate-8a3TbQbs.cjs +2 -0
  110. package/dist/lib/hydrate-8a3TbQbs.cjs.map +1 -0
  111. package/dist/lib/hydrate-CgZGrTxF.js +126 -0
  112. package/dist/lib/hydrate-CgZGrTxF.js.map +1 -0
  113. package/dist/lib/image/index.cjs +2 -0
  114. package/dist/lib/image/index.cjs.map +1 -0
  115. package/dist/lib/image/index.d.cts +37 -0
  116. package/dist/lib/image/index.d.ts +37 -0
  117. package/dist/lib/image/index.js +49 -0
  118. package/dist/lib/image/index.js.map +1 -0
  119. package/dist/lib/image/pipeline.d.cts +37 -0
  120. package/dist/lib/image/pipeline.d.ts +37 -0
  121. package/dist/lib/image/registry.cjs +2 -0
  122. package/dist/lib/image/registry.cjs.map +1 -0
  123. package/dist/lib/image/registry.d.cts +26 -0
  124. package/dist/lib/image/registry.d.ts +26 -0
  125. package/dist/lib/image/registry.js +23 -0
  126. package/dist/lib/image/registry.js.map +1 -0
  127. package/dist/lib/image/service.cjs +2 -0
  128. package/dist/lib/image/service.cjs.map +1 -0
  129. package/dist/lib/image/service.d.cts +164 -0
  130. package/dist/lib/image/service.d.ts +164 -0
  131. package/dist/lib/image/service.js +308 -0
  132. package/dist/lib/image/service.js.map +1 -0
  133. package/dist/lib/index.cjs +1 -0
  134. package/dist/lib/index.cjs.map +1 -0
  135. package/dist/lib/index.d.cts +38 -0
  136. package/dist/lib/index.d.ts +38 -0
  137. package/dist/lib/index.js +1 -0
  138. package/dist/lib/index.js.map +1 -0
  139. package/dist/lib/integrations/hooks.d.cts +58 -0
  140. package/dist/lib/integrations/hooks.d.ts +58 -0
  141. package/dist/lib/integrations/index.cjs +1 -0
  142. package/dist/lib/integrations/index.d.cts +18 -0
  143. package/dist/lib/integrations/index.d.ts +18 -0
  144. package/dist/lib/integrations/index.js +2 -0
  145. package/dist/lib/integrations-7dpN1RYB.js +35 -0
  146. package/dist/lib/integrations-7dpN1RYB.js.map +1 -0
  147. package/dist/lib/integrations-DDqfWjvH.cjs +2 -0
  148. package/dist/lib/integrations-DDqfWjvH.cjs.map +1 -0
  149. package/dist/lib/interpolation-plugin-Bt-KRKVY.js +365 -0
  150. package/dist/lib/interpolation-plugin-Bt-KRKVY.js.map +1 -0
  151. package/dist/lib/interpolation-plugin-CMPCDlVJ.cjs +31 -0
  152. package/dist/lib/interpolation-plugin-CMPCDlVJ.cjs.map +1 -0
  153. package/dist/lib/island/generate-entry.d.cts +27 -0
  154. package/dist/lib/island/generate-entry.d.ts +27 -0
  155. package/dist/lib/island/hydrate.d.cts +33 -0
  156. package/dist/lib/island/hydrate.d.ts +33 -0
  157. package/dist/lib/island/index.cjs +1 -0
  158. package/dist/lib/island/index.d.cts +2 -0
  159. package/dist/lib/island/index.d.ts +2 -0
  160. package/dist/lib/island/index.js +1 -0
  161. package/dist/lib/island/island.d.cts +32 -0
  162. package/dist/lib/island/island.d.ts +32 -0
  163. package/dist/lib/island/scan.d.cts +14 -0
  164. package/dist/lib/island/scan.d.ts +14 -0
  165. package/dist/lib/manifest/index.cjs +3 -0
  166. package/dist/lib/manifest/index.cjs.map +1 -0
  167. package/dist/lib/manifest/index.d.cts +18 -0
  168. package/dist/lib/manifest/index.d.ts +18 -0
  169. package/dist/lib/manifest/index.js +110 -0
  170. package/dist/lib/manifest/index.js.map +1 -0
  171. package/dist/lib/middleware/index.d.cts +56 -0
  172. package/dist/lib/middleware/index.d.ts +56 -0
  173. package/dist/lib/middleware/stream-boundary.d.cts +53 -0
  174. package/dist/lib/middleware/stream-boundary.d.ts +53 -0
  175. package/dist/lib/node-http-DRAUhO0c.js +617 -0
  176. package/dist/lib/node-http-DRAUhO0c.js.map +1 -0
  177. package/dist/lib/node-http-DhxguYyz.cjs +13 -0
  178. package/dist/lib/node-http-DhxguYyz.cjs.map +1 -0
  179. package/dist/lib/render/render-to-string.d.cts +12 -0
  180. package/dist/lib/render/render-to-string.d.ts +12 -0
  181. package/dist/lib/render/ssr-flag.d.cts +4 -0
  182. package/dist/lib/render/ssr-flag.d.ts +4 -0
  183. package/dist/lib/route-scanner-JY6YrNH5.cjs +2 -0
  184. package/dist/lib/route-scanner-JY6YrNH5.cjs.map +1 -0
  185. package/dist/lib/route-scanner-XcLId7lK.js +100 -0
  186. package/dist/lib/route-scanner-XcLId7lK.js.map +1 -0
  187. package/dist/lib/router/client.cjs +2 -0
  188. package/dist/lib/router/client.cjs.map +1 -0
  189. package/dist/lib/router/client.d.cts +47 -0
  190. package/dist/lib/router/client.d.ts +47 -0
  191. package/dist/lib/router/client.js +298 -0
  192. package/dist/lib/router/client.js.map +1 -0
  193. package/dist/lib/router/route-scanner.d.cts +50 -0
  194. package/dist/lib/router/route-scanner.d.ts +50 -0
  195. package/dist/lib/runtime/capabilities.d.cts +47 -0
  196. package/dist/lib/runtime/capabilities.d.ts +47 -0
  197. package/dist/lib/runtime/context.d.cts +140 -0
  198. package/dist/lib/runtime/context.d.ts +140 -0
  199. package/dist/lib/runtime/handler.d.cts +39 -0
  200. package/dist/lib/runtime/handler.d.ts +39 -0
  201. package/dist/lib/runtime/index.cjs +1 -0
  202. package/dist/lib/runtime/index.d.cts +6 -0
  203. package/dist/lib/runtime/index.d.ts +6 -0
  204. package/dist/lib/runtime/index.js +4 -0
  205. package/dist/lib/runtime/node-http.d.cts +2 -0
  206. package/dist/lib/runtime/node-http.d.ts +2 -0
  207. package/dist/lib/runtime/security-headers.d.cts +13 -0
  208. package/dist/lib/runtime/security-headers.d.ts +13 -0
  209. package/dist/lib/runtime/static.d.cts +1 -0
  210. package/dist/lib/runtime/static.d.ts +1 -0
  211. package/dist/lib/runtime-CrH5X-Es.cjs +2 -0
  212. package/dist/lib/runtime-CrH5X-Es.cjs.map +1 -0
  213. package/dist/lib/runtime-Ds2-wH6P.js +520 -0
  214. package/dist/lib/runtime-Ds2-wH6P.js.map +1 -0
  215. package/dist/lib/scan-CY2O9ty6.cjs +2 -0
  216. package/dist/lib/scan-CY2O9ty6.cjs.map +1 -0
  217. package/dist/lib/scan-CjMUgnsd.js +22 -0
  218. package/dist/lib/scan-CjMUgnsd.js.map +1 -0
  219. package/dist/lib/scan-DXzYtmRQ.cjs +2 -0
  220. package/dist/lib/scan-DXzYtmRQ.cjs.map +1 -0
  221. package/dist/lib/scan-Dn93ycXG.js +33 -0
  222. package/dist/lib/scan-Dn93ycXG.js.map +1 -0
  223. package/dist/lib/seo/index.cjs +10 -0
  224. package/dist/lib/seo/index.cjs.map +1 -0
  225. package/dist/lib/seo/index.d.cts +99 -0
  226. package/dist/lib/seo/index.d.ts +99 -0
  227. package/dist/lib/seo/index.js +41 -0
  228. package/dist/lib/seo/index.js.map +1 -0
  229. package/dist/lib/shared-CfqOLsKj.cjs +183 -0
  230. package/dist/lib/shared-CfqOLsKj.cjs.map +1 -0
  231. package/dist/lib/shared-DU3iQgr7.js +236 -0
  232. package/dist/lib/shared-DU3iQgr7.js.map +1 -0
  233. package/dist/lib/ssr/match.d.cts +25 -0
  234. package/dist/lib/ssr/match.d.ts +25 -0
  235. package/dist/lib/ssr/render.d.cts +62 -0
  236. package/dist/lib/ssr/render.d.ts +62 -0
  237. package/dist/lib/ssr/server.d.cts +41 -0
  238. package/dist/lib/ssr/server.d.ts +41 -0
  239. package/dist/lib/ssr/stream.d.cts +48 -0
  240. package/dist/lib/ssr/stream.d.ts +48 -0
  241. package/dist/lib/stream-CpOG052V.js +104 -0
  242. package/dist/lib/stream-CpOG052V.js.map +1 -0
  243. package/dist/lib/stream-DmZjYqHm.cjs +17 -0
  244. package/dist/lib/stream-DmZjYqHm.cjs.map +1 -0
  245. package/dist/lib/types.d.cts +97 -0
  246. package/dist/lib/types.d.ts +97 -0
  247. package/dist/lib/vite/index.cjs +2 -0
  248. package/dist/lib/vite/index.cjs.map +1 -0
  249. package/dist/lib/vite/index.d.cts +41 -0
  250. package/dist/lib/vite/index.d.ts +41 -0
  251. package/dist/lib/vite/index.js +229 -0
  252. package/dist/lib/vite/index.js.map +1 -0
  253. package/dist/lib/vite/interpolation-plugin.d.cts +64 -0
  254. package/dist/lib/vite/interpolation-plugin.d.ts +64 -0
  255. package/package.json +292 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-scanner-XcLId7lK.js","names":[],"sources":["../../src/router/route-scanner.ts"],"sourcesContent":["import { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\n// --- Route scanner ---\n//\n// Walks src/app/ and maps file conventions to URL paths.\n//\n// Supported conventions:\n// - page.ts -> URL path\n// - page.data.ts -> loader for that page\n// - layout.ts -> layout wrapping pages in the same segment\n// - route.ts -> API endpoint (collected separately)\n//\n// Dynamic segments:\n// - [slug] -> :slug\n// - [...slug] -> catch-all (rendered as :slug*)\n// - [[...slug]] -> optional catch-all (rendered as :slug* but matches\n// the base path too)\n//\n// Route conflicts (two routes with the same path pattern) cause an error\n// during scanRoutes (plan §11.1).\n\n/** A page route discovered by the scanner. */\nexport interface PageRoute {\n /** URL path, e.g. \"/blog/:slug\". */\n path: string;\n /** File system path to the page.ts module. */\n pagePath: string;\n /** File system path to the page.data.ts module, if any. */\n dataPath?: string;\n /** File system path to the page.action.ts module, if any. */\n actionPath?: string;\n /** Ordered list of layout.ts modules from root to leaf. */\n layouts: string[];\n /** File system path to the loading.ts module, if any. */\n loadingPath?: string;\n /** Dynamic parameter names extracted from the path. */\n params: string[];\n /** Whether the route has an optional catch-all segment. */\n optionalCatchAll?: boolean;\n /**\n * Named slot modules discovered in the same directory as the page.\n * Keyed by slot name (filename without `.slot.ts` suffix).\n * (v2.1 — Fix #2: Layout Slots)\n */\n slots?: Record<string, string>;\n}\n\n/** An API route discovered by the scanner. */\nexport interface ApiRoute {\n /** URL path, e.g. \"/api/posts\". */\n path: string;\n /** File system path to the route.ts module. */\n routePath: string;\n /** Dynamic parameter names extracted from the path. */\n params: string[];\n}\n\n/** Result of scanning the app directory. */\nexport interface ScannedRoutes {\n pages: PageRoute[];\n api: ApiRoute[];\n /** Optional 404 error page. */\n error404?: PageRoute;\n /** Optional 500 error page. */\n error500?: PageRoute;\n}\n\nfunction isRouteGroup(segment: string): boolean {\n return segment.startsWith(\"(\") && segment.endsWith(\")\");\n}\n\nfunction segmentToUrl(segment: string): string {\n // Optional catch-all: [[...slug]] -> :slug* (matches base path too)\n if (segment.startsWith(\"[[...\") && segment.endsWith(\"]]\")) {\n return `:${segment.slice(5, -2)}*`;\n }\n // Catch-all: [...slug] -> :slug*\n if (segment.startsWith(\"[...\") && segment.endsWith(\"]\")) {\n return `:${segment.slice(4, -1)}*`;\n }\n // Dynamic: [slug] -> :slug\n if (segment.startsWith(\"[\") && segment.endsWith(\"]\")) {\n return `:${segment.slice(1, -1)}`;\n }\n return segment;\n}\n\nfunction extractParams(segment: string): string[] {\n if (segment.startsWith(\"[[...\") && segment.endsWith(\"]]\")) {\n return [segment.slice(5, -2)];\n }\n if (segment.startsWith(\"[...\") && segment.endsWith(\"]\")) {\n return [segment.slice(4, -1)];\n }\n if (segment.startsWith(\"[\") && segment.endsWith(\"]\")) {\n return [segment.slice(1, -1)];\n }\n return [];\n}\n\nfunction isOptionalCatchAll(segment: string): boolean {\n return segment.startsWith(\"[[...\") && segment.endsWith(\"]]\");\n}\n\nasync function collectFiles(dir: string): Promise<string[]> {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n return entries\n .filter((e) => e.isFile() && e.name.endsWith(\".ts\"))\n .map((e) => e.name);\n } catch {\n return [];\n }\n}\n\nasync function collectDirs(dir: string): Promise<string[]> {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n return entries.filter((e) => e.isDirectory()).map((e) => e.name);\n } catch {\n return [];\n }\n}\n\nasync function scanRecursive(\n appDir: string,\n currentDir: string,\n urlSegments: string[],\n params: string[],\n layouts: string[],\n result: ScannedRoutes,\n hasOptionalCatchAll = false,\n): Promise<void> {\n const files = await collectFiles(currentDir);\n const dirs = await collectDirs(currentDir);\n\n const pagePath = files.includes(\"page.ts\")\n ? join(currentDir, \"page.ts\")\n : undefined;\n const dataPath = files.includes(\"page.data.ts\")\n ? join(currentDir, \"page.data.ts\")\n : undefined;\n const actionPath = files.includes(\"page.action.ts\")\n ? join(currentDir, \"page.action.ts\")\n : undefined;\n const loadingPath = files.includes(\"loading.ts\")\n ? join(currentDir, \"loading.ts\")\n : undefined;\n const layoutPath = files.includes(\"layout.ts\")\n ? join(currentDir, \"layout.ts\")\n : undefined;\n const routePath = files.includes(\"route.ts\")\n ? join(currentDir, \"route.ts\")\n : undefined;\n\n const currentLayouts = layoutPath\n ? [...layouts, layoutPath]\n : [...layouts];\n\n if (routePath) {\n result.api.push({\n path: urlSegments.length === 0 ? \"/\" : \"/\" + urlSegments.join(\"/\"),\n routePath,\n params: [...params],\n });\n }\n\n if (pagePath) {\n const path = urlSegments.length === 0 ? \"/\" : \"/\" + urlSegments.join(\"/\");\n // Detect named slot files: *.slot.ts (v2.1 — Fix #2: Layout Slots)\n const slots: Record<string, string> = {};\n for (const file of files) {\n const slotMatch = file.match(/^(.+)\\.slot\\.ts$/);\n if (slotMatch) {\n slots[slotMatch[1]] = join(currentDir, file);\n }\n }\n result.pages.push({\n path,\n pagePath,\n dataPath,\n actionPath,\n layouts: currentLayouts,\n loadingPath,\n params: [...params],\n optionalCatchAll: hasOptionalCatchAll,\n slots: Object.keys(slots).length > 0 ? slots : undefined,\n });\n }\n\n for (const dir of dirs) {\n if (isRouteGroup(dir)) {\n // Route groups do not add a URL segment, but they can add a layout.\n const groupDir = join(currentDir, dir);\n const groupFiles = await collectFiles(groupDir);\n const groupLayout = groupFiles.includes(\"layout.ts\")\n ? join(groupDir, \"layout.ts\")\n : undefined;\n await scanRecursive(\n appDir,\n groupDir,\n urlSegments,\n params,\n groupLayout ? [...currentLayouts, groupLayout] : currentLayouts,\n result,\n );\n continue;\n }\n\n const optional = isOptionalCatchAll(dir);\n await scanRecursive(\n appDir,\n join(currentDir, dir),\n [...urlSegments, segmentToUrl(dir)],\n [...params, ...extractParams(dir)],\n currentLayouts,\n result,\n optional,\n );\n }\n}\n\n/**\n * Scans an app directory for Elur Kit file-based routes.\n *\n * @param appDir Absolute path to the app directory (e.g. \"src/app\").\n * @returns Discovered page and API routes.\n */\nexport async function scanRoutes(appDir: string): Promise<ScannedRoutes> {\n const result: ScannedRoutes = { pages: [], api: [] };\n const rootFiles = await collectFiles(appDir);\n const rootLayout = rootFiles.includes(\"layout.ts\")\n ? join(appDir, \"layout.ts\")\n : undefined;\n\n if (rootFiles.includes(\"404.page.ts\")) {\n result.error404 = {\n path: \"/404\",\n pagePath: join(appDir, \"404.page.ts\"),\n dataPath: rootFiles.includes(\"404.page.data.ts\")\n ? join(appDir, \"404.page.data.ts\")\n : undefined,\n layouts: rootLayout ? [rootLayout] : [],\n params: [],\n };\n }\n\n if (rootFiles.includes(\"500.page.ts\")) {\n result.error500 = {\n path: \"/500\",\n pagePath: join(appDir, \"500.page.ts\"),\n dataPath: rootFiles.includes(\"500.page.data.ts\")\n ? join(appDir, \"500.page.data.ts\")\n : undefined,\n layouts: rootLayout ? [rootLayout] : [],\n params: [],\n };\n }\n\n await scanRecursive(appDir, appDir, [], [], [], result);\n\n // Detect route conflicts (plan §11.1): two routes with the same path\n // pattern is an error during manifest generation.\n detectRouteConflicts(result);\n\n return result;\n}\n\n/**\n * Detects and throws on route conflicts (plan §11.1, runtime-security §10).\n * Two routes with the same path pattern cause an error.\n */\nfunction detectRouteConflicts(routes: ScannedRoutes): void {\n const pagePaths = new Map<string, string>();\n for (const page of routes.pages) {\n const existing = pagePaths.get(page.path);\n if (existing) {\n throw new Error(\n `[elur-kit] Route conflict: \"${page.path}\" is defined by both ` +\n `\"${existing}\" and \"${page.pagePath}\". ` +\n `Remove one of the conflicting page.ts files.`,\n );\n }\n pagePaths.set(page.path, page.pagePath);\n }\n\n // Also check API route conflicts.\n const apiPaths = new Map<string, string>();\n for (const api of routes.api) {\n const existing = apiPaths.get(api.path);\n if (existing) {\n throw new Error(\n `[elur-kit] API route conflict: \"${api.path}\" is defined by both ` +\n `\"${existing}\" and \"${api.routePath}\".`,\n );\n }\n apiPaths.set(api.path, api.routePath);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=!0,t=null;function n(){return e?(t||=(async()=>{let t=new URL(`/__elur-js/render`,location.origin);t.searchParams.set(`page`,`/`);try{let n=await fetch(t.toString(),{headers:{Accept:`application/json`,"X-Elur-Probe":`1`}});return e=n.ok,n.ok}catch{return e=!1,!1}})(),t):Promise.resolve(!1)}function r(e){return e.tagName===`A`&&e.hostname===location.hostname&&e.target===``&&!e.getAttribute(`download`)&&!e.hasAttribute(`data-no-router`)}function i(e){return e.metaKey||e.ctrlKey||e.shiftKey||e.altKey}var a=3e4,o=new Map;function s(e,t){return e+t}function c(e){let t=o.get(e);if(t){if(Date.now()-t.ts>a){o.delete(e);return}return t.payload}}function l(e,t){o.set(e,{payload:t,ts:Date.now()})}async function u(t,r,i){let a=s(t,r),o=c(a);if(o)return o;if(e&&await n()){let e=await d(t,r,i);if(e)return l(a,e),e}let u=await f(t,r,i);return u&&l(a,u),u}async function d(e,t,n){let r=new URL(`/__elur-js/render`,location.origin);r.searchParams.set(`page`,e);let i=new URL(location.href);r.searchParams.set(`search`,t||i.search);let a;try{a=await fetch(r.toString(),{headers:{Accept:`application/json`},signal:n})}catch(e){e instanceof DOMException&&e.name;return}if(!a.ok)return;let o;try{o=await a.json()}catch{return}return o}async function f(e,t,n){let r=e+(t||``),i;try{i=await fetch(r,{headers:{Accept:`text/html`},signal:n})}catch(e){e instanceof DOMException&&e.name;return}if(!i.ok)return;let a;try{a=await i.text()}catch{return}let o=new DOMParser().parseFromString(a,`text/html`),s=o.getElementById(`app`);if(!s)return;let c=s.innerHTML,l=o.querySelectorAll(`[data-elur-head]`),u=``;for(let e of l)u+=e.outerHTML;let d=o.head.querySelectorAll(`link[rel='icon'], link[rel='apple-touch-icon'], link[rel='manifest'], meta[name='theme-color']`);for(let e of d){let t=e.getAttribute(`href`);t&&document.head.querySelector(`link[href="${t}"]`)||(u+=e.outerHTML)}let f=o.querySelector(`title`)?.textContent??void 0;return{body:c,head:u,title:f}}async function p(e,t=``){let n=s(e,t);if(o.has(n)){let e=o.get(n);if(Date.now()-e.ts<=a)return}await u(e,t)}function m(){return window.matchMedia?.(`(prefers-reduced-motion: reduce)`).matches??!1}function h(){return typeof document.startViewTransition==`function`}var g=null;function _(){g&&=(g.controller.abort(),null)}function v(e){let t=e.querySelectorAll(`link[rel="stylesheet"]`);for(let e of t){let t=e.getAttribute(`href`);if(t){if(document.head.querySelector(`link[rel="stylesheet"][href="${t}"]`)){e.remove();continue}e.setAttribute(`data-elur-hoisted`,``),document.head.appendChild(e)}}let n=e.querySelectorAll(`style`);for(let e of n){let t=e.textContent?.trim();if(t){if(Array.from(document.head.querySelectorAll(`style`)).find(e=>e.textContent?.trim()===t)){e.remove();continue}e.setAttribute(`data-elur-hoisted`,``),document.head.appendChild(e)}}}function y(e){let t=document.getElementById(`elur-route-announcer`);t||(t=document.createElement(`div`),t.id=`elur-route-announcer`,t.setAttribute(`aria-live`,`assertive`),t.setAttribute(`aria-atomic`,`true`),t.setAttribute(`role`,`status`),t.setAttribute(`style`,`position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden;`),document.body.appendChild(t)),t.textContent=``;let n=t,r=setTimeout(()=>{if(typeof document<`u`&&n){let t=document.title||e;n.textContent=t}},50);typeof r==`object`&&r&&`unref`in r&&r.unref()}function b(){let e=document.getElementById(`app`);e&&(e.hasAttribute(`tabindex`)||e.setAttribute(`tabindex`,`-1`),e.focus({preventScroll:!1}))}function x(e,t){let n=location.origin+e+(t||``),r=document.querySelector(`link[rel="canonical"]`);r||(r=document.createElement(`link`),r.rel=`canonical`,document.head.appendChild(r)),r.href=n;let i=document.querySelector(`meta[property="og:url"]`);i||(i=document.createElement(`meta`),i.setAttribute(`property`,`og:url`),document.head.appendChild(i)),i.content=n}async function S(e,t=``,n=!0){_();let r=new AbortController;g={controller:r,pathname:e};let i;try{i=await u(e,t,r.signal)}catch(e){if(r.signal.aborted)return!1;throw e}if(g&&g.controller!==r&&r.signal.aborted||(g=null,!i))return!1;let a=document.getElementById(`app`);if(!a)return!1;n&&history.replaceState({n:location.pathname,scroll:window.scrollY},``,location.href);let o=new URL(location.href),s=()=>{let r=[];a.querySelectorAll(`[data-scroll-preserve]`).forEach(e=>{r.push({el:e,top:e.scrollTop})}),v(a);let s=document.createElement(`template`);s.innerHTML=i.body,v(s.content),a.innerHTML=s.innerHTML,C(i.head,i.title),i.clearActionErrorCookie&&(document.cookie=i.clearActionErrorCookie),n&&history.pushState({n:e,scroll:0},``,e+(t||o.search));let c=n?0:history.state?.scroll??0;window.scrollTo(0,c);for(let e of r){let t=a.querySelector(`[data-scroll-preserve="${e.el.getAttribute(`data-scroll-preserve`)}"]`);t&&(t.scrollTop=e.top)}x(e,t),y(e),n&&b(),document.dispatchEvent(new CustomEvent(`elur:rendered`))};return h()&&!m()?document.startViewTransition(()=>s()):s(),!0}function C(e,t){if(document.querySelectorAll(`[data-elur-head]`).forEach(e=>e.remove()),e&&e.trim().length>0){let t=document.createElement(`template`);t.innerHTML=e;let n=t.content,r=n.querySelector(`title`);r&&(document.title=r.textContent??``,r.remove()),document.head.appendChild(n)}else t&&(document.title=t)}var w=new WeakSet;function T(){let e=e=>{let t=e.getAttribute(`href`);if(!t||t.startsWith(`#`)||t.startsWith(`mailto:`)||t.startsWith(`javascript:`))return null;let n=t.indexOf(`?`);return{path:n===-1?t:t.slice(0,n),search:n===-1?``:t.slice(n)}},t=t=>{if(w.has(t)||!r(t)||t.hasAttribute(`data-no-prefetch`))return;w.add(t);let n=()=>{let n=e(t);n&&p(n.path,n.search)};if(t.addEventListener(`pointerenter`,n,{once:!0}),t.addEventListener(`focus`,n,{once:!0}),t.dataset.prefetch===`viewport`&&`IntersectionObserver`in window){let n=new IntersectionObserver(t=>{for(let r of t){if(!r.isIntersecting)continue;let t=e(r.target);t&&p(t.path,t.search),n.disconnect()}},{rootMargin:`200px`,threshold:0});n.observe(t)}},n=()=>{let e=document.querySelectorAll(`a[href]`);for(let n of e)t(n)};n(),new MutationObserver(()=>n()).observe(document.body,{childList:!0,subtree:!0}),document.addEventListener(`elur:rendered`,n)}function E(){document.querySelector(`meta[name="elur:render-endpoint"]`)?.getAttribute(`content`)===`off`&&(e=!1);let t=document.getElementById(`app`);t&&v(t),document.addEventListener(`click`,async e=>{if(!(e instanceof MouseEvent)||i(e)||e.defaultPrevented)return;let t=e.target.closest(`a`);if(!t||!r(t))return;let n=t.getAttribute(`href`);if(!n||n.startsWith(`mailto:`)||n.startsWith(`javascript:`))return;if(n.startsWith(`#`)){if(n.length>1){let t=document.getElementById(n.slice(1));t&&(e.preventDefault(),t.scrollIntoView({behavior:`smooth`,block:`start`}),history.replaceState(null,``,n))}return}e.preventDefault();let a=n.indexOf(`?`);await S(a===-1?n:n.slice(0,a),a===-1?``:n.slice(a))||location.assign(n)}),window.addEventListener(`popstate`,e=>{S(e.state?.n??location.pathname,location.search,!1)}),T()}function D(){o.clear(),g=null,e=!0,t=null}exports.__resetRouterState=D,exports.hoistStyles=v,exports.navigateTo=S,exports.prefetch=p,exports.startClientRouter=E;
2
+ //# sourceMappingURL=client.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.cjs","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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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Client-side router for Elur Kit.
3
+ *
4
+ * Intercepts clicks on internal links, fetches the rendered page body from
5
+ * `/__elur-js/render`, swaps the `#app` content and updates the history state.
6
+ * This is loaded as part of the client bundle instead of being inlined in
7
+ * every HTML page.
8
+ *
9
+ * Features:
10
+ * - SPA navigation with head merge (title, meta, OG tags)
11
+ * - Scroll restoration on back/forward
12
+ * - Prefetch on hover/focus (Astro-style), with opt-in viewport prefetch
13
+ * - View Transitions API with `prefers-reduced-motion` respect
14
+ */
15
+ /**
16
+ * Prefetches a path without navigating. Called by the IntersectionObserver
17
+ * when a link enters the viewport, and on hover/focus.
18
+ */
19
+ export declare function prefetch(pathname: string, search?: string): Promise<void>;
20
+ /**
21
+ * Hoists `<link rel="stylesheet">` and `<style>` tags from inside `#app` into
22
+ * `<head>` so they persist across SPA navigations (prevents FOUC/flashing).
23
+ * Deduplicates by `href` for links and by text content for styles.
24
+ */
25
+ export declare function hoistStyles(container: ParentNode): void;
26
+ /**
27
+ * Navigates to a page without a full reload: fetches the fresh body from the
28
+ * `/__elur-js/render` endpoint, swaps `#app`, updates the document title and
29
+ * dispatches `elur:rendered` so islands re-hydrate. Used by the router on
30
+ * clicks and available for programmatic navigation (e.g. after a server
31
+ * action returns a redirect, so the target page shows fresh server data).
32
+ *
33
+ * Concurrent navigations are handled: a new navigateTo() cancels any
34
+ * in-flight navigation to prevent out-of-order content swaps.
35
+ *
36
+ * @param pathname Path without query, e.g. "/movies/inception".
37
+ * @param search Query string, e.g. "?reviewed=1" (optional).
38
+ * @param push Whether to push a history entry (default true).
39
+ * @returns true on success, false if the render failed.
40
+ */
41
+ export declare function navigateTo(pathname: string, search?: string, push?: boolean): Promise<boolean>;
42
+ export declare function startClientRouter(): void;
43
+ /**
44
+ * Resets all internal router state. Intended for test isolation only.
45
+ * @internal
46
+ */
47
+ export declare function __resetRouterState(): void;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Client-side router for Elur Kit.
3
+ *
4
+ * Intercepts clicks on internal links, fetches the rendered page body from
5
+ * `/__elur-js/render`, swaps the `#app` content and updates the history state.
6
+ * This is loaded as part of the client bundle instead of being inlined in
7
+ * every HTML page.
8
+ *
9
+ * Features:
10
+ * - SPA navigation with head merge (title, meta, OG tags)
11
+ * - Scroll restoration on back/forward
12
+ * - Prefetch on hover/focus (Astro-style), with opt-in viewport prefetch
13
+ * - View Transitions API with `prefers-reduced-motion` respect
14
+ */
15
+ /**
16
+ * Prefetches a path without navigating. Called by the IntersectionObserver
17
+ * when a link enters the viewport, and on hover/focus.
18
+ */
19
+ export declare function prefetch(pathname: string, search?: string): Promise<void>;
20
+ /**
21
+ * Hoists `<link rel="stylesheet">` and `<style>` tags from inside `#app` into
22
+ * `<head>` so they persist across SPA navigations (prevents FOUC/flashing).
23
+ * Deduplicates by `href` for links and by text content for styles.
24
+ */
25
+ export declare function hoistStyles(container: ParentNode): void;
26
+ /**
27
+ * Navigates to a page without a full reload: fetches the fresh body from the
28
+ * `/__elur-js/render` endpoint, swaps `#app`, updates the document title and
29
+ * dispatches `elur:rendered` so islands re-hydrate. Used by the router on
30
+ * clicks and available for programmatic navigation (e.g. after a server
31
+ * action returns a redirect, so the target page shows fresh server data).
32
+ *
33
+ * Concurrent navigations are handled: a new navigateTo() cancels any
34
+ * in-flight navigation to prevent out-of-order content swaps.
35
+ *
36
+ * @param pathname Path without query, e.g. "/movies/inception".
37
+ * @param search Query string, e.g. "?reviewed=1" (optional).
38
+ * @param push Whether to push a history entry (default true).
39
+ * @returns true on success, false if the render failed.
40
+ */
41
+ export declare function navigateTo(pathname: string, search?: string, push?: boolean): Promise<boolean>;
42
+ export declare function startClientRouter(): void;
43
+ /**
44
+ * Resets all internal router state. Intended for test isolation only.
45
+ * @internal
46
+ */
47
+ export declare function __resetRouterState(): void;
@@ -0,0 +1,298 @@
1
+ //#region src/router/client.ts
2
+ var e = !0, t = null;
3
+ function n() {
4
+ return e ? (t ||= (async () => {
5
+ let t = new URL("/__elur-js/render", location.origin);
6
+ t.searchParams.set("page", "/");
7
+ try {
8
+ let n = await fetch(t.toString(), { headers: {
9
+ Accept: "application/json",
10
+ "X-Elur-Probe": "1"
11
+ } });
12
+ return e = n.ok, n.ok;
13
+ } catch {
14
+ return e = !1, !1;
15
+ }
16
+ })(), t) : Promise.resolve(!1);
17
+ }
18
+ function r(e) {
19
+ return e.tagName === "A" && e.hostname === location.hostname && e.target === "" && !e.getAttribute("download") && !e.hasAttribute("data-no-router");
20
+ }
21
+ function i(e) {
22
+ return e.metaKey || e.ctrlKey || e.shiftKey || e.altKey;
23
+ }
24
+ var a = 3e4, o = /* @__PURE__ */ new Map();
25
+ function s(e, t) {
26
+ return e + t;
27
+ }
28
+ function c(e) {
29
+ let t = o.get(e);
30
+ if (t) {
31
+ if (Date.now() - t.ts > a) {
32
+ o.delete(e);
33
+ return;
34
+ }
35
+ return t.payload;
36
+ }
37
+ }
38
+ function l(e, t) {
39
+ o.set(e, {
40
+ payload: t,
41
+ ts: Date.now()
42
+ });
43
+ }
44
+ async function u(t, r, i) {
45
+ let a = s(t, r), o = c(a);
46
+ if (o) return o;
47
+ if (e && await n()) {
48
+ let e = await d(t, r, i);
49
+ if (e) return l(a, e), e;
50
+ }
51
+ let u = await f(t, r, i);
52
+ return u && l(a, u), u;
53
+ }
54
+ async function d(e, t, n) {
55
+ let r = new URL("/__elur-js/render", location.origin);
56
+ r.searchParams.set("page", e);
57
+ let i = new URL(location.href);
58
+ r.searchParams.set("search", t || i.search);
59
+ let a;
60
+ try {
61
+ a = await fetch(r.toString(), {
62
+ headers: { Accept: "application/json" },
63
+ signal: n
64
+ });
65
+ } catch (e) {
66
+ e instanceof DOMException && e.name;
67
+ return;
68
+ }
69
+ if (!a.ok) return;
70
+ let o;
71
+ try {
72
+ o = await a.json();
73
+ } catch {
74
+ return;
75
+ }
76
+ return o;
77
+ }
78
+ async function f(e, t, n) {
79
+ let r = e + (t || ""), i;
80
+ try {
81
+ i = await fetch(r, {
82
+ headers: { Accept: "text/html" },
83
+ signal: n
84
+ });
85
+ } catch (e) {
86
+ e instanceof DOMException && e.name;
87
+ return;
88
+ }
89
+ if (!i.ok) return;
90
+ let a;
91
+ try {
92
+ a = await i.text();
93
+ } catch {
94
+ return;
95
+ }
96
+ let o = new DOMParser().parseFromString(a, "text/html"), s = o.getElementById("app");
97
+ if (!s) return;
98
+ let c = s.innerHTML, l = o.querySelectorAll("[data-elur-head]"), u = "";
99
+ for (let e of l) u += e.outerHTML;
100
+ let d = o.head.querySelectorAll("link[rel='icon'], link[rel='apple-touch-icon'], link[rel='manifest'], meta[name='theme-color']");
101
+ for (let e of d) {
102
+ let t = e.getAttribute("href");
103
+ t && document.head.querySelector(`link[href="${t}"]`) || (u += e.outerHTML);
104
+ }
105
+ let f = o.querySelector("title")?.textContent ?? void 0;
106
+ return {
107
+ body: c,
108
+ head: u,
109
+ title: f
110
+ };
111
+ }
112
+ async function p(e, t = "") {
113
+ let n = s(e, t);
114
+ if (o.has(n)) {
115
+ let e = o.get(n);
116
+ if (Date.now() - e.ts <= a) return;
117
+ }
118
+ await u(e, t);
119
+ }
120
+ function m() {
121
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? !1;
122
+ }
123
+ function h() {
124
+ return typeof document.startViewTransition == "function";
125
+ }
126
+ var g = null;
127
+ function _() {
128
+ g &&= (g.controller.abort(), null);
129
+ }
130
+ function v(e) {
131
+ let t = e.querySelectorAll("link[rel=\"stylesheet\"]");
132
+ for (let e of t) {
133
+ let t = e.getAttribute("href");
134
+ if (t) {
135
+ if (document.head.querySelector(`link[rel="stylesheet"][href="${t}"]`)) {
136
+ e.remove();
137
+ continue;
138
+ }
139
+ e.setAttribute("data-elur-hoisted", ""), document.head.appendChild(e);
140
+ }
141
+ }
142
+ let n = e.querySelectorAll("style");
143
+ for (let e of n) {
144
+ let t = e.textContent?.trim();
145
+ if (t) {
146
+ if (Array.from(document.head.querySelectorAll("style")).find((e) => e.textContent?.trim() === t)) {
147
+ e.remove();
148
+ continue;
149
+ }
150
+ e.setAttribute("data-elur-hoisted", ""), document.head.appendChild(e);
151
+ }
152
+ }
153
+ }
154
+ function y(e) {
155
+ let t = document.getElementById("elur-route-announcer");
156
+ t || (t = document.createElement("div"), t.id = "elur-route-announcer", t.setAttribute("aria-live", "assertive"), t.setAttribute("aria-atomic", "true"), t.setAttribute("role", "status"), t.setAttribute("style", "position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden;"), document.body.appendChild(t)), t.textContent = "";
157
+ let n = t, r = setTimeout(() => {
158
+ if (typeof document < "u" && n) {
159
+ let t = document.title || e;
160
+ n.textContent = t;
161
+ }
162
+ }, 50);
163
+ typeof r == "object" && r && "unref" in r && r.unref();
164
+ }
165
+ function b() {
166
+ let e = document.getElementById("app");
167
+ e && (e.hasAttribute("tabindex") || e.setAttribute("tabindex", "-1"), e.focus({ preventScroll: !1 }));
168
+ }
169
+ function x(e, t) {
170
+ let n = location.origin + e + (t || ""), r = document.querySelector("link[rel=\"canonical\"]");
171
+ r || (r = document.createElement("link"), r.rel = "canonical", document.head.appendChild(r)), r.href = n;
172
+ let i = document.querySelector("meta[property=\"og:url\"]");
173
+ i || (i = document.createElement("meta"), i.setAttribute("property", "og:url"), document.head.appendChild(i)), i.content = n;
174
+ }
175
+ async function S(e, t = "", n = !0) {
176
+ _();
177
+ let r = new AbortController();
178
+ g = {
179
+ controller: r,
180
+ pathname: e
181
+ };
182
+ let i;
183
+ try {
184
+ i = await u(e, t, r.signal);
185
+ } catch (e) {
186
+ if (r.signal.aborted) return !1;
187
+ throw e;
188
+ }
189
+ if (g && g.controller !== r && r.signal.aborted || (g = null, !i)) return !1;
190
+ let a = document.getElementById("app");
191
+ if (!a) return !1;
192
+ n && history.replaceState({
193
+ n: location.pathname,
194
+ scroll: window.scrollY
195
+ }, "", location.href);
196
+ let o = new URL(location.href), s = () => {
197
+ let r = [];
198
+ a.querySelectorAll("[data-scroll-preserve]").forEach((e) => {
199
+ r.push({
200
+ el: e,
201
+ top: e.scrollTop
202
+ });
203
+ }), v(a);
204
+ let s = document.createElement("template");
205
+ s.innerHTML = i.body, v(s.content), a.innerHTML = s.innerHTML, C(i.head, i.title), i.clearActionErrorCookie && (document.cookie = i.clearActionErrorCookie), n && history.pushState({
206
+ n: e,
207
+ scroll: 0
208
+ }, "", e + (t || o.search));
209
+ let c = n ? 0 : history.state?.scroll ?? 0;
210
+ window.scrollTo(0, c);
211
+ for (let e of r) {
212
+ let t = a.querySelector(`[data-scroll-preserve="${e.el.getAttribute("data-scroll-preserve")}"]`);
213
+ t && (t.scrollTop = e.top);
214
+ }
215
+ x(e, t), y(e), n && b(), document.dispatchEvent(new CustomEvent("elur:rendered"));
216
+ };
217
+ return h() && !m() ? document.startViewTransition(() => s()) : s(), !0;
218
+ }
219
+ function C(e, t) {
220
+ if (document.querySelectorAll("[data-elur-head]").forEach((e) => e.remove()), e && e.trim().length > 0) {
221
+ let t = document.createElement("template");
222
+ t.innerHTML = e;
223
+ let n = t.content, r = n.querySelector("title");
224
+ r && (document.title = r.textContent ?? "", r.remove()), document.head.appendChild(n);
225
+ } else t && (document.title = t);
226
+ }
227
+ var w = /* @__PURE__ */ new WeakSet();
228
+ function T() {
229
+ let e = (e) => {
230
+ let t = e.getAttribute("href");
231
+ if (!t || t.startsWith("#") || t.startsWith("mailto:") || t.startsWith("javascript:")) return null;
232
+ let n = t.indexOf("?");
233
+ return {
234
+ path: n === -1 ? t : t.slice(0, n),
235
+ search: n === -1 ? "" : t.slice(n)
236
+ };
237
+ }, t = (t) => {
238
+ if (w.has(t) || !r(t) || t.hasAttribute("data-no-prefetch")) return;
239
+ w.add(t);
240
+ let n = () => {
241
+ let n = e(t);
242
+ n && p(n.path, n.search);
243
+ };
244
+ if (t.addEventListener("pointerenter", n, { once: !0 }), t.addEventListener("focus", n, { once: !0 }), t.dataset.prefetch === "viewport" && "IntersectionObserver" in window) {
245
+ let n = new IntersectionObserver((t) => {
246
+ for (let r of t) {
247
+ if (!r.isIntersecting) continue;
248
+ let t = e(r.target);
249
+ t && p(t.path, t.search), n.disconnect();
250
+ }
251
+ }, {
252
+ rootMargin: "200px",
253
+ threshold: 0
254
+ });
255
+ n.observe(t);
256
+ }
257
+ }, n = () => {
258
+ let e = document.querySelectorAll("a[href]");
259
+ for (let n of e) t(n);
260
+ };
261
+ n(), new MutationObserver(() => n()).observe(document.body, {
262
+ childList: !0,
263
+ subtree: !0
264
+ }), document.addEventListener("elur:rendered", n);
265
+ }
266
+ function E() {
267
+ document.querySelector("meta[name=\"elur:render-endpoint\"]")?.getAttribute("content") === "off" && (e = !1);
268
+ let t = document.getElementById("app");
269
+ t && v(t), document.addEventListener("click", async (e) => {
270
+ if (!(e instanceof MouseEvent) || i(e) || e.defaultPrevented) return;
271
+ let t = e.target.closest("a");
272
+ if (!t || !r(t)) return;
273
+ let n = t.getAttribute("href");
274
+ if (!n || n.startsWith("mailto:") || n.startsWith("javascript:")) return;
275
+ if (n.startsWith("#")) {
276
+ if (n.length > 1) {
277
+ let t = document.getElementById(n.slice(1));
278
+ t && (e.preventDefault(), t.scrollIntoView({
279
+ behavior: "smooth",
280
+ block: "start"
281
+ }), history.replaceState(null, "", n));
282
+ }
283
+ return;
284
+ }
285
+ e.preventDefault();
286
+ let a = n.indexOf("?");
287
+ await S(a === -1 ? n : n.slice(0, a), a === -1 ? "" : n.slice(a)) || location.assign(n);
288
+ }), window.addEventListener("popstate", (e) => {
289
+ S(e.state?.n ?? location.pathname, location.search, !1);
290
+ }), T();
291
+ }
292
+ function D() {
293
+ o.clear(), g = null, e = !0, t = null;
294
+ }
295
+ //#endregion
296
+ export { D as __resetRouterState, v as hoistStyles, S as navigateTo, p as prefetch, E as startClientRouter };
297
+
298
+ //# sourceMappingURL=client.js.map
@@ -0,0 +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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"}