@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.
Files changed (188) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/README.md +248 -15
  3. package/dist/lib/action/index.cjs +1 -1
  4. package/dist/lib/action/index.js +1 -1
  5. package/dist/lib/{action-DZHihU3Z.cjs → action-BiE4tEty.cjs} +2 -2
  6. package/dist/lib/{action-DZHihU3Z.cjs.map → action-BiE4tEty.cjs.map} +1 -1
  7. package/dist/lib/{action-BB6N5yD-.js → action-CXVk8rWt.js} +3 -3
  8. package/dist/lib/{action-BB6N5yD-.js.map → action-CXVk8rWt.js.map} +1 -1
  9. package/dist/lib/adapter-DNsjXvq5.js +160 -0
  10. package/dist/lib/adapter-DNsjXvq5.js.map +1 -0
  11. package/dist/lib/adapter-Dz2hzR36.cjs +3 -0
  12. package/dist/lib/adapter-Dz2hzR36.cjs.map +1 -0
  13. package/dist/lib/adapters/bun.cjs +2 -2
  14. package/dist/lib/adapters/bun.cjs.map +1 -1
  15. package/dist/lib/adapters/bun.js +17 -9
  16. package/dist/lib/adapters/bun.js.map +1 -1
  17. package/dist/lib/adapters/index.d.cts +34 -0
  18. package/dist/lib/adapters/index.d.ts +34 -0
  19. package/dist/lib/adapters/netlify.cjs +1 -1
  20. package/dist/lib/adapters/netlify.js +7 -7
  21. package/dist/lib/adapters/node.cjs +5 -5
  22. package/dist/lib/adapters/node.cjs.map +1 -1
  23. package/dist/lib/adapters/node.js +20 -12
  24. package/dist/lib/adapters/node.js.map +1 -1
  25. package/dist/lib/adapters/vercel.cjs +1 -1
  26. package/dist/lib/adapters/vercel.js +8 -8
  27. package/dist/lib/build/build.d.cts +40 -0
  28. package/dist/lib/build/build.d.ts +40 -0
  29. package/dist/lib/build/document-shell.d.cts +47 -1
  30. package/dist/lib/build/document-shell.d.ts +47 -1
  31. package/dist/lib/build/vite-build.d.cts +25 -2
  32. package/dist/lib/build/vite-build.d.ts +25 -2
  33. package/dist/lib/cache/adapter.d.cts +41 -0
  34. package/dist/lib/cache/adapter.d.ts +41 -0
  35. package/dist/lib/cache/index.cjs +2 -0
  36. package/dist/lib/cache/index.cjs.map +1 -0
  37. package/dist/lib/cache/index.d.cts +5 -0
  38. package/dist/lib/cache/index.d.ts +5 -0
  39. package/dist/lib/cache/index.js +113 -0
  40. package/dist/lib/cache/index.js.map +1 -0
  41. package/dist/lib/cache/invalidation.d.cts +36 -0
  42. package/dist/lib/cache/invalidation.d.ts +36 -0
  43. package/dist/lib/cache/redis-adapter.d.cts +49 -0
  44. package/dist/lib/cache/redis-adapter.d.ts +49 -0
  45. package/dist/lib/cache-DGhTYyqh.js +50 -0
  46. package/dist/lib/cache-DGhTYyqh.js.map +1 -0
  47. package/dist/lib/cache-X1n4Ossu.cjs +2 -0
  48. package/dist/lib/cache-X1n4Ossu.cjs.map +1 -0
  49. package/dist/lib/cli/output.d.cts +58 -0
  50. package/dist/lib/cli/output.d.ts +58 -0
  51. package/dist/lib/cli/ports.d.cts +20 -0
  52. package/dist/lib/cli/ports.d.ts +20 -0
  53. package/dist/lib/cli.cjs +2559 -1488
  54. package/dist/lib/cli.cjs.map +1 -1
  55. package/dist/lib/cli.d.cts +21 -0
  56. package/dist/lib/cli.d.ts +21 -0
  57. package/dist/lib/cli.js +2558 -1488
  58. package/dist/lib/cli.js.map +1 -1
  59. package/dist/lib/config/index.cjs +1 -1
  60. package/dist/lib/config/index.cjs.map +1 -1
  61. package/dist/lib/config/index.d.cts +79 -0
  62. package/dist/lib/config/index.d.ts +79 -0
  63. package/dist/lib/config/index.js +21 -14
  64. package/dist/lib/config/index.js.map +1 -1
  65. package/dist/lib/{errors-ePYxDlhU.js → errors-j4cg9VN3.js} +24 -30
  66. package/dist/lib/{errors-ePYxDlhU.js.map → errors-j4cg9VN3.js.map} +1 -1
  67. package/dist/lib/errors-kSl9yLkg.cjs +2 -0
  68. package/dist/lib/{errors-0X7pCFNU.cjs.map → errors-kSl9yLkg.cjs.map} +1 -1
  69. package/dist/lib/hydrate-BSLuI0Nh.cjs +2 -0
  70. package/dist/lib/hydrate-BSLuI0Nh.cjs.map +1 -0
  71. package/dist/lib/hydrate-BaS7-d8C.js +133 -0
  72. package/dist/lib/hydrate-BaS7-d8C.js.map +1 -0
  73. package/dist/lib/idiomorph.esm-BOOTOg1r.cjs +2 -0
  74. package/dist/lib/idiomorph.esm-BOOTOg1r.cjs.map +1 -0
  75. package/dist/lib/idiomorph.esm-CXuY-EHB.js +371 -0
  76. package/dist/lib/idiomorph.esm-CXuY-EHB.js.map +1 -0
  77. package/dist/lib/index.cjs +1 -1
  78. package/dist/lib/index.cjs.map +1 -1
  79. package/dist/lib/index.d.cts +8 -6
  80. package/dist/lib/index.d.ts +8 -6
  81. package/dist/lib/index.js +1 -1
  82. package/dist/lib/index.js.map +1 -1
  83. package/dist/lib/interpolation-plugin-C0dTd69H.cjs +58 -0
  84. package/dist/lib/interpolation-plugin-C0dTd69H.cjs.map +1 -0
  85. package/dist/lib/{interpolation-plugin-Wgb1j4pT.js → interpolation-plugin-K2qE69tF.js} +95 -126
  86. package/dist/lib/interpolation-plugin-K2qE69tF.js.map +1 -0
  87. package/dist/lib/invalidation-CFpNQUv6.cjs +2 -0
  88. package/dist/lib/invalidation-CFpNQUv6.cjs.map +1 -0
  89. package/dist/lib/invalidation-DXhdE9wj.js +64 -0
  90. package/dist/lib/invalidation-DXhdE9wj.js.map +1 -0
  91. package/dist/lib/island/generate-entry.d.cts +40 -2
  92. package/dist/lib/island/generate-entry.d.ts +40 -2
  93. package/dist/lib/island/hydrate.d.cts +19 -6
  94. package/dist/lib/island/hydrate.d.ts +19 -6
  95. package/dist/lib/island/index.cjs +1 -1
  96. package/dist/lib/island/index.d.cts +2 -1
  97. package/dist/lib/island/index.d.ts +2 -1
  98. package/dist/lib/island/index.js +1 -1
  99. package/dist/lib/island/island.d.cts +15 -0
  100. package/dist/lib/island/island.d.ts +15 -0
  101. package/dist/lib/island-CTBnsOhI.js +59 -0
  102. package/dist/lib/island-CTBnsOhI.js.map +1 -0
  103. package/dist/lib/island-kfQEKRe_.cjs +2 -0
  104. package/dist/lib/island-kfQEKRe_.cjs.map +1 -0
  105. package/dist/lib/node-http-DVFcYeBS.js +723 -0
  106. package/dist/lib/node-http-DVFcYeBS.js.map +1 -0
  107. package/dist/lib/node-http-Uct7DhmN.cjs +13 -0
  108. package/dist/lib/node-http-Uct7DhmN.cjs.map +1 -0
  109. package/dist/lib/rolldown-runtime-BIInC43l.cjs +1 -0
  110. package/dist/lib/rolldown-runtime-Dy4uBu1J.js +11 -0
  111. package/dist/lib/router/client.cjs +1 -1
  112. package/dist/lib/router/client.cjs.map +1 -1
  113. package/dist/lib/router/client.d.cts +55 -8
  114. package/dist/lib/router/client.d.ts +55 -8
  115. package/dist/lib/router/client.js +298 -97
  116. package/dist/lib/router/client.js.map +1 -1
  117. package/dist/lib/router/redirects.d.cts +34 -0
  118. package/dist/lib/router/redirects.d.ts +34 -0
  119. package/dist/lib/runtime/handler.d.cts +59 -0
  120. package/dist/lib/runtime/handler.d.ts +59 -0
  121. package/dist/lib/runtime/index.cjs +1 -1
  122. package/dist/lib/runtime/index.d.cts +2 -1
  123. package/dist/lib/runtime/index.d.ts +2 -1
  124. package/dist/lib/runtime/index.js +4 -4
  125. package/dist/lib/runtime/logger.d.cts +47 -0
  126. package/dist/lib/runtime/logger.d.ts +47 -0
  127. package/dist/lib/runtime/node-http.d.cts +11 -1
  128. package/dist/lib/runtime/node-http.d.ts +11 -1
  129. package/dist/lib/runtime-ChE4flX3.cjs +2 -0
  130. package/dist/lib/runtime-ChE4flX3.cjs.map +1 -0
  131. package/dist/lib/runtime-OMyz5yE-.js +905 -0
  132. package/dist/lib/runtime-OMyz5yE-.js.map +1 -0
  133. package/dist/lib/seo/index.cjs +1 -10
  134. package/dist/lib/seo/index.d.cts +1 -0
  135. package/dist/lib/seo/index.d.ts +1 -0
  136. package/dist/lib/seo/index.js +2 -41
  137. package/dist/lib/seo/sitemap-from-routes.d.cts +24 -0
  138. package/dist/lib/seo/sitemap-from-routes.d.ts +24 -0
  139. package/dist/lib/{shared-CfqOLsKj.cjs → shared-CP0Dc3cp.cjs} +72 -47
  140. package/dist/lib/shared-CP0Dc3cp.cjs.map +1 -0
  141. package/dist/lib/{shared-DU3iQgr7.js → shared-D8w-neq2.js} +87 -58
  142. package/dist/lib/shared-D8w-neq2.js.map +1 -0
  143. package/dist/lib/sitemap-from-routes-CaBoiGvE.js +77 -0
  144. package/dist/lib/sitemap-from-routes-CaBoiGvE.js.map +1 -0
  145. package/dist/lib/sitemap-from-routes-DbcLLL4G.cjs +13 -0
  146. package/dist/lib/sitemap-from-routes-DbcLLL4G.cjs.map +1 -0
  147. package/dist/lib/ssr/render.d.cts +8 -2
  148. package/dist/lib/ssr/render.d.ts +8 -2
  149. package/dist/lib/ssr/server.d.cts +7 -0
  150. package/dist/lib/ssr/server.d.ts +7 -0
  151. package/dist/lib/ssr/stream-response.d.cts +30 -0
  152. package/dist/lib/ssr/stream-response.d.ts +30 -0
  153. package/dist/lib/ssr/stream.d.cts +16 -1
  154. package/dist/lib/ssr/stream.d.ts +16 -1
  155. package/dist/lib/{stream-DmZjYqHm.cjs → stream-Cg-xYG82.cjs} +3 -3
  156. package/dist/lib/stream-Cg-xYG82.cjs.map +1 -0
  157. package/dist/lib/stream-D-ytogdu.js +106 -0
  158. package/dist/lib/stream-D-ytogdu.js.map +1 -0
  159. package/dist/lib/vite/index.cjs +1 -1
  160. package/dist/lib/vite/index.cjs.map +1 -1
  161. package/dist/lib/vite/index.d.cts +11 -0
  162. package/dist/lib/vite/index.d.ts +11 -0
  163. package/dist/lib/vite/index.js +101 -94
  164. package/dist/lib/vite/index.js.map +1 -1
  165. package/package.json +25 -8
  166. package/dist/lib/errors-0X7pCFNU.cjs +0 -2
  167. package/dist/lib/hydrate-CJbUWdXu.js +0 -126
  168. package/dist/lib/hydrate-CJbUWdXu.js.map +0 -1
  169. package/dist/lib/hydrate-D6MR24mz.cjs +0 -2
  170. package/dist/lib/hydrate-D6MR24mz.cjs.map +0 -1
  171. package/dist/lib/interpolation-plugin-TWDch8nn.cjs +0 -39
  172. package/dist/lib/interpolation-plugin-TWDch8nn.cjs.map +0 -1
  173. package/dist/lib/interpolation-plugin-Wgb1j4pT.js.map +0 -1
  174. package/dist/lib/node-http-DRAUhO0c.js +0 -617
  175. package/dist/lib/node-http-DRAUhO0c.js.map +0 -1
  176. package/dist/lib/node-http-DhxguYyz.cjs +0 -13
  177. package/dist/lib/node-http-DhxguYyz.cjs.map +0 -1
  178. package/dist/lib/runtime-CrH5X-Es.cjs +0 -2
  179. package/dist/lib/runtime-CrH5X-Es.cjs.map +0 -1
  180. package/dist/lib/runtime-Ds2-wH6P.js +0 -520
  181. package/dist/lib/runtime-Ds2-wH6P.js.map +0 -1
  182. package/dist/lib/seo/index.cjs.map +0 -1
  183. package/dist/lib/seo/index.js.map +0 -1
  184. package/dist/lib/shared-CfqOLsKj.cjs.map +0 -1
  185. package/dist/lib/shared-DU3iQgr7.js.map +0 -1
  186. package/dist/lib/stream-CpOG052V.js +0 -104
  187. package/dist/lib/stream-CpOG052V.js.map +0 -1
  188. package/dist/lib/stream-DmZjYqHm.cjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared-CP0Dc3cp.cjs","names":[],"sources":["../../src/adapters/shared.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { mkdir, readdir, copyFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join, relative, resolve } from \"node:path\";\nimport { scanRoutes } from \"../router/route-scanner.js\";\nimport type { AdapterOptions } from \"./index.js\";\nimport type { PageRoute } from \"../router/route-scanner.js\";\n\n/**\n * Shared helper: copy a directory recursively.\n */\nexport async function copyStatic(from: string, to: string): Promise<void> {\n await mkdir(to, { recursive: true });\n const entries = await readdir(from, { withFileTypes: true });\n for (const entry of entries) {\n const src = join(from, entry.name);\n const dest = join(to, entry.name);\n if (entry.isDirectory()) {\n await copyStatic(src, dest);\n } else {\n await copyFile(src, dest);\n }\n }\n}\n\n/** Adds every module the SSR runtime may import for a page to the registry. */\nfunction collectPageModules(\n page: PageRoute,\n moduleSet: Set<string>,\n actionPathsByPage: Map<string, Set<string>>,\n): void {\n moduleSet.add(page.pagePath);\n if (page.dataPath) moduleSet.add(page.dataPath);\n if (page.loadingPath) moduleSet.add(page.loadingPath);\n for (const layout of page.layouts) {\n moduleSet.add(layout);\n const layoutDataPath = layout.replace(/layout\\.ts$/, \"layout.data.ts\");\n if (layoutDataPath !== layout && existsSync(layoutDataPath)) {\n moduleSet.add(layoutDataPath);\n }\n }\n if (page.actionPath) {\n moduleSet.add(page.actionPath);\n let set = actionPathsByPage.get(page.path);\n if (!set) {\n set = new Set<string>();\n actionPathsByPage.set(page.path, set);\n }\n set.add(page.actionPath);\n }\n}\n\n/**\n * Build a self-contained SSR entry file for a platform adapter.\n * The generated module exports a default `handler(request: Request): Response`\n * and embeds the full route table plus a registry of all page/layout/data/\n * action modules so the runtime never touches the file system.\n */\nexport async function buildSsrEntry(\n routes: Awaited<ReturnType<typeof scanRoutes>>,\n options: AdapterOptions,\n entryDir: string,\n): Promise<string> {\n // Collect all module paths that the SSR runtime may need to import.\n const moduleSet = new Set<string>();\n const actionPathsByPage = new Map<string, Set<string>>();\n for (const page of routes.pages) {\n collectPageModules(page, moduleSet, actionPathsByPage);\n }\n if (routes.error404) collectPageModules(routes.error404, moduleSet, actionPathsByPage);\n if (routes.error500) collectPageModules(routes.error500, moduleSet, actionPathsByPage);\n for (const api of routes.api) {\n moduleSet.add(api.routePath);\n }\n const modules = Array.from(moduleSet);\n const moduleIndex = new Map(modules.map((path, index) => [path, index]));\n\n const imports = modules\n .map((path, index) => {\n const rel = relativeToPosix(entryDir, path);\n return `import * as m_${index} from ${JSON.stringify(rel)};`;\n })\n .join(\"\\n\");\n\n const renderPageRecord = (page: PageRoute): string => `{\n path: ${JSON.stringify(page.path)},\n pagePath: ${JSON.stringify(page.pagePath)},\n dataPath: ${JSON.stringify(page.dataPath ?? null)},\n actionPath: ${JSON.stringify(page.actionPath ?? null)},\n loadingPath: ${JSON.stringify(page.loadingPath ?? null)},\n layouts: ${JSON.stringify(page.layouts)},\n params: ${JSON.stringify(page.params)},\n }`;\n\n const pages = routes.pages.map(renderPageRecord).join(\",\\n\");\n\n const apiRoutes = routes.api\n .map((api) => {\n const index = moduleIndex.get(api.routePath);\n return ` { path: ${JSON.stringify(api.path)}, routePath: m_${index} },`;\n })\n .join(\"\\n\");\n\n const actionModules = Array.from(actionPathsByPage.entries())\n .map(([pagePath, paths]) => {\n const entries = Array.from(paths)\n .map((path) => {\n const index = moduleIndex.get(path);\n return ` [${JSON.stringify(path)}, m_${index}],`;\n })\n .join(\"\\n\");\n return ` [${JSON.stringify(pagePath)}, new Map([\\n${entries}\\n ])],`;\n })\n .join(\"\\n\");\n\n const actionsRegistry: Record<string, string[]> = {};\n for (const page of routes.pages) {\n if (!page.actionPath) continue;\n const mod = (await import(page.actionPath)) as Record<string, unknown>;\n const names: string[] = [];\n for (const [name, value] of Object.entries(mod)) {\n if (name === \"default\") continue;\n if (typeof value === \"function\") {\n names.push(name);\n }\n }\n if (names.length > 0) {\n actionsRegistry[page.path] = names;\n }\n }\n\n // The render config baked into the generated handler. The split router\n // chunk is advertised only when the bundle actually emitted it (the file\n // exists in the build output), so single-input legacy bundles keep working.\n const routerEnabled = options.router?.enabled !== false;\n const routerEntry =\n routerEnabled && options.js !== \"legacy\" &&\n existsSync(resolve(options.root, options.outDir, \"_elur\", \"router.js\"))\n ? \"/_elur/router.js\"\n : null;\n\n return `// AUTO-GENERATED by @elurjs/kit. Do not edit.\nimport { handleActionRequest, matchApiRoute, matchRoute, renderPage, renderPageBody, renderErrorPage, createStreamingResponse } from \"@elurjs/kit\";\n${imports}\n\nconst registry = new Map<string, unknown>([\n${modules.map((path, index) => ` [${JSON.stringify(path)}, m_${index}],`).join(\"\\n\")}\n]);\n\nconst pages = [\n${pages},\n];\n\nconst apiRoutes = [\n${apiRoutes}\n];\n\nconst actionModules = new Map<string, Map<string, unknown>>([\n${actionModules}\n]);\n\nconst actions = ${JSON.stringify(actionsRegistry)};\n\nconst routes = {\n pages,\n api: apiRoutes,\n error404: ${routes.error404 ? renderPageRecord(routes.error404) : \"undefined\"},\n error500: ${routes.error500 ? renderPageRecord(routes.error500) : \"undefined\"},\n};\n\nconst clientEntry = ${JSON.stringify(options.clientEntry)};\nconst lang = ${JSON.stringify(options.lang)};\n// Client router/JS emission rules baked at adapter build time.\nconst router = ${JSON.stringify({ enabled: routerEnabled, entry: routerEntry })};\nconst jsMode = ${JSON.stringify(options.js === \"legacy\" ? \"legacy\" : \"modern\")};\n// Opt-in streaming SSR (experimental): routes with a loading boundary stream\n// the shell first and swap in the resolved content as a follow-up chunk.\nconst streaming = ${options.streaming === true};\n\nfunction loadModule(path: string) {\n const mod = registry.get(path);\n if (mod) return mod;\n throw new Error(\\`Module not found in registry: \\${path}\\`);\n}\n\nasync function resolveAction(name: string, page?: string) {\n // Match concrete page paths (e.g. /movies/inception) to their route pattern\n // (/movies/:slug) so actions on dynamic routes resolve by scope.\n let pageKey: string | undefined;\n if (page) {\n pageKey = routes.pages.some((route) => route.path === page)\n ? page\n : (matchRoute(page, routes.pages)?.route.path ?? page);\n }\n const pageModules = pageKey ? actionModules.get(pageKey) : undefined;\n const candidates = pageModules ? [...pageModules.values()] : [];\n if (!pageModules) {\n for (const mods of actionModules.values()) {\n for (const mod of mods.values()) {\n const action = (mod as Record<string, unknown>)[name];\n if (typeof action === \"function\") return action;\n }\n }\n }\n for (const mod of candidates) {\n const action = (mod as Record<string, unknown>)[name];\n if (typeof action === \"function\") {\n return action as (...args: unknown[]) => unknown;\n }\n }\n return undefined;\n}\n\nexport default async function handler(request: Request): Promise<Response> {\n const url = new URL(request.url);\n\n if (url.pathname === \"/__elur-js/actions\") {\n return handleActionRequest(request, resolveAction);\n }\n\n // Render endpoint used by the SPA router and streaming boundaries.\n if (url.pathname === \"/__elur-js/render\") {\n const page = url.searchParams.get(\"page\") ?? \"/\";\n const search = url.searchParams.get(\"search\") ?? \"\";\n const wantsJson = (request.headers.get(\"Accept\") ?? \"\").includes(\"application/json\");\n try {\n const result = await renderPageBody({\n routes,\n pathname: page,\n searchParams: new URLSearchParams(search),\n config: { lang, clientEntry, router, js: jsMode },\n importer: loadModule,\n actions,\n request,\n });\n // A thrown Response from a loader is a first-class response (A-22).\n if (result.response) return result.response;\n const { body, title, head, clearActionErrorCookie, data, actions: actionsPayload } = result;\n if (wantsJson) {\n const headers = { \"Content-Type\": \"application/json; charset=utf-8\" };\n if (clearActionErrorCookie) headers[\"X-Elur-Action-Clear-Cookie\"] = clearActionErrorCookie;\n // The ?? null fallbacks keep every key present — JSON.stringify drops\n // undefined and the SPA payload shape must be stable across runtimes.\n return new Response(\n JSON.stringify({\n title,\n body,\n head: head ?? null,\n data: data ?? null,\n actions: actionsPayload ?? null,\n clearActionErrorCookie: clearActionErrorCookie ?? null,\n }),\n { status: 200, headers },\n );\n }\n const headers = { \"Content-Type\": \"text/html; charset=utf-8\" };\n if (clearActionErrorCookie) headers[\"Set-Cookie\"] = clearActionErrorCookie;\n return new Response(body, { status: 200, headers });\n } catch (err) {\n if ((err as { name?: string }).name === \"RouteNotFoundError\") {\n return new Response(\"Not Found\", {\n status: 404,\n headers: { \"Content-Type\": \"text/plain\" },\n });\n }\n console.error(\"[elur-kit] render endpoint error:\", err);\n return new Response(\"Internal Server Error\", {\n status: 500,\n headers: { \"Content-Type\": \"text/plain\" },\n });\n }\n }\n\n const apiMatch = matchApiRoute(url.pathname, apiRoutes);\n if (apiMatch) {\n const mod = apiMatch.route.routePath as Record<\n string,\n (request: Request, context?: { params: Record<string, string | string[]> }) => unknown\n >;\n const handler = mod[request.method ?? \"GET\"];\n if (typeof handler !== \"function\") {\n return new Response(\"Method not allowed: \" + request.method, { status: 405, headers: { \"Content-Type\": \"text/plain\" } });\n }\n return (await handler(request, { params: apiMatch.params })) as Response;\n }\n\n const match = matchRoute(url.pathname, routes.pages);\n if (!match) {\n const errorResult = await renderErrorPage({ routes, status: 404, config: { lang, clientEntry, router, js: jsMode }, actions, importer: loadModule });\n if (errorResult) {\n return new Response(errorResult.html, { status: errorResult.status, headers: { \"Content-Type\": \"text/html; charset=utf-8\" } });\n }\n return new Response(\"Not Found\", { status: 404, headers: { \"Content-Type\": \"text/plain\" } });\n }\n\n try {\n if (streaming && match.route.loadingPath) {\n // Streaming SSR: shell + loading boundary first, resolved content as a\n // follow-up chunk. Streamed pages are rendered live (no cache).\n return createStreamingResponse({\n route: match.route,\n params: match.params,\n searchParams: new URLSearchParams(url.search),\n config: { lang, clientEntry, router, js: jsMode },\n importer: loadModule,\n actions,\n request,\n signal: request.signal,\n });\n }\n const result = await renderPage({\n route: match.route,\n params: match.params,\n searchParams: new URLSearchParams(url.search),\n config: { lang, clientEntry, router, js: jsMode },\n importer: loadModule,\n actions,\n request,\n });\n // A thrown Response from a loader is a first-class response (A-22).\n if (result.response) return result.response;\n const headers = { \"Content-Type\": \"text/html; charset=utf-8\" };\n if (result.clearActionErrorCookie) headers[\"Set-Cookie\"] = result.clearActionErrorCookie;\n return new Response(result.html, { status: 200, headers });\n } catch (err) {\n console.error(\"[elur-kit] SSR render error:\", err);\n const errorResult = await renderErrorPage({ routes, status: 500, error: err, config: { lang, clientEntry, router, js: jsMode }, actions, importer: loadModule });\n if (errorResult) {\n return new Response(errorResult.html, { status: errorResult.status, headers: { \"Content-Type\": \"text/html; charset=utf-8\" } });\n }\n return new Response(\"Internal Server Error\", { status: 500, headers: { \"Content-Type\": \"text/plain; charset=utf-8\" } });\n }\n}\n`;\n}\n\nfunction relativeToPosix(from: string, to: string): string {\n return relative(from, to).split(\"\\\\\").join(\"/\");\n}\n\n/**\n * Write a generated SSR entry file for an adapter.\n */\nexport async function writeSsrEntry(\n entryPath: string,\n routes: Awaited<ReturnType<typeof scanRoutes>>,\n options: AdapterOptions,\n): Promise<void> {\n await writeFile(\n entryPath,\n await buildSsrEntry(routes, options, dirname(entryPath)),\n \"utf8\",\n );\n}\n"],"mappings":"8EAUA,eAAsB,EAAW,EAAc,EAA2B,CACxE,MAAA,EAAM,EAAA,MAAA,CAAM,EAAI,CAAE,UAAW,EAAK,CAAC,EACnC,IAAM,EAAU,MAAA,EAAM,EAAA,QAAA,CAAQ,EAAM,CAAE,cAAe,EAAK,CAAC,EAC3D,IAAK,IAAM,KAAS,EAAS,CAC3B,IAAM,GAAA,EAAM,EAAA,KAAA,CAAK,EAAM,EAAM,IAAI,EAC3B,GAAA,EAAO,EAAA,KAAA,CAAK,EAAI,EAAM,IAAI,EAC5B,EAAM,YAAY,EACpB,MAAM,EAAW,EAAK,CAAI,EAE1B,MAAA,EAAM,EAAA,SAAA,CAAS,EAAK,CAAI,CAE5B,CACF,CAGA,SAAS,EACP,EACA,EACA,EACM,CACN,EAAU,IAAI,EAAK,QAAQ,EACvB,EAAK,UAAU,EAAU,IAAI,EAAK,QAAQ,EAC1C,EAAK,aAAa,EAAU,IAAI,EAAK,WAAW,EACpD,IAAK,IAAM,KAAU,EAAK,QAAS,CACjC,EAAU,IAAI,CAAM,EACpB,IAAM,EAAiB,EAAO,QAAQ,cAAe,gBAAgB,EACjE,IAAmB,IAAA,EAAU,EAAA,WAAA,CAAW,CAAc,GACxD,EAAU,IAAI,CAAc,CAEhC,CACA,GAAI,EAAK,WAAY,CACnB,EAAU,IAAI,EAAK,UAAU,EAC7B,IAAI,EAAM,EAAkB,IAAI,EAAK,IAAI,EACpC,IACH,EAAM,IAAI,IACV,EAAkB,IAAI,EAAK,KAAM,CAAG,GAEtC,EAAI,IAAI,EAAK,UAAU,CACzB,CACF,CAQA,eAAsB,EACpB,EACA,EACA,EACiB,CAEjB,IAAM,EAAY,IAAI,IAChB,EAAoB,IAAI,IAC9B,IAAK,IAAM,KAAQ,EAAO,MACxB,EAAmB,EAAM,EAAW,CAAiB,EAEnD,EAAO,UAAU,EAAmB,EAAO,SAAU,EAAW,CAAiB,EACjF,EAAO,UAAU,EAAmB,EAAO,SAAU,EAAW,CAAiB,EACrF,IAAK,IAAM,KAAO,EAAO,IACvB,EAAU,IAAI,EAAI,SAAS,EAE7B,IAAM,EAAU,MAAM,KAAK,CAAS,EAC9B,EAAc,IAAI,IAAI,EAAQ,KAAK,EAAM,IAAU,CAAC,EAAM,CAAK,CAAC,CAAC,EAEjE,EAAU,EACb,KAAK,EAAM,IAAU,CACpB,IAAM,EAAM,EAAgB,EAAU,CAAI,EAC1C,MAAO,iBAAiB,EAAM,QAAQ,KAAK,UAAU,CAAG,EAAE,EAC5D,CAAC,CAAC,CACD,KAAK;CAAI,EAEN,EAAoB,GAA4B;YAC5C,KAAK,UAAU,EAAK,IAAI,EAAE;gBACtB,KAAK,UAAU,EAAK,QAAQ,EAAE;gBAC9B,KAAK,UAAU,EAAK,UAAY,IAAI,EAAE;kBACpC,KAAK,UAAU,EAAK,YAAc,IAAI,EAAE;mBACvC,KAAK,UAAU,EAAK,aAAe,IAAI,EAAE;eAC7C,KAAK,UAAU,EAAK,OAAO,EAAE;cAC9B,KAAK,UAAU,EAAK,MAAM,EAAE;KAGlC,EAAQ,EAAO,MAAM,IAAI,CAAgB,CAAC,CAAC,KAAK;CAAK,EAErD,EAAY,EAAO,IACtB,IAAK,GAAQ,CACZ,IAAM,EAAQ,EAAY,IAAI,EAAI,SAAS,EAC3C,MAAO,aAAa,KAAK,UAAU,EAAI,IAAI,EAAE,iBAAiB,EAAM,IACtE,CAAC,CAAC,CACD,KAAK;CAAI,EAEN,EAAgB,MAAM,KAAK,EAAkB,QAAQ,CAAC,CAAC,CAC1D,KAAK,CAAC,EAAU,KAAW,CAC1B,IAAM,EAAU,MAAM,KAAK,CAAK,CAAC,CAC9B,IAAK,GAAS,CACb,IAAM,EAAQ,EAAY,IAAI,CAAI,EAClC,MAAO,UAAU,KAAK,UAAU,CAAI,EAAE,MAAM,EAAM,GACpD,CAAC,CAAC,CACD,KAAK;CAAI,EACZ,MAAO,MAAM,KAAK,UAAU,CAAQ,EAAE,eAAe,EAAQ,WAC/D,CAAC,CAAC,CACD,KAAK;CAAI,EAEN,EAA4C,CAAC,EACnD,IAAK,IAAM,KAAQ,EAAO,MAAO,CAC/B,GAAI,CAAC,EAAK,WAAY,SACtB,IAAM,EAAO,MAAM,OAAO,EAAK,YACzB,EAAkB,CAAC,EACzB,IAAK,GAAM,CAAC,EAAM,KAAU,OAAO,QAAQ,CAAG,EACxC,IAAS,WACT,OAAO,GAAU,YACnB,EAAM,KAAK,CAAI,EAGf,EAAM,OAAS,IACjB,EAAgB,EAAK,MAAQ,EAEjC,CAKA,IAAM,EAAgB,EAAQ,QAAQ,UAAY,GAC5C,EACJ,GAAiB,EAAQ,KAAO,WAAA,EAC9B,EAAA,WAAA,EAAA,EAAW,EAAA,QAAA,CAAQ,EAAQ,KAAM,EAAQ,OAAQ,QAAS,WAAW,CAAC,EACpE,mBACA,KAEN,MAAO;;EAEP,EAAQ;;;EAGR,EAAQ,KAAK,EAAM,IAAU,MAAM,KAAK,UAAU,CAAI,EAAE,MAAM,EAAM,GAAG,CAAC,CAAC,KAAK;CAAI,EAAE;;;;EAIpF,EAAM;;;;EAIN,EAAU;;;;EAIV,EAAc;;;kBAGE,KAAK,UAAU,CAAe,EAAE;;;;;cAKpC,EAAO,SAAW,EAAiB,EAAO,QAAQ,EAAI,YAAY;cAClE,EAAO,SAAW,EAAiB,EAAO,QAAQ,EAAI,YAAY;;;sBAG1D,KAAK,UAAU,EAAQ,WAAW,EAAE;eAC3C,KAAK,UAAU,EAAQ,IAAI,EAAE;;iBAE3B,KAAK,UAAU,CAAE,QAAS,EAAe,MAAO,CAAY,CAAC,EAAE;iBAC/D,KAAK,UAAU,EAAQ,KAAO,SAAW,SAAW,QAAQ,EAAE;;;oBAG3D,EAAQ,YAAc,GAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6J/C,CAEA,SAAS,EAAgB,EAAc,EAAoB,CACzD,OAAA,EAAO,EAAA,SAAA,CAAS,EAAM,CAAE,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,GAAG,CAChD,CAKA,eAAsB,EACpB,EACA,EACA,EACe,CACf,MAAA,EAAM,EAAA,UAAA,CACJ,EACA,MAAM,EAAc,EAAQ,GAAA,EAAS,EAAA,QAAA,CAAQ,CAAS,CAAC,EACvD,MACF,CACF"}
@@ -1,21 +1,21 @@
1
1
  import { copyFile as e, mkdir as t, readdir as n, writeFile as r } from "node:fs/promises";
2
- import { dirname as i, join as a, relative as o } from "node:path";
3
- import { existsSync as s } from "node:fs";
2
+ import { dirname as i, join as a, relative as o, resolve as s } from "node:path";
3
+ import { existsSync as c } from "node:fs";
4
4
  //#region src/adapters/shared.ts
5
- async function c(r, i) {
5
+ async function l(r, i) {
6
6
  await t(i, { recursive: !0 });
7
7
  let o = await n(r, { withFileTypes: !0 });
8
8
  for (let t of o) {
9
9
  let n = a(r, t.name), o = a(i, t.name);
10
- t.isDirectory() ? await c(n, o) : await e(n, o);
10
+ t.isDirectory() ? await l(n, o) : await e(n, o);
11
11
  }
12
12
  }
13
- function l(e, t, n) {
13
+ function u(e, t, n) {
14
14
  t.add(e.pagePath), e.dataPath && t.add(e.dataPath), e.loadingPath && t.add(e.loadingPath);
15
15
  for (let n of e.layouts) {
16
16
  t.add(n);
17
17
  let e = n.replace(/layout\.ts$/, "layout.data.ts");
18
- e !== n && s(e) && t.add(e);
18
+ e !== n && c(e) && t.add(e);
19
19
  }
20
20
  if (e.actionPath) {
21
21
  t.add(e.actionPath);
@@ -23,15 +23,15 @@ function l(e, t, n) {
23
23
  r || (r = /* @__PURE__ */ new Set(), n.set(e.path, r)), r.add(e.actionPath);
24
24
  }
25
25
  }
26
- async function u(e, t, n) {
26
+ async function d(e, t, n) {
27
27
  let r = /* @__PURE__ */ new Set(), i = /* @__PURE__ */ new Map();
28
- for (let t of e.pages) l(t, r, i);
29
- e.error404 && l(e.error404, r, i), e.error500 && l(e.error500, r, i);
28
+ for (let t of e.pages) u(t, r, i);
29
+ e.error404 && u(e.error404, r, i), e.error500 && u(e.error500, r, i);
30
30
  for (let t of e.api) r.add(t.routePath);
31
- let a = Array.from(r), o = new Map(a.map((e, t) => [e, t])), s = a.map((e, t) => {
32
- let r = d(n, e);
31
+ let a = Array.from(r), o = new Map(a.map((e, t) => [e, t])), l = a.map((e, t) => {
32
+ let r = f(n, e);
33
33
  return `import * as m_${t} from ${JSON.stringify(r)};`;
34
- }).join("\n"), c = (e) => `{
34
+ }).join("\n"), d = (e) => `{
35
35
  path: ${JSON.stringify(e.path)},
36
36
  pagePath: ${JSON.stringify(e.pagePath)},
37
37
  dataPath: ${JSON.stringify(e.dataPath ?? null)},
@@ -39,53 +39,63 @@ async function u(e, t, n) {
39
39
  loadingPath: ${JSON.stringify(e.loadingPath ?? null)},
40
40
  layouts: ${JSON.stringify(e.layouts)},
41
41
  params: ${JSON.stringify(e.params)},
42
- }`, u = e.pages.map(c).join(",\n"), f = e.api.map((e) => {
42
+ }`, p = e.pages.map(d).join(",\n"), m = e.api.map((e) => {
43
43
  let t = o.get(e.routePath);
44
44
  return ` { path: ${JSON.stringify(e.path)}, routePath: m_${t} },`;
45
- }).join("\n"), p = Array.from(i.entries()).map(([e, t]) => {
45
+ }).join("\n"), h = Array.from(i.entries()).map(([e, t]) => {
46
46
  let n = Array.from(t).map((e) => {
47
47
  let t = o.get(e);
48
48
  return ` [${JSON.stringify(e)}, m_${t}],`;
49
49
  }).join("\n");
50
50
  return ` [${JSON.stringify(e)}, new Map([\n${n}\n ])],`;
51
- }).join("\n"), m = {};
51
+ }).join("\n"), g = {};
52
52
  for (let t of e.pages) {
53
53
  if (!t.actionPath) continue;
54
54
  let e = await import(t.actionPath), n = [];
55
55
  for (let [t, r] of Object.entries(e)) t !== "default" && typeof r == "function" && n.push(t);
56
- n.length > 0 && (m[t.path] = n);
56
+ n.length > 0 && (g[t.path] = n);
57
57
  }
58
+ let _ = t.router?.enabled !== !1, v = _ && t.js !== "legacy" && c(s(t.root, t.outDir, "_elur", "router.js")) ? "/_elur/router.js" : null;
58
59
  return `// AUTO-GENERATED by @elurjs/kit. Do not edit.
59
- import { handleActionRequest, matchApiRoute, matchRoute, renderPage, renderErrorPage } from "@elurjs/kit";
60
- ${s}
60
+ import { handleActionRequest, matchApiRoute, matchRoute, renderPage, renderPageBody, renderErrorPage, createStreamingResponse } from "@elurjs/kit";
61
+ ${l}
61
62
 
62
63
  const registry = new Map<string, unknown>([
63
64
  ${a.map((e, t) => ` [${JSON.stringify(e)}, m_${t}],`).join("\n")}
64
65
  ]);
65
66
 
66
67
  const pages = [
67
- ${u},
68
+ ${p},
68
69
  ];
69
70
 
70
71
  const apiRoutes = [
71
- ${f}
72
+ ${m}
72
73
  ];
73
74
 
74
75
  const actionModules = new Map<string, Map<string, unknown>>([
75
- ${p}
76
+ ${h}
76
77
  ]);
77
78
 
78
- const actions = ${JSON.stringify(m)};
79
+ const actions = ${JSON.stringify(g)};
79
80
 
80
81
  const routes = {
81
82
  pages,
82
83
  api: apiRoutes,
83
- error404: ${e.error404 ? c(e.error404) : "undefined"},
84
- error500: ${e.error500 ? c(e.error500) : "undefined"},
84
+ error404: ${e.error404 ? d(e.error404) : "undefined"},
85
+ error500: ${e.error500 ? d(e.error500) : "undefined"},
85
86
  };
86
87
 
87
88
  const clientEntry = ${JSON.stringify(t.clientEntry)};
88
89
  const lang = ${JSON.stringify(t.lang)};
90
+ // Client router/JS emission rules baked at adapter build time.
91
+ const router = ${JSON.stringify({
92
+ enabled: _,
93
+ entry: v
94
+ })};
95
+ const jsMode = ${JSON.stringify(t.js === "legacy" ? "legacy" : "modern")};
96
+ // Opt-in streaming SSR (experimental): routes with a loading boundary stream
97
+ // the shell first and swap in the resolved content as a follow-up chunk.
98
+ const streaming = ${t.streaming === !0};
89
99
 
90
100
  function loadModule(path: string) {
91
101
  const mod = registry.get(path);
@@ -134,37 +144,38 @@ export default async function handler(request: Request): Promise<Response> {
134
144
  const search = url.searchParams.get("search") ?? "";
135
145
  const wantsJson = (request.headers.get("Accept") ?? "").includes("application/json");
136
146
  try {
137
- const match = matchRoute(page, routes.pages);
138
- if (!match) throw new Error(\`No route found for \${page}\`);
139
- const { html } = await renderPage({
140
- route: match.route,
141
- params: match.params,
147
+ const result = await renderPageBody({
148
+ routes,
149
+ pathname: page,
142
150
  searchParams: new URLSearchParams(search),
143
- config: { lang, clientEntry },
151
+ config: { lang, clientEntry, router, js: jsMode },
144
152
  importer: loadModule,
145
153
  actions,
146
154
  request,
147
155
  });
148
- const bodyStart = html.indexOf('<div id="app">');
149
- const bodyEnd = html.lastIndexOf("</div>");
150
- const body = bodyStart >= 0 && bodyEnd > bodyStart
151
- ? html.slice(bodyStart + 13, bodyEnd).trim()
152
- : html;
153
- const titleStart = html.indexOf("<title>");
154
- const titleEnd = html.indexOf("</title>");
155
- const title = titleStart >= 0 && titleEnd > titleStart
156
- ? html.slice(titleStart + 7, titleEnd)
157
- : "";
156
+ // A thrown Response from a loader is a first-class response (A-22).
157
+ if (result.response) return result.response;
158
+ const { body, title, head, clearActionErrorCookie, data, actions: actionsPayload } = result;
158
159
  if (wantsJson) {
159
- return new Response(JSON.stringify({ title, body }), {
160
- status: 200,
161
- headers: { "Content-Type": "application/json; charset=utf-8" },
162
- });
160
+ const headers = { "Content-Type": "application/json; charset=utf-8" };
161
+ if (clearActionErrorCookie) headers["X-Elur-Action-Clear-Cookie"] = clearActionErrorCookie;
162
+ // The ?? null fallbacks keep every key present — JSON.stringify drops
163
+ // undefined and the SPA payload shape must be stable across runtimes.
164
+ return new Response(
165
+ JSON.stringify({
166
+ title,
167
+ body,
168
+ head: head ?? null,
169
+ data: data ?? null,
170
+ actions: actionsPayload ?? null,
171
+ clearActionErrorCookie: clearActionErrorCookie ?? null,
172
+ }),
173
+ { status: 200, headers },
174
+ );
163
175
  }
164
- return new Response(body, {
165
- status: 200,
166
- headers: { "Content-Type": "text/html; charset=utf-8" },
167
- });
176
+ const headers = { "Content-Type": "text/html; charset=utf-8" };
177
+ if (clearActionErrorCookie) headers["Set-Cookie"] = clearActionErrorCookie;
178
+ return new Response(body, { status: 200, headers });
168
179
  } catch (err) {
169
180
  if ((err as { name?: string }).name === "RouteNotFoundError") {
170
181
  return new Response("Not Found", {
@@ -195,7 +206,7 @@ export default async function handler(request: Request): Promise<Response> {
195
206
 
196
207
  const match = matchRoute(url.pathname, routes.pages);
197
208
  if (!match) {
198
- const errorResult = await renderErrorPage({ routes, status: 404, config: { lang, clientEntry }, actions, importer: loadModule });
209
+ const errorResult = await renderErrorPage({ routes, status: 404, config: { lang, clientEntry, router, js: jsMode }, actions, importer: loadModule });
199
210
  if (errorResult) {
200
211
  return new Response(errorResult.html, { status: errorResult.status, headers: { "Content-Type": "text/html; charset=utf-8" } });
201
212
  }
@@ -203,19 +214,37 @@ export default async function handler(request: Request): Promise<Response> {
203
214
  }
204
215
 
205
216
  try {
206
- const { html } = await renderPage({
217
+ if (streaming && match.route.loadingPath) {
218
+ // Streaming SSR: shell + loading boundary first, resolved content as a
219
+ // follow-up chunk. Streamed pages are rendered live (no cache).
220
+ return createStreamingResponse({
221
+ route: match.route,
222
+ params: match.params,
223
+ searchParams: new URLSearchParams(url.search),
224
+ config: { lang, clientEntry, router, js: jsMode },
225
+ importer: loadModule,
226
+ actions,
227
+ request,
228
+ signal: request.signal,
229
+ });
230
+ }
231
+ const result = await renderPage({
207
232
  route: match.route,
208
233
  params: match.params,
209
234
  searchParams: new URLSearchParams(url.search),
210
- config: { lang, clientEntry },
235
+ config: { lang, clientEntry, router, js: jsMode },
211
236
  importer: loadModule,
212
237
  actions,
213
238
  request,
214
239
  });
215
- return new Response(html, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8" } });
240
+ // A thrown Response from a loader is a first-class response (A-22).
241
+ if (result.response) return result.response;
242
+ const headers = { "Content-Type": "text/html; charset=utf-8" };
243
+ if (result.clearActionErrorCookie) headers["Set-Cookie"] = result.clearActionErrorCookie;
244
+ return new Response(result.html, { status: 200, headers });
216
245
  } catch (err) {
217
246
  console.error("[elur-kit] SSR render error:", err);
218
- const errorResult = await renderErrorPage({ routes, status: 500, error: err, config: { lang, clientEntry }, actions, importer: loadModule });
247
+ const errorResult = await renderErrorPage({ routes, status: 500, error: err, config: { lang, clientEntry, router, js: jsMode }, actions, importer: loadModule });
219
248
  if (errorResult) {
220
249
  return new Response(errorResult.html, { status: errorResult.status, headers: { "Content-Type": "text/html; charset=utf-8" } });
221
250
  }
@@ -224,13 +253,13 @@ export default async function handler(request: Request): Promise<Response> {
224
253
  }
225
254
  `;
226
255
  }
227
- function d(e, t) {
256
+ function f(e, t) {
228
257
  return o(e, t).split("\\").join("/");
229
258
  }
230
- async function f(e, t, n) {
231
- await r(e, await u(t, n, i(e)), "utf8");
259
+ async function p(e, t, n) {
260
+ await r(e, await d(t, n, i(e)), "utf8");
232
261
  }
233
262
  //#endregion
234
- export { f as n, c as t };
263
+ export { p as n, l as t };
235
264
 
236
- //# sourceMappingURL=shared-DU3iQgr7.js.map
265
+ //# sourceMappingURL=shared-D8w-neq2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared-D8w-neq2.js","names":[],"sources":["../../src/adapters/shared.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { mkdir, readdir, copyFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join, relative, resolve } from \"node:path\";\nimport { scanRoutes } from \"../router/route-scanner.js\";\nimport type { AdapterOptions } from \"./index.js\";\nimport type { PageRoute } from \"../router/route-scanner.js\";\n\n/**\n * Shared helper: copy a directory recursively.\n */\nexport async function copyStatic(from: string, to: string): Promise<void> {\n await mkdir(to, { recursive: true });\n const entries = await readdir(from, { withFileTypes: true });\n for (const entry of entries) {\n const src = join(from, entry.name);\n const dest = join(to, entry.name);\n if (entry.isDirectory()) {\n await copyStatic(src, dest);\n } else {\n await copyFile(src, dest);\n }\n }\n}\n\n/** Adds every module the SSR runtime may import for a page to the registry. */\nfunction collectPageModules(\n page: PageRoute,\n moduleSet: Set<string>,\n actionPathsByPage: Map<string, Set<string>>,\n): void {\n moduleSet.add(page.pagePath);\n if (page.dataPath) moduleSet.add(page.dataPath);\n if (page.loadingPath) moduleSet.add(page.loadingPath);\n for (const layout of page.layouts) {\n moduleSet.add(layout);\n const layoutDataPath = layout.replace(/layout\\.ts$/, \"layout.data.ts\");\n if (layoutDataPath !== layout && existsSync(layoutDataPath)) {\n moduleSet.add(layoutDataPath);\n }\n }\n if (page.actionPath) {\n moduleSet.add(page.actionPath);\n let set = actionPathsByPage.get(page.path);\n if (!set) {\n set = new Set<string>();\n actionPathsByPage.set(page.path, set);\n }\n set.add(page.actionPath);\n }\n}\n\n/**\n * Build a self-contained SSR entry file for a platform adapter.\n * The generated module exports a default `handler(request: Request): Response`\n * and embeds the full route table plus a registry of all page/layout/data/\n * action modules so the runtime never touches the file system.\n */\nexport async function buildSsrEntry(\n routes: Awaited<ReturnType<typeof scanRoutes>>,\n options: AdapterOptions,\n entryDir: string,\n): Promise<string> {\n // Collect all module paths that the SSR runtime may need to import.\n const moduleSet = new Set<string>();\n const actionPathsByPage = new Map<string, Set<string>>();\n for (const page of routes.pages) {\n collectPageModules(page, moduleSet, actionPathsByPage);\n }\n if (routes.error404) collectPageModules(routes.error404, moduleSet, actionPathsByPage);\n if (routes.error500) collectPageModules(routes.error500, moduleSet, actionPathsByPage);\n for (const api of routes.api) {\n moduleSet.add(api.routePath);\n }\n const modules = Array.from(moduleSet);\n const moduleIndex = new Map(modules.map((path, index) => [path, index]));\n\n const imports = modules\n .map((path, index) => {\n const rel = relativeToPosix(entryDir, path);\n return `import * as m_${index} from ${JSON.stringify(rel)};`;\n })\n .join(\"\\n\");\n\n const renderPageRecord = (page: PageRoute): string => `{\n path: ${JSON.stringify(page.path)},\n pagePath: ${JSON.stringify(page.pagePath)},\n dataPath: ${JSON.stringify(page.dataPath ?? null)},\n actionPath: ${JSON.stringify(page.actionPath ?? null)},\n loadingPath: ${JSON.stringify(page.loadingPath ?? null)},\n layouts: ${JSON.stringify(page.layouts)},\n params: ${JSON.stringify(page.params)},\n }`;\n\n const pages = routes.pages.map(renderPageRecord).join(\",\\n\");\n\n const apiRoutes = routes.api\n .map((api) => {\n const index = moduleIndex.get(api.routePath);\n return ` { path: ${JSON.stringify(api.path)}, routePath: m_${index} },`;\n })\n .join(\"\\n\");\n\n const actionModules = Array.from(actionPathsByPage.entries())\n .map(([pagePath, paths]) => {\n const entries = Array.from(paths)\n .map((path) => {\n const index = moduleIndex.get(path);\n return ` [${JSON.stringify(path)}, m_${index}],`;\n })\n .join(\"\\n\");\n return ` [${JSON.stringify(pagePath)}, new Map([\\n${entries}\\n ])],`;\n })\n .join(\"\\n\");\n\n const actionsRegistry: Record<string, string[]> = {};\n for (const page of routes.pages) {\n if (!page.actionPath) continue;\n const mod = (await import(page.actionPath)) as Record<string, unknown>;\n const names: string[] = [];\n for (const [name, value] of Object.entries(mod)) {\n if (name === \"default\") continue;\n if (typeof value === \"function\") {\n names.push(name);\n }\n }\n if (names.length > 0) {\n actionsRegistry[page.path] = names;\n }\n }\n\n // The render config baked into the generated handler. The split router\n // chunk is advertised only when the bundle actually emitted it (the file\n // exists in the build output), so single-input legacy bundles keep working.\n const routerEnabled = options.router?.enabled !== false;\n const routerEntry =\n routerEnabled && options.js !== \"legacy\" &&\n existsSync(resolve(options.root, options.outDir, \"_elur\", \"router.js\"))\n ? \"/_elur/router.js\"\n : null;\n\n return `// AUTO-GENERATED by @elurjs/kit. Do not edit.\nimport { handleActionRequest, matchApiRoute, matchRoute, renderPage, renderPageBody, renderErrorPage, createStreamingResponse } from \"@elurjs/kit\";\n${imports}\n\nconst registry = new Map<string, unknown>([\n${modules.map((path, index) => ` [${JSON.stringify(path)}, m_${index}],`).join(\"\\n\")}\n]);\n\nconst pages = [\n${pages},\n];\n\nconst apiRoutes = [\n${apiRoutes}\n];\n\nconst actionModules = new Map<string, Map<string, unknown>>([\n${actionModules}\n]);\n\nconst actions = ${JSON.stringify(actionsRegistry)};\n\nconst routes = {\n pages,\n api: apiRoutes,\n error404: ${routes.error404 ? renderPageRecord(routes.error404) : \"undefined\"},\n error500: ${routes.error500 ? renderPageRecord(routes.error500) : \"undefined\"},\n};\n\nconst clientEntry = ${JSON.stringify(options.clientEntry)};\nconst lang = ${JSON.stringify(options.lang)};\n// Client router/JS emission rules baked at adapter build time.\nconst router = ${JSON.stringify({ enabled: routerEnabled, entry: routerEntry })};\nconst jsMode = ${JSON.stringify(options.js === \"legacy\" ? \"legacy\" : \"modern\")};\n// Opt-in streaming SSR (experimental): routes with a loading boundary stream\n// the shell first and swap in the resolved content as a follow-up chunk.\nconst streaming = ${options.streaming === true};\n\nfunction loadModule(path: string) {\n const mod = registry.get(path);\n if (mod) return mod;\n throw new Error(\\`Module not found in registry: \\${path}\\`);\n}\n\nasync function resolveAction(name: string, page?: string) {\n // Match concrete page paths (e.g. /movies/inception) to their route pattern\n // (/movies/:slug) so actions on dynamic routes resolve by scope.\n let pageKey: string | undefined;\n if (page) {\n pageKey = routes.pages.some((route) => route.path === page)\n ? page\n : (matchRoute(page, routes.pages)?.route.path ?? page);\n }\n const pageModules = pageKey ? actionModules.get(pageKey) : undefined;\n const candidates = pageModules ? [...pageModules.values()] : [];\n if (!pageModules) {\n for (const mods of actionModules.values()) {\n for (const mod of mods.values()) {\n const action = (mod as Record<string, unknown>)[name];\n if (typeof action === \"function\") return action;\n }\n }\n }\n for (const mod of candidates) {\n const action = (mod as Record<string, unknown>)[name];\n if (typeof action === \"function\") {\n return action as (...args: unknown[]) => unknown;\n }\n }\n return undefined;\n}\n\nexport default async function handler(request: Request): Promise<Response> {\n const url = new URL(request.url);\n\n if (url.pathname === \"/__elur-js/actions\") {\n return handleActionRequest(request, resolveAction);\n }\n\n // Render endpoint used by the SPA router and streaming boundaries.\n if (url.pathname === \"/__elur-js/render\") {\n const page = url.searchParams.get(\"page\") ?? \"/\";\n const search = url.searchParams.get(\"search\") ?? \"\";\n const wantsJson = (request.headers.get(\"Accept\") ?? \"\").includes(\"application/json\");\n try {\n const result = await renderPageBody({\n routes,\n pathname: page,\n searchParams: new URLSearchParams(search),\n config: { lang, clientEntry, router, js: jsMode },\n importer: loadModule,\n actions,\n request,\n });\n // A thrown Response from a loader is a first-class response (A-22).\n if (result.response) return result.response;\n const { body, title, head, clearActionErrorCookie, data, actions: actionsPayload } = result;\n if (wantsJson) {\n const headers = { \"Content-Type\": \"application/json; charset=utf-8\" };\n if (clearActionErrorCookie) headers[\"X-Elur-Action-Clear-Cookie\"] = clearActionErrorCookie;\n // The ?? null fallbacks keep every key present — JSON.stringify drops\n // undefined and the SPA payload shape must be stable across runtimes.\n return new Response(\n JSON.stringify({\n title,\n body,\n head: head ?? null,\n data: data ?? null,\n actions: actionsPayload ?? null,\n clearActionErrorCookie: clearActionErrorCookie ?? null,\n }),\n { status: 200, headers },\n );\n }\n const headers = { \"Content-Type\": \"text/html; charset=utf-8\" };\n if (clearActionErrorCookie) headers[\"Set-Cookie\"] = clearActionErrorCookie;\n return new Response(body, { status: 200, headers });\n } catch (err) {\n if ((err as { name?: string }).name === \"RouteNotFoundError\") {\n return new Response(\"Not Found\", {\n status: 404,\n headers: { \"Content-Type\": \"text/plain\" },\n });\n }\n console.error(\"[elur-kit] render endpoint error:\", err);\n return new Response(\"Internal Server Error\", {\n status: 500,\n headers: { \"Content-Type\": \"text/plain\" },\n });\n }\n }\n\n const apiMatch = matchApiRoute(url.pathname, apiRoutes);\n if (apiMatch) {\n const mod = apiMatch.route.routePath as Record<\n string,\n (request: Request, context?: { params: Record<string, string | string[]> }) => unknown\n >;\n const handler = mod[request.method ?? \"GET\"];\n if (typeof handler !== \"function\") {\n return new Response(\"Method not allowed: \" + request.method, { status: 405, headers: { \"Content-Type\": \"text/plain\" } });\n }\n return (await handler(request, { params: apiMatch.params })) as Response;\n }\n\n const match = matchRoute(url.pathname, routes.pages);\n if (!match) {\n const errorResult = await renderErrorPage({ routes, status: 404, config: { lang, clientEntry, router, js: jsMode }, actions, importer: loadModule });\n if (errorResult) {\n return new Response(errorResult.html, { status: errorResult.status, headers: { \"Content-Type\": \"text/html; charset=utf-8\" } });\n }\n return new Response(\"Not Found\", { status: 404, headers: { \"Content-Type\": \"text/plain\" } });\n }\n\n try {\n if (streaming && match.route.loadingPath) {\n // Streaming SSR: shell + loading boundary first, resolved content as a\n // follow-up chunk. Streamed pages are rendered live (no cache).\n return createStreamingResponse({\n route: match.route,\n params: match.params,\n searchParams: new URLSearchParams(url.search),\n config: { lang, clientEntry, router, js: jsMode },\n importer: loadModule,\n actions,\n request,\n signal: request.signal,\n });\n }\n const result = await renderPage({\n route: match.route,\n params: match.params,\n searchParams: new URLSearchParams(url.search),\n config: { lang, clientEntry, router, js: jsMode },\n importer: loadModule,\n actions,\n request,\n });\n // A thrown Response from a loader is a first-class response (A-22).\n if (result.response) return result.response;\n const headers = { \"Content-Type\": \"text/html; charset=utf-8\" };\n if (result.clearActionErrorCookie) headers[\"Set-Cookie\"] = result.clearActionErrorCookie;\n return new Response(result.html, { status: 200, headers });\n } catch (err) {\n console.error(\"[elur-kit] SSR render error:\", err);\n const errorResult = await renderErrorPage({ routes, status: 500, error: err, config: { lang, clientEntry, router, js: jsMode }, actions, importer: loadModule });\n if (errorResult) {\n return new Response(errorResult.html, { status: errorResult.status, headers: { \"Content-Type\": \"text/html; charset=utf-8\" } });\n }\n return new Response(\"Internal Server Error\", { status: 500, headers: { \"Content-Type\": \"text/plain; charset=utf-8\" } });\n }\n}\n`;\n}\n\nfunction relativeToPosix(from: string, to: string): string {\n return relative(from, to).split(\"\\\\\").join(\"/\");\n}\n\n/**\n * Write a generated SSR entry file for an adapter.\n */\nexport async function writeSsrEntry(\n entryPath: string,\n routes: Awaited<ReturnType<typeof scanRoutes>>,\n options: AdapterOptions,\n): Promise<void> {\n await writeFile(\n entryPath,\n await buildSsrEntry(routes, options, dirname(entryPath)),\n \"utf8\",\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ import { mkdir as e, writeFile as t } from "node:fs/promises";
2
+ import { dirname as n, join as r } from "node:path";
3
+ //#region src/seo/index.ts
4
+ async function i(i) {
5
+ let { siteUrl: a, urls: o, outDir: c } = i, l = a.replace(/\/$/, ""), u = `<?xml version="1.0" encoding="UTF-8"?>
6
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
7
+ ${o.map((e) => {
8
+ let t = typeof e == "string" ? { url: e } : e, n = [" <url>", ` <loc>${s(`${l}${t.url.startsWith("/") ? "" : "/"}${t.url}`)}</loc>`];
9
+ return t.lastmod && n.push(` <lastmod>${t.lastmod}</lastmod>`), t.changefreq && n.push(` <changefreq>${t.changefreq}</changefreq>`), t.priority !== void 0 && n.push(` <priority>${t.priority.toFixed(1)}</priority>`), n.push(" </url>"), n.join("\n");
10
+ }).join("\n")}
11
+ </urlset>
12
+ `, d = r(c, "sitemap.xml");
13
+ return await e(n(d), { recursive: !0 }), await t(d, u, "utf8"), d;
14
+ }
15
+ async function a(i) {
16
+ let { siteUrl: a, outDir: o, rules: s, disallow: c, sitemapUrl: l } = i, u = a.replace(/\/$/, ""), d = [];
17
+ if (s && s.length > 0) for (let e of s) {
18
+ if (d.push(`User-agent: ${e.userAgent}`), e.allow) for (let t of e.allow) d.push(`Allow: ${t}`);
19
+ if (e.disallow) for (let t of e.disallow) d.push(`Disallow: ${t}`);
20
+ e.crawlDelay !== void 0 && d.push(`Crawl-delay: ${e.crawlDelay}`), d.push("");
21
+ }
22
+ else {
23
+ if (d.push("User-agent: *"), c && c.length > 0) for (let e of c) d.push(`Disallow: ${e}`);
24
+ else d.push("Disallow:");
25
+ d.push("");
26
+ }
27
+ let f = l ?? `${u}/sitemap.xml`;
28
+ d.push(`Sitemap: ${f}`);
29
+ let p = d.join("\n") + "\n", m = r(o, "robots.txt");
30
+ return await e(n(m), { recursive: !0 }), await t(m, p, "utf8"), m;
31
+ }
32
+ function o(e) {
33
+ return `<script type="application/ld+json">${JSON.stringify(e).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}<\/script>`;
34
+ }
35
+ function s(e) {
36
+ return e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
37
+ }
38
+ //#endregion
39
+ //#region src/seo/sitemap-from-routes.ts
40
+ async function c(e) {
41
+ let { siteUrl: t, outDir: n, routes: r, extraUrls: a = [], maxUrlsPerSitemap: o = 5e4 } = e, s = [];
42
+ for (let t of r.pages) t.params.length > 0 || t.path !== "/404" && t.path !== "/500" && (t.path.startsWith("/_elur") || t.path.startsWith("/__elur-js") || s.push({
43
+ url: t.path,
44
+ changefreq: e.defaultChangefreq,
45
+ priority: e.defaultPriority
46
+ }));
47
+ let c = [...s, ...a];
48
+ return c.length <= o ? [await i({
49
+ siteUrl: t,
50
+ outDir: n,
51
+ urls: c
52
+ })] : l({
53
+ siteUrl: t,
54
+ outDir: n,
55
+ urls: c,
56
+ maxUrlsPerSitemap: o
57
+ });
58
+ }
59
+ async function l(e) {
60
+ let { siteUrl: t, outDir: n, urls: r, maxUrlsPerSitemap: i } = e, a = t.replace(/\/$/, ""), o = [], s = [];
61
+ for (let e = 0; e < r.length; e += i) {
62
+ let t = r.slice(e, e + i), c = `sitemap-${Math.floor(e / i) + 1}.xml`, { writeFile: l, mkdir: d } = await import("node:fs/promises"), { join: f, dirname: p } = await import("node:path"), m = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${t.map((e) => {
63
+ let t = typeof e == "string" ? { url: e } : e, n = [" <url>", ` <loc>${u(`${a}${t.url.startsWith("/") ? "" : "/"}${t.url}`)}</loc>`];
64
+ return t.lastmod && n.push(` <lastmod>${t.lastmod}</lastmod>`), t.changefreq && n.push(` <changefreq>${t.changefreq}</changefreq>`), t.priority !== void 0 && n.push(` <priority>${t.priority.toFixed(1)}</priority>`), n.push(" </url>"), n.join("\n");
65
+ }).join("\n")}\n</urlset>\n`, h = f(n, c);
66
+ await d(p(h), { recursive: !0 }), await l(h, m, "utf8"), o.push(h), s.push(`${a}/${c}`);
67
+ }
68
+ let { writeFile: c, mkdir: l } = await import("node:fs/promises"), { join: d, dirname: f } = await import("node:path"), p = `<?xml version="1.0" encoding="UTF-8"?>\n<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${s.map((e) => ` <sitemap>\n <loc>${u(e)}</loc>\n </sitemap>`).join("\n")}\n</sitemapindex>\n`, m = d(n, "sitemap.xml");
69
+ return await l(f(m), { recursive: !0 }), await c(m, p, "utf8"), o.push(m), o;
70
+ }
71
+ function u(e) {
72
+ return e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
73
+ }
74
+ //#endregion
75
+ export { o as i, a as n, i as r, c as t };
76
+
77
+ //# sourceMappingURL=sitemap-from-routes-CaBoiGvE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap-from-routes-CaBoiGvE.js","names":[],"sources":["../../src/seo/index.ts","../../src/seo/sitemap-from-routes.ts"],"sourcesContent":["/**\n * SEO utilities — sitemap.xml and robots.txt generation.\n *\n * @module\n */\n\nimport { writeFile, mkdir } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\n\n// Sitemap generation from the scanned route manifest (wired into `build`).\nexport { generateSitemapFromRoutes, type SitemapFromRoutesOptions } from \"./sitemap-from-routes.js\";\n\n// Types\n\nexport interface SitemapEntry {\n /** URL path, e.g. \"/docs/getting-started/introduction\". */\n url: string;\n /** Last modification date (ISO 8601 or YYYY-MM-DD). */\n lastmod?: string;\n /** Change frequency: always, hourly, daily, weekly, monthly, yearly, never. */\n changefreq?: \"always\" | \"hourly\" | \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"never\";\n /** Priority 0.0–1.0. */\n priority?: number;\n}\n\nexport interface SitemapConfig {\n /** Base URL of the site, e.g. \"https://example.com\". */\n siteUrl: string;\n /** List of URL entries to include in the sitemap. */\n urls: (SitemapEntry | string)[];\n /** Output directory where sitemap.xml will be written. */\n outDir: string;\n}\n\nexport interface RobotsConfig {\n /** Base URL of the site, e.g. \"https://example.com\". */\n siteUrl: string;\n /** Output directory where robots.txt will be written. */\n outDir: string;\n /** Rules for specific user agents. */\n rules?: RobotsRule[];\n /** Paths to disallow for all crawlers (shorthand for rules). */\n disallow?: string[];\n /** Sitemap URL override. If not set, defaults to `${siteUrl}/sitemap.xml`. */\n sitemapUrl?: string;\n}\n\nexport interface RobotsRule {\n /** User-agent, e.g. \"Googlebot\" or \"*\" for all. */\n userAgent: string;\n /** Paths to disallow. */\n disallow?: string[];\n /** Paths to allow. */\n allow?: string[];\n /** Crawl delay in seconds. */\n crawlDelay?: number;\n}\n\n// Sitemap generation\n\n/**\n * Generates a `sitemap.xml` file from a list of URLs.\n *\n * @example\n * ```ts\n * import { generateSitemap } from \"@elurjs/kit/seo\";\n *\n * await generateSitemap({\n * siteUrl: \"https://elur-kit.dev\",\n * outDir: \"./dist\",\n * urls: [\n * \"/\",\n * \"/docs/introduction\",\n * { url: \"/docs/routing\", changefreq: \"weekly\", priority: 0.8 },\n * ],\n * });\n * ```\n */\nexport async function generateSitemap(config: SitemapConfig): Promise<string> {\n const { siteUrl, urls, outDir } = config;\n const base = siteUrl.replace(/\\/$/, \"\");\n\n const entries: string[] = urls.map((entry) => {\n const e = typeof entry === \"string\" ? { url: entry } : entry;\n const loc = `${base}${e.url.startsWith(\"/\") ? \"\" : \"/\"}${e.url}`;\n const lines = [` <url>`, ` <loc>${escapeXml(loc)}</loc>`];\n if (e.lastmod) lines.push(` <lastmod>${e.lastmod}</lastmod>`);\n if (e.changefreq) lines.push(` <changefreq>${e.changefreq}</changefreq>`);\n if (e.priority !== undefined) lines.push(` <priority>${e.priority.toFixed(1)}</priority>`);\n lines.push(` </url>`);\n return lines.join(\"\\n\");\n });\n\n const xml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${entries.join(\"\\n\")}\n</urlset>\n`;\n\n const filePath = join(outDir, \"sitemap.xml\");\n await mkdir(dirname(filePath), { recursive: true });\n await writeFile(filePath, xml, \"utf8\");\n return filePath;\n}\n\n// Robots.txt generation\n\n/**\n * Generates a `robots.txt` file.\n *\n * @example\n * ```ts\n * import { generateRobots } from \"@elurjs/kit/seo\";\n *\n * await generateRobots({\n * siteUrl: \"https://elur-kit.dev\",\n * outDir: \"./dist\",\n * disallow: [\"/api/\", \"/_elur/\"],\n * });\n * ```\n */\nexport async function generateRobots(config: RobotsConfig): Promise<string> {\n const { siteUrl, outDir, rules, disallow, sitemapUrl } = config;\n const base = siteUrl.replace(/\\/$/, \"\");\n const lines: string[] = [];\n\n if (rules && rules.length > 0) {\n for (const rule of rules) {\n lines.push(`User-agent: ${rule.userAgent}`);\n if (rule.allow) {\n for (const path of rule.allow) lines.push(`Allow: ${path}`);\n }\n if (rule.disallow) {\n for (const path of rule.disallow) lines.push(`Disallow: ${path}`);\n }\n if (rule.crawlDelay !== undefined) {\n lines.push(`Crawl-delay: ${rule.crawlDelay}`);\n }\n lines.push(\"\");\n }\n } else {\n lines.push(\"User-agent: *\");\n if (disallow && disallow.length > 0) {\n for (const path of disallow) lines.push(`Disallow: ${path}`);\n } else {\n lines.push(\"Disallow:\");\n }\n lines.push(\"\");\n }\n\n const sitemap = sitemapUrl ?? `${base}/sitemap.xml`;\n lines.push(`Sitemap: ${sitemap}`);\n\n const content = lines.join(\"\\n\") + \"\\n\";\n const filePath = join(outDir, \"robots.txt\");\n await mkdir(dirname(filePath), { recursive: true });\n await writeFile(filePath, content, \"utf8\");\n return filePath;\n}\n\n// JSON-LD / Structured data\n\nexport interface JsonLdSchema {\n [key: string]: unknown;\n}\n\n/**\n * Serializes a JSON-LD structured data object into a `<script type=\"application/ld+json\">` tag.\n *\n * @example\n * ```ts\n * import { jsonLd } from \"@elurjs/kit/seo\";\n *\n * const schema = jsonLd({\n * \"@context\": \"https://schema.org\",\n * \"@type\": \"TechArticle\",\n * headline: \"Routing\",\n * author: { \"@type\": \"Person\", name: \"Deiver Vasquez\" },\n * });\n * // Returns: <script type=\"application/ld+json\">{...}</script>\n * ```\n */\nexport function jsonLd(schema: JsonLdSchema | JsonLdSchema[]): string {\n const data = JSON.stringify(Array.isArray(schema) ? schema : schema);\n // Escape sequences that could close the <script> tag or introduce markup.\n // Per the HTML spec, inside a <script> block the only dangerous sequence\n // is \"</script\" (case-insensitive). We also escape \"<\" more broadly to\n // prevent any interpreter from seeing markup-like content, and escape\n // \"<!--\" to prevent HTML comment-based escapes.\n const safe = data\n .replace(/</g, \"\\\\u003c\")\n .replace(/>/g, \"\\\\u003e\")\n .replace(/&/g, \"\\\\u0026\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n return `<script type=\"application/ld+json\">${safe}</script>`;\n}\n\n// Helpers\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n","// --- Sitemap generation from route manifest (plan §11.3) ---\n//\n// Generates a sitemap.xml from the scanned routes, excluding:\n// - API routes\n// - Dynamic routes (they require data to generate URLs)\n// - Error pages (404, 500)\n// - Routes with noindex metadata\n//\n// For dynamic routes, the author should provide a `generateSitemapUrls()`\n// function in their page.data.ts that returns concrete URLs.\n//\n// Supports large sitemaps via sitemap index files (split at 50,000 URLs).\n\nimport type { ScannedRoutes } from \"../router/route-scanner.js\";\nimport { generateSitemap, type SitemapEntry } from \"./index.js\";\n\nexport interface SitemapFromRoutesOptions {\n siteUrl: string;\n outDir: string;\n routes: ScannedRoutes;\n /** Additional URLs to include (e.g. from dynamic routes). */\n extraUrls?: (SitemapEntry | string)[];\n /** Max URLs per sitemap file. Default: 50000. */\n maxUrlsPerSitemap?: number;\n /** Default changefreq for routes. */\n defaultChangefreq?: SitemapEntry[\"changefreq\"];\n /** Default priority for routes. */\n defaultPriority?: number;\n}\n\n/**\n * Generates a sitemap.xml from the route manifest.\n *\n * Static routes are included automatically. Dynamic routes require the author\n * to provide URLs via `extraUrls` or a `generateSitemapUrls()` export.\n *\n * For large sites (>50,000 URLs), a sitemap index is generated.\n */\nexport async function generateSitemapFromRoutes(\n options: SitemapFromRoutesOptions,\n): Promise<string[]> {\n const { siteUrl, outDir, routes, extraUrls = [], maxUrlsPerSitemap = 50000 } = options;\n\n // Collect static route URLs.\n const routeUrls: SitemapEntry[] = [];\n for (const page of routes.pages) {\n // Skip dynamic routes (they have params).\n if (page.params.length > 0) continue;\n // Skip error pages.\n if (page.path === \"/404\" || page.path === \"/500\") continue;\n // Skip internal namespaces.\n if (page.path.startsWith(\"/_elur\") || page.path.startsWith(\"/__elur-js\")) continue;\n\n routeUrls.push({\n url: page.path,\n changefreq: options.defaultChangefreq,\n priority: options.defaultPriority,\n });\n }\n\n // Merge with extra URLs.\n const allUrls = [...routeUrls, ...extraUrls];\n\n // If under the limit, generate a single sitemap.\n if (allUrls.length <= maxUrlsPerSitemap) {\n const path = await generateSitemap({ siteUrl, outDir, urls: allUrls });\n return [path];\n }\n\n // For large sitemaps, split into multiple files with an index.\n return generateSitemapIndex({ siteUrl, outDir, urls: allUrls, maxUrlsPerSitemap });\n}\n\n/**\n * Generates a sitemap index file that references multiple sitemap files.\n * Used for large sites (>50,000 URLs).\n */\nasync function generateSitemapIndex(\n options: { siteUrl: string; outDir: string; urls: (SitemapEntry | string)[]; maxUrlsPerSitemap: number },\n): Promise<string[]> {\n const { siteUrl, outDir, urls, maxUrlsPerSitemap } = options;\n const base = siteUrl.replace(/\\/$/, \"\");\n const files: string[] = [];\n const sitemapUrls: string[] = [];\n\n // Split URLs into chunks.\n for (let i = 0; i < urls.length; i += maxUrlsPerSitemap) {\n const chunk = urls.slice(i, i + maxUrlsPerSitemap);\n const filename = `sitemap-${Math.floor(i / maxUrlsPerSitemap) + 1}.xml`;\n const { writeFile, mkdir } = await import(\"node:fs/promises\");\n const { join, dirname } = await import(\"node:path\");\n\n // Generate the chunk sitemap.\n const entries = chunk.map((entry) => {\n const e = typeof entry === \"string\" ? { url: entry } : entry;\n const loc = `${base}${e.url.startsWith(\"/\") ? \"\" : \"/\"}${e.url}`;\n const lines = [` <url>`, ` <loc>${escapeXml(loc)}</loc>`];\n if (e.lastmod) lines.push(` <lastmod>${e.lastmod}</lastmod>`);\n if (e.changefreq) lines.push(` <changefreq>${e.changefreq}</changefreq>`);\n if (e.priority !== undefined) lines.push(` <priority>${e.priority.toFixed(1)}</priority>`);\n lines.push(` </url>`);\n return lines.join(\"\\n\");\n });\n\n const xml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n${entries.join(\"\\n\")}\\n</urlset>\\n`;\n const filePath = join(outDir, filename);\n await mkdir(dirname(filePath), { recursive: true });\n await writeFile(filePath, xml, \"utf8\");\n files.push(filePath);\n sitemapUrls.push(`${base}/${filename}`);\n }\n\n // Generate the index file.\n const { writeFile, mkdir } = await import(\"node:fs/promises\");\n const { join, dirname } = await import(\"node:path\");\n\n const indexEntries = sitemapUrls.map((url) => ` <sitemap>\\n <loc>${escapeXml(url)}</loc>\\n </sitemap>`).join(\"\\n\");\n const indexXml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n${indexEntries}\\n</sitemapindex>\\n`;\n const indexPath = join(outDir, \"sitemap.xml\");\n await mkdir(dirname(indexPath), { recursive: true });\n await writeFile(indexPath, indexXml, \"utf8\");\n files.push(indexPath);\n\n return files;\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n"],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ let e=require("node:fs/promises"),t=require("node:path");async function n(n){let{siteUrl:r,urls:i,outDir:o}=n,s=r.replace(/\/$/,``),c=`<?xml version="1.0" encoding="UTF-8"?>
2
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
+ ${i.map(e=>{let t=typeof e==`string`?{url:e}:e,n=[` <url>`,` <loc>${a(`${s}${t.url.startsWith(`/`)?``:`/`}${t.url}`)}</loc>`];return t.lastmod&&n.push(` <lastmod>${t.lastmod}</lastmod>`),t.changefreq&&n.push(` <changefreq>${t.changefreq}</changefreq>`),t.priority!==void 0&&n.push(` <priority>${t.priority.toFixed(1)}</priority>`),n.push(` </url>`),n.join(`
4
+ `)}).join(`
5
+ `)}
6
+ </urlset>
7
+ `,l=(0,t.join)(o,`sitemap.xml`);return await(0,e.mkdir)((0,t.dirname)(l),{recursive:!0}),await(0,e.writeFile)(l,c,`utf8`),l}async function r(n){let{siteUrl:r,outDir:i,rules:a,disallow:o,sitemapUrl:s}=n,c=r.replace(/\/$/,``),l=[];if(a&&a.length>0)for(let e of a){if(l.push(`User-agent: ${e.userAgent}`),e.allow)for(let t of e.allow)l.push(`Allow: ${t}`);if(e.disallow)for(let t of e.disallow)l.push(`Disallow: ${t}`);e.crawlDelay!==void 0&&l.push(`Crawl-delay: ${e.crawlDelay}`),l.push(``)}else{if(l.push(`User-agent: *`),o&&o.length>0)for(let e of o)l.push(`Disallow: ${e}`);else l.push(`Disallow:`);l.push(``)}let u=s??`${c}/sitemap.xml`;l.push(`Sitemap: ${u}`);let d=l.join(`
8
+ `)+`
9
+ `,f=(0,t.join)(i,`robots.txt`);return await(0,e.mkdir)((0,t.dirname)(f),{recursive:!0}),await(0,e.writeFile)(f,d,`utf8`),f}function i(e){return`<script type="application/ld+json">${JSON.stringify(e).replace(/</g,`\\u003c`).replace(/>/g,`\\u003e`).replace(/&/g,`\\u0026`).replace(/\u2028/g,`\\u2028`).replace(/\u2029/g,`\\u2029`)}<\/script>`}function a(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&apos;`)}async function o(e){let{siteUrl:t,outDir:r,routes:i,extraUrls:a=[],maxUrlsPerSitemap:o=5e4}=e,c=[];for(let t of i.pages)t.params.length>0||t.path!==`/404`&&t.path!==`/500`&&(t.path.startsWith(`/_elur`)||t.path.startsWith(`/__elur-js`)||c.push({url:t.path,changefreq:e.defaultChangefreq,priority:e.defaultPriority}));let l=[...c,...a];return l.length<=o?[await n({siteUrl:t,outDir:r,urls:l})]:s({siteUrl:t,outDir:r,urls:l,maxUrlsPerSitemap:o})}async function s(e){let{siteUrl:t,outDir:n,urls:r,maxUrlsPerSitemap:i}=e,a=t.replace(/\/$/,``),o=[],s=[];for(let e=0;e<r.length;e+=i){let t=r.slice(e,e+i),l=`sitemap-${Math.floor(e/i)+1}.xml`,{writeFile:u,mkdir:d}=await import(`node:fs/promises`),{join:f,dirname:p}=await import(`node:path`),m=`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${t.map(e=>{let t=typeof e==`string`?{url:e}:e,n=[` <url>`,` <loc>${c(`${a}${t.url.startsWith(`/`)?``:`/`}${t.url}`)}</loc>`];return t.lastmod&&n.push(` <lastmod>${t.lastmod}</lastmod>`),t.changefreq&&n.push(` <changefreq>${t.changefreq}</changefreq>`),t.priority!==void 0&&n.push(` <priority>${t.priority.toFixed(1)}</priority>`),n.push(` </url>`),n.join(`
10
+ `)}).join(`
11
+ `)}\n</urlset>\n`,h=f(n,l);await d(p(h),{recursive:!0}),await u(h,m,`utf8`),o.push(h),s.push(`${a}/${l}`)}let{writeFile:l,mkdir:u}=await import(`node:fs/promises`),{join:d,dirname:f}=await import(`node:path`),p=`<?xml version="1.0" encoding="UTF-8"?>\n<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${s.map(e=>` <sitemap>\n <loc>${c(e)}</loc>\n </sitemap>`).join(`
12
+ `)}\n</sitemapindex>\n`,m=d(n,`sitemap.xml`);return await u(f(m),{recursive:!0}),await l(m,p,`utf8`),o.push(m),o}function c(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&apos;`)}Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return n}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return o}});
13
+ //# sourceMappingURL=sitemap-from-routes-DbcLLL4G.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap-from-routes-DbcLLL4G.cjs","names":[],"sources":["../../src/seo/index.ts","../../src/seo/sitemap-from-routes.ts"],"sourcesContent":["/**\n * SEO utilities — sitemap.xml and robots.txt generation.\n *\n * @module\n */\n\nimport { writeFile, mkdir } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\n\n// Sitemap generation from the scanned route manifest (wired into `build`).\nexport { generateSitemapFromRoutes, type SitemapFromRoutesOptions } from \"./sitemap-from-routes.js\";\n\n// Types\n\nexport interface SitemapEntry {\n /** URL path, e.g. \"/docs/getting-started/introduction\". */\n url: string;\n /** Last modification date (ISO 8601 or YYYY-MM-DD). */\n lastmod?: string;\n /** Change frequency: always, hourly, daily, weekly, monthly, yearly, never. */\n changefreq?: \"always\" | \"hourly\" | \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"never\";\n /** Priority 0.0–1.0. */\n priority?: number;\n}\n\nexport interface SitemapConfig {\n /** Base URL of the site, e.g. \"https://example.com\". */\n siteUrl: string;\n /** List of URL entries to include in the sitemap. */\n urls: (SitemapEntry | string)[];\n /** Output directory where sitemap.xml will be written. */\n outDir: string;\n}\n\nexport interface RobotsConfig {\n /** Base URL of the site, e.g. \"https://example.com\". */\n siteUrl: string;\n /** Output directory where robots.txt will be written. */\n outDir: string;\n /** Rules for specific user agents. */\n rules?: RobotsRule[];\n /** Paths to disallow for all crawlers (shorthand for rules). */\n disallow?: string[];\n /** Sitemap URL override. If not set, defaults to `${siteUrl}/sitemap.xml`. */\n sitemapUrl?: string;\n}\n\nexport interface RobotsRule {\n /** User-agent, e.g. \"Googlebot\" or \"*\" for all. */\n userAgent: string;\n /** Paths to disallow. */\n disallow?: string[];\n /** Paths to allow. */\n allow?: string[];\n /** Crawl delay in seconds. */\n crawlDelay?: number;\n}\n\n// Sitemap generation\n\n/**\n * Generates a `sitemap.xml` file from a list of URLs.\n *\n * @example\n * ```ts\n * import { generateSitemap } from \"@elurjs/kit/seo\";\n *\n * await generateSitemap({\n * siteUrl: \"https://elur-kit.dev\",\n * outDir: \"./dist\",\n * urls: [\n * \"/\",\n * \"/docs/introduction\",\n * { url: \"/docs/routing\", changefreq: \"weekly\", priority: 0.8 },\n * ],\n * });\n * ```\n */\nexport async function generateSitemap(config: SitemapConfig): Promise<string> {\n const { siteUrl, urls, outDir } = config;\n const base = siteUrl.replace(/\\/$/, \"\");\n\n const entries: string[] = urls.map((entry) => {\n const e = typeof entry === \"string\" ? { url: entry } : entry;\n const loc = `${base}${e.url.startsWith(\"/\") ? \"\" : \"/\"}${e.url}`;\n const lines = [` <url>`, ` <loc>${escapeXml(loc)}</loc>`];\n if (e.lastmod) lines.push(` <lastmod>${e.lastmod}</lastmod>`);\n if (e.changefreq) lines.push(` <changefreq>${e.changefreq}</changefreq>`);\n if (e.priority !== undefined) lines.push(` <priority>${e.priority.toFixed(1)}</priority>`);\n lines.push(` </url>`);\n return lines.join(\"\\n\");\n });\n\n const xml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${entries.join(\"\\n\")}\n</urlset>\n`;\n\n const filePath = join(outDir, \"sitemap.xml\");\n await mkdir(dirname(filePath), { recursive: true });\n await writeFile(filePath, xml, \"utf8\");\n return filePath;\n}\n\n// Robots.txt generation\n\n/**\n * Generates a `robots.txt` file.\n *\n * @example\n * ```ts\n * import { generateRobots } from \"@elurjs/kit/seo\";\n *\n * await generateRobots({\n * siteUrl: \"https://elur-kit.dev\",\n * outDir: \"./dist\",\n * disallow: [\"/api/\", \"/_elur/\"],\n * });\n * ```\n */\nexport async function generateRobots(config: RobotsConfig): Promise<string> {\n const { siteUrl, outDir, rules, disallow, sitemapUrl } = config;\n const base = siteUrl.replace(/\\/$/, \"\");\n const lines: string[] = [];\n\n if (rules && rules.length > 0) {\n for (const rule of rules) {\n lines.push(`User-agent: ${rule.userAgent}`);\n if (rule.allow) {\n for (const path of rule.allow) lines.push(`Allow: ${path}`);\n }\n if (rule.disallow) {\n for (const path of rule.disallow) lines.push(`Disallow: ${path}`);\n }\n if (rule.crawlDelay !== undefined) {\n lines.push(`Crawl-delay: ${rule.crawlDelay}`);\n }\n lines.push(\"\");\n }\n } else {\n lines.push(\"User-agent: *\");\n if (disallow && disallow.length > 0) {\n for (const path of disallow) lines.push(`Disallow: ${path}`);\n } else {\n lines.push(\"Disallow:\");\n }\n lines.push(\"\");\n }\n\n const sitemap = sitemapUrl ?? `${base}/sitemap.xml`;\n lines.push(`Sitemap: ${sitemap}`);\n\n const content = lines.join(\"\\n\") + \"\\n\";\n const filePath = join(outDir, \"robots.txt\");\n await mkdir(dirname(filePath), { recursive: true });\n await writeFile(filePath, content, \"utf8\");\n return filePath;\n}\n\n// JSON-LD / Structured data\n\nexport interface JsonLdSchema {\n [key: string]: unknown;\n}\n\n/**\n * Serializes a JSON-LD structured data object into a `<script type=\"application/ld+json\">` tag.\n *\n * @example\n * ```ts\n * import { jsonLd } from \"@elurjs/kit/seo\";\n *\n * const schema = jsonLd({\n * \"@context\": \"https://schema.org\",\n * \"@type\": \"TechArticle\",\n * headline: \"Routing\",\n * author: { \"@type\": \"Person\", name: \"Deiver Vasquez\" },\n * });\n * // Returns: <script type=\"application/ld+json\">{...}</script>\n * ```\n */\nexport function jsonLd(schema: JsonLdSchema | JsonLdSchema[]): string {\n const data = JSON.stringify(Array.isArray(schema) ? schema : schema);\n // Escape sequences that could close the <script> tag or introduce markup.\n // Per the HTML spec, inside a <script> block the only dangerous sequence\n // is \"</script\" (case-insensitive). We also escape \"<\" more broadly to\n // prevent any interpreter from seeing markup-like content, and escape\n // \"<!--\" to prevent HTML comment-based escapes.\n const safe = data\n .replace(/</g, \"\\\\u003c\")\n .replace(/>/g, \"\\\\u003e\")\n .replace(/&/g, \"\\\\u0026\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n return `<script type=\"application/ld+json\">${safe}</script>`;\n}\n\n// Helpers\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n","// --- Sitemap generation from route manifest (plan §11.3) ---\n//\n// Generates a sitemap.xml from the scanned routes, excluding:\n// - API routes\n// - Dynamic routes (they require data to generate URLs)\n// - Error pages (404, 500)\n// - Routes with noindex metadata\n//\n// For dynamic routes, the author should provide a `generateSitemapUrls()`\n// function in their page.data.ts that returns concrete URLs.\n//\n// Supports large sitemaps via sitemap index files (split at 50,000 URLs).\n\nimport type { ScannedRoutes } from \"../router/route-scanner.js\";\nimport { generateSitemap, type SitemapEntry } from \"./index.js\";\n\nexport interface SitemapFromRoutesOptions {\n siteUrl: string;\n outDir: string;\n routes: ScannedRoutes;\n /** Additional URLs to include (e.g. from dynamic routes). */\n extraUrls?: (SitemapEntry | string)[];\n /** Max URLs per sitemap file. Default: 50000. */\n maxUrlsPerSitemap?: number;\n /** Default changefreq for routes. */\n defaultChangefreq?: SitemapEntry[\"changefreq\"];\n /** Default priority for routes. */\n defaultPriority?: number;\n}\n\n/**\n * Generates a sitemap.xml from the route manifest.\n *\n * Static routes are included automatically. Dynamic routes require the author\n * to provide URLs via `extraUrls` or a `generateSitemapUrls()` export.\n *\n * For large sites (>50,000 URLs), a sitemap index is generated.\n */\nexport async function generateSitemapFromRoutes(\n options: SitemapFromRoutesOptions,\n): Promise<string[]> {\n const { siteUrl, outDir, routes, extraUrls = [], maxUrlsPerSitemap = 50000 } = options;\n\n // Collect static route URLs.\n const routeUrls: SitemapEntry[] = [];\n for (const page of routes.pages) {\n // Skip dynamic routes (they have params).\n if (page.params.length > 0) continue;\n // Skip error pages.\n if (page.path === \"/404\" || page.path === \"/500\") continue;\n // Skip internal namespaces.\n if (page.path.startsWith(\"/_elur\") || page.path.startsWith(\"/__elur-js\")) continue;\n\n routeUrls.push({\n url: page.path,\n changefreq: options.defaultChangefreq,\n priority: options.defaultPriority,\n });\n }\n\n // Merge with extra URLs.\n const allUrls = [...routeUrls, ...extraUrls];\n\n // If under the limit, generate a single sitemap.\n if (allUrls.length <= maxUrlsPerSitemap) {\n const path = await generateSitemap({ siteUrl, outDir, urls: allUrls });\n return [path];\n }\n\n // For large sitemaps, split into multiple files with an index.\n return generateSitemapIndex({ siteUrl, outDir, urls: allUrls, maxUrlsPerSitemap });\n}\n\n/**\n * Generates a sitemap index file that references multiple sitemap files.\n * Used for large sites (>50,000 URLs).\n */\nasync function generateSitemapIndex(\n options: { siteUrl: string; outDir: string; urls: (SitemapEntry | string)[]; maxUrlsPerSitemap: number },\n): Promise<string[]> {\n const { siteUrl, outDir, urls, maxUrlsPerSitemap } = options;\n const base = siteUrl.replace(/\\/$/, \"\");\n const files: string[] = [];\n const sitemapUrls: string[] = [];\n\n // Split URLs into chunks.\n for (let i = 0; i < urls.length; i += maxUrlsPerSitemap) {\n const chunk = urls.slice(i, i + maxUrlsPerSitemap);\n const filename = `sitemap-${Math.floor(i / maxUrlsPerSitemap) + 1}.xml`;\n const { writeFile, mkdir } = await import(\"node:fs/promises\");\n const { join, dirname } = await import(\"node:path\");\n\n // Generate the chunk sitemap.\n const entries = chunk.map((entry) => {\n const e = typeof entry === \"string\" ? { url: entry } : entry;\n const loc = `${base}${e.url.startsWith(\"/\") ? \"\" : \"/\"}${e.url}`;\n const lines = [` <url>`, ` <loc>${escapeXml(loc)}</loc>`];\n if (e.lastmod) lines.push(` <lastmod>${e.lastmod}</lastmod>`);\n if (e.changefreq) lines.push(` <changefreq>${e.changefreq}</changefreq>`);\n if (e.priority !== undefined) lines.push(` <priority>${e.priority.toFixed(1)}</priority>`);\n lines.push(` </url>`);\n return lines.join(\"\\n\");\n });\n\n const xml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n${entries.join(\"\\n\")}\\n</urlset>\\n`;\n const filePath = join(outDir, filename);\n await mkdir(dirname(filePath), { recursive: true });\n await writeFile(filePath, xml, \"utf8\");\n files.push(filePath);\n sitemapUrls.push(`${base}/${filename}`);\n }\n\n // Generate the index file.\n const { writeFile, mkdir } = await import(\"node:fs/promises\");\n const { join, dirname } = await import(\"node:path\");\n\n const indexEntries = sitemapUrls.map((url) => ` <sitemap>\\n <loc>${escapeXml(url)}</loc>\\n </sitemap>`).join(\"\\n\");\n const indexXml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n${indexEntries}\\n</sitemapindex>\\n`;\n const indexPath = join(outDir, \"sitemap.xml\");\n await mkdir(dirname(indexPath), { recursive: true });\n await writeFile(indexPath, indexXml, \"utf8\");\n files.push(indexPath);\n\n return files;\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ export interface RenderPageOptions {
6
6
  route: PageRoute;
7
7
  params?: RouteParams;
8
8
  searchParams?: URLSearchParams;
9
- config: Pick<BuildConfig, "lang" | "clientEntry" | "renderEndpoint">;
9
+ config: Pick<BuildConfig, "lang" | "clientEntry" | "renderEndpoint" | "router" | "js">;
10
10
  /** Custom module loader. Defaults to native dynamic import. */
11
11
  importer?: (path: string) => Promise<unknown>;
12
12
  /** Per-page action names exposed in the HTML shell. */
@@ -27,6 +27,12 @@ export interface RenderPageResult {
27
27
  head?: string;
28
28
  /** Resolved page title (from metadata or fallback). */
29
29
  resolvedTitle?: string;
30
+ /**
31
+ * Loader data as rendered into `<script id="elur-data">`. Exposed so the
32
+ * SPA render endpoint can ship it in the payload and the client router can
33
+ * keep the serialized data fresh across navigations.
34
+ */
35
+ data?: unknown;
30
36
  /**
31
37
  * When a loader or layout throws a `Response` (e.g. `throw new Response(...,
32
38
  * { status: 404 })`), it is captured here as a first-class response instead
@@ -52,7 +58,7 @@ export interface RenderErrorPageOptions {
52
58
  routes: ScannedRoutes;
53
59
  status: 404 | 500;
54
60
  error?: unknown;
55
- config: Pick<BuildConfig, "lang" | "clientEntry" | "renderEndpoint">;
61
+ config: Pick<BuildConfig, "lang" | "clientEntry" | "renderEndpoint" | "router" | "js">;
56
62
  actions?: Record<string, string[]>;
57
63
  importer?: (path: string) => Promise<unknown>;
58
64
  }
@@ -6,7 +6,7 @@ export interface RenderPageOptions {
6
6
  route: PageRoute;
7
7
  params?: RouteParams;
8
8
  searchParams?: URLSearchParams;
9
- config: Pick<BuildConfig, "lang" | "clientEntry" | "renderEndpoint">;
9
+ config: Pick<BuildConfig, "lang" | "clientEntry" | "renderEndpoint" | "router" | "js">;
10
10
  /** Custom module loader. Defaults to native dynamic import. */
11
11
  importer?: (path: string) => Promise<unknown>;
12
12
  /** Per-page action names exposed in the HTML shell. */
@@ -27,6 +27,12 @@ export interface RenderPageResult {
27
27
  head?: string;
28
28
  /** Resolved page title (from metadata or fallback). */
29
29
  resolvedTitle?: string;
30
+ /**
31
+ * Loader data as rendered into `<script id="elur-data">`. Exposed so the
32
+ * SPA render endpoint can ship it in the payload and the client router can
33
+ * keep the serialized data fresh across navigations.
34
+ */
35
+ data?: unknown;
30
36
  /**
31
37
  * When a loader or layout throws a `Response` (e.g. `throw new Response(...,
32
38
  * { status: 404 })`), it is captured here as a first-class response instead
@@ -52,7 +58,7 @@ export interface RenderErrorPageOptions {
52
58
  routes: ScannedRoutes;
53
59
  status: 404 | 500;
54
60
  error?: unknown;
55
- config: Pick<BuildConfig, "lang" | "clientEntry" | "renderEndpoint">;
61
+ config: Pick<BuildConfig, "lang" | "clientEntry" | "renderEndpoint" | "router" | "js">;
56
62
  actions?: Record<string, string[]>;
57
63
  importer?: (path: string) => Promise<unknown>;
58
64
  }
@@ -31,6 +31,13 @@ export interface SsrServer {
31
31
  }
32
32
  /**
33
33
  * Create an SSR server that renders pages on demand and serves static files.
34
+ *
35
+ * @deprecated Legacy pipeline superseded by the unified Web handler
36
+ * (`createWebHandler` from `@elurjs/kit/runtime`), which adds structured
37
+ * logging, security headers, redirects/rewrites, streaming SSR and the
38
+ * pluggable ISR cache. The CLI no longer uses this server (`dev`, `preview`
39
+ * and `start` all run through `createWebHandler`); it remains exported for
40
+ * backward compatibility and will be removed in a future major release.
34
41
  */
35
42
  export declare function createSsrServer(options: SsrServerOptions): Promise<SsrServer>;
36
43
  /**
@@ -31,6 +31,13 @@ export interface SsrServer {
31
31
  }
32
32
  /**
33
33
  * Create an SSR server that renders pages on demand and serves static files.
34
+ *
35
+ * @deprecated Legacy pipeline superseded by the unified Web handler
36
+ * (`createWebHandler` from `@elurjs/kit/runtime`), which adds structured
37
+ * logging, security headers, redirects/rewrites, streaming SSR and the
38
+ * pluggable ISR cache. The CLI no longer uses this server (`dev`, `preview`
39
+ * and `start` all run through `createWebHandler`); it remains exported for
40
+ * backward compatibility and will be removed in a future major release.
34
41
  */
35
42
  export declare function createSsrServer(options: SsrServerOptions): Promise<SsrServer>;
36
43
  /**