@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
package/CHANGELOG.md CHANGED
@@ -5,6 +5,319 @@ All notable changes to Elur Kit are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.5.1]
9
+
10
+ ### Fixed
11
+
12
+ - **`elur-kit build` never generated image variants** — the CLI bundle
13
+ (`dist/lib/cli.js`) had `import("sharp")` rewritten by Vite's
14
+ optional-peer-dep transform into a stub that always throws
15
+ (`Could not resolve "sharp"`), because `sharp` was missing from the
16
+ externals in `vite.cli.config.ts`. `loadSharp()` caught the error and
17
+ permanently returned `null`, so `processImageBatch` fell back to
18
+ manifest-only entries (`variants: []`) and every `image()` emitted a
19
+ plain `<img>` instead of a `<picture>` with WebP/AVIF sources. `sharp`
20
+ is now external in the CLI build, so the optional peer resolves from
21
+ the consuming project's `node_modules` at runtime.
22
+
23
+ ## [2.5.0]
24
+
25
+ All changes are additive and backwards-compatible: existing projects work
26
+ unchanged without touching their config.
27
+
28
+ ### Added
29
+
30
+ - **Structured request logging** — the unified web handler
31
+ (`createWebHandler`, used by the dev server, preview server, and the
32
+ generated Node/Bun servers) now creates a `StructuredLogger` per request
33
+ instead of bare `console.error` calls. Error logs are emitted as JSON in
34
+ production (readable `[LEVEL]` text in dev) with structured fields
35
+ (`path`, `method`, `route`, `error`, `stack`), sensitive-header
36
+ redaction, and a request ID taken from the incoming `X-Request-ID`
37
+ header or generated per request. Configure via
38
+ `defineConfig({ logger: { level: "debug" | "info" | "warn" | "error" } })`
39
+ (default: `debug` in dev, `info` in production).
40
+ - **`X-Request-ID` response header** on every handler response, enabling
41
+ request correlation between clients, proxies, and logs.
42
+ - **`Server-Timing` response header** with per-phase timings (`action`,
43
+ `render-endpoint`, `api`, `ssr`), visible in browser DevTools → Network
44
+ → Server Timing.
45
+ - **Redirects, rewrites, and route headers** — the previously internal
46
+ `src/router/redirects.ts` module is now wired into the handler pipeline.
47
+ Configure via `defineConfig({ redirects, rewrites, headers })`:
48
+ - `redirects: [{ from, to, status? }]` are evaluated before all routing
49
+ and return a `Location` response (default status `308`; also `301`,
50
+ `302`, `307`).
51
+ - `rewrites: [{ from, to }]` rewrite the pathname transparently (the
52
+ browser URL does not change); the rewritten path drives static
53
+ serving, SSR, API routes, and the ISR cache key.
54
+ - `headers: [{ path, headers }]` merge custom headers into responses
55
+ matching the public (pre-rewrite) path; they may override security
56
+ headers, while `Server-Timing`/`X-Request-ID` always win.
57
+ - Patterns support literal segments, `:param`, `*` wildcards, and
58
+ `:param*` catch-alls, with `:param` interpolation in destinations.
59
+ - Works in dev, preview, and the generated Node/Bun servers. The
60
+ Vercel/Netlify bundled handlers do not pick this up yet.
61
+ - **Public exports** — `StructuredLogger`, `createRequestLogger`, and the
62
+ `LogLevel`/`LogEntry`/`ServerTimingMetric` types are now exported from
63
+ the package root.
64
+ - **Pluggable ISR cache adapters in the handler** — the unified web
65
+ handler now stores rendered pages through the `CacheAdapter` interface
66
+ instead of the legacy cache module. Configure via
67
+ `defineConfig({ cache: { adapter } })` (e.g. Redis or Cloudflare KV);
68
+ when omitted, a filesystem adapter rooted at `cache.dir` is created and
69
+ shared per directory. Page entries are keyed with `cacheKey(pathname)`
70
+ (SHA-256), the same scheme used by path-based invalidation.
71
+ - **Cache policy tags are now honored** — `tags` declared in a route's
72
+ `cache` policy (`page.data.ts`) are stored with the cache entry, so
73
+ tag-based invalidation works end to end.
74
+ - **Automatic cache invalidation from actions** — after a successful
75
+ action run, `invalidateTags`/`invalidatePaths` declared via
76
+ `defineAction()` are dispatched to the connected cache adapter
77
+ (`defaultInvalidator`). Actions that return `fail(...)` do not
78
+ invalidate anything.
79
+ - **Stale-while-revalidate for ISR pages** — stale cache entries are
80
+ served immediately while the page re-renders in the background, instead
81
+ of blocking the request on a full re-render.
82
+ - **New `@elurjs/kit/cache` subpath** — exports `CacheAdapter`,
83
+ `createFsCacheAdapter`, `createRedisCacheAdapter`,
84
+ `createCloudflareKVCacheAdapter`, `getWithSWR`, `cacheKey`, the cache
85
+ policy helpers, and the invalidation primitives.
86
+ - **Automatic sitemap generation in `build`** — when `site` is configured
87
+ in `defineConfig`, the build now writes `sitemap.xml` from the scanned
88
+ route manifest (dynamic routes, error pages and internal namespaces are
89
+ excluded; sites over 50,000 URLs are split into `sitemap-N.xml` files
90
+ with a `sitemap.xml` index). An existing `sitemap.xml` — copied from
91
+ `public/` or written by an integration `build` hook — always takes
92
+ precedence and is never overwritten. `generateSitemapFromRoutes` and its
93
+ options type are re-exported from `@elurjs/kit/seo`.
94
+ - **`--verbose` / `--quiet` CLI flags** — override `logger.level` from the
95
+ config: `--verbose` maps to `debug`, `--quiet` maps to `error` (only
96
+ errors are printed; `--quiet` wins if both are passed).
97
+ - **Real streaming SSR in the unified handler (opt-in, experimental)** —
98
+ `defineConfig({ streaming: true })` enables true streaming for dynamic
99
+ routes that declare a `loading` boundary: the document shell + loading
100
+ fallback are flushed immediately, the page render continues in the
101
+ background, and the resolved content arrives as a `<template>` chunk that
102
+ swaps the boundary in-place. Details:
103
+ - Wired into `createWebHandler` (new `streaming` and `capabilities`
104
+ options), so `dev`, `preview`, `start` and the generated Node/Bun
105
+ servers all stream through the same code path. Hosts that declare
106
+ `capabilities.streaming: false` degrade to buffered rendering, and the
107
+ CLI `adapter` command validates the combination at build time.
108
+ - Streamed responses send `Content-Type: text/html` early, no
109
+ `Content-Length`, `X-Accel-Buffering: no` (nginx must not buffer) and
110
+ `Cache-Control: no-store`. `Server-Timing`'s `ssr` metric measures
111
+ time-to-shell.
112
+ - **ISR interaction**: streamed pages bypass the cache entirely (never
113
+ read, never written); buffered routes keep the existing ISR behavior.
114
+ - **Abort handling**: client disconnects cancel the stream via
115
+ `request.signal`; late background renders are discarded. The Node
116
+ server now writes responses with the new `sendWebResponse()` helper
117
+ (`@elurjs/kit/runtime`), which forwards chunks as they are produced
118
+ with backpressure and cancels the upstream stream on socket close.
119
+ - **Mid-stream failures**: a loader redirect `Response` navigates via an
120
+ inline script chunk; a render error swaps the boundary for an
121
+ inline-styled `role="alert"` notice instead of a dead spinner.
122
+ - `documentShell` now delimits the `#app` content with explicit
123
+ `<!--elur:app:start-->`/`<!--elur:app:end-->` markers (invisible HTML
124
+ comments), replacing the fragile `<div id="app">` regex extraction;
125
+ `extractAppBody()` is exported for adapters. `createStreamingResponse`
126
+ and `createBufferedResponse` are now exported from the package root.
127
+ - **User middleware in the unified handler** — `src/middleware.ts` now runs
128
+ in `dev`, `preview` and `start` (previously only the legacy SSR server ran
129
+ it). Semantics: after redirects/rewrites and the internal endpoints,
130
+ before routing; a returned `Response` short-circuits through the standard
131
+ finalize step (security headers, `X-Request-ID`, `Server-Timing` still
132
+ apply); `next({ headers })` merges into the downstream request and
133
+ `next({ locals })` is exposed to API routes as `ctx.locals`. Middleware
134
+ errors return a sanitized 500 instead of crashing the request. The
135
+ generated Node/Bun adapter servers do not run the middleware file yet.
136
+ - **`RedirectRule` / `RewriteRule` / `RouteHeadersRule` type exports** from
137
+ the package root, so config rule arrays can be annotated outside
138
+ `defineConfig`.
139
+ - **0% JavaScript by default (per-page script gating)** — `documentShell`
140
+ no longer emits the client entry unconditionally. In the default
141
+ `js: "modern"` mode the shell inspects the rendered body for
142
+ `data-elur-island` markers: pages without islands ship **no client JS**
143
+ when the router is disabled, or **only the router chunk** when it is on.
144
+ Streaming shells are exempt (the body is not known when the shell is
145
+ sent, so streamed routes always emit the entry). Opt out entirely with
146
+ `defineConfig({ js: "legacy" })`, which restores the unconditional
147
+ combined entry.
148
+ - **Split client entries (hydration vs router)** — the generated client
149
+ entry is now hydrate-only and the router lives in its own module
150
+ (`.elur/router.ts` → `/_elur/router.js`). The CLI decides the layout
151
+ before rendering pages: projects with a two-input
152
+ `vite.client.config.*` (or no client config at all — a default
153
+ two-input config is now synthesized) get the split; single-input
154
+ configs keep the router embedded in `entry-client.js` and the shell
155
+ emits it whenever islands exist *or* the router is enabled. Wired
156
+ through `ElurConfig.router`, `BuildConfig.router`,
157
+ `GenerateEntryOptions.router`, `WebHandlerOptions.router`,
158
+ `AdapterOptions.router`/`js`, `documentShell` (`routerEntry`,
159
+ `routerEnabled`), and the generated adapter SSR entries. The example
160
+ `vite.client.config.ts` now uses two named inputs.
161
+ - **`<link rel="modulepreload">` for emitted entries** — every module
162
+ script the shell emits (hydration entry and/or router chunk) also gets
163
+ a `modulepreload` link so the fetch starts during HTML parsing.
164
+ - **Navigation lifecycle events** — the client router dispatches
165
+ `elur:navigate-start`, `elur:navigate-end` and `elur:navigate-error`
166
+ with `{ pathname, search, fromCache, popstate }`, plus
167
+ `elur:before-render` right before the `#app` swap (with
168
+ `detail.persisted` listing the nodes that will survive). Islands are
169
+ now disposed on `elur:before-render` — while still attached — instead
170
+ of on `elur:rendered`.
171
+ - **`data-elur-persist` element persistence** — mark an element with
172
+ `data-elur-persist="key"` and the router moves the *same live node*
173
+ into the matching position of the new page (matched by attribute
174
+ value) using `Element.moveBefore()` when available
175
+ (`replaceWith` fallback). Island state, media playback and scroll
176
+ positions survive navigations; islands inside persisted subtrees are
177
+ skipped by the navigation cleanup. When an island inside a persisted
178
+ node would receive different props, an `elur:persist-props-changed`
179
+ event bubbles from its marker. `cleanupHydratedIslands({ except })`
180
+ supports exclusion lists.
181
+ - **Complete SPA render payload** — `/__elur-js/render` now returns
182
+ `{ title, body, head, data, actions, clearActionErrorCookie }` in every
183
+ runtime (unified handler, generated Node/Bun adapters, dev server and
184
+ the legacy `createSsrServer`): `head` carries the managed
185
+ `data-elur-head` tags (title/meta/OG/Twitter), `data`/`actions` carry
186
+ the serialized contents of `#elur-data`/`#elur-actions`, and
187
+ `clearActionErrorCookie` is also relayed as the
188
+ `X-Elur-Action-Clear-Cookie` response header. Loader-thrown
189
+ `Response`s propagate as first-class responses. After a navigation the
190
+ router refreshes the inert `#elur-data`/`#elur-actions` scripts, merges
191
+ the managed head tags, and re-executes inline `<script>` elements in
192
+ the new body (external `src` scripts are deduplicated across
193
+ navigations; `data-elur-no-reload` opts out).
194
+ - **Router robustness** — the prefetch cache is now a bounded LRU (32
195
+ entries, 30 s TTL), `history.scrollRestoration` is set to `manual`
196
+ with per-entry scroll save/restore on back/forward, prefetching starts
197
+ on `pointerdown` in addition to hover/focus (and opt-in viewport via
198
+ `data-prefetch="viewport"`), and constrained networks are respected:
199
+ `Save-Data` and `slow-2g`/`2g` effective connection types skip prefetch
200
+ unless the link forces it with `data-prefetch="always"` (or
201
+ `prefetch(path, search, { force: true })` is called directly).
202
+ - **Optional DOM morphing (`router.morph`)** — `defineConfig({ router: {
203
+ morph: true } })` swaps `#app` with idiomorph-based DOM morphing
204
+ instead of a wholesale `replaceChildren` (experimental). Hydrated
205
+ islands and `data-elur-persist` subtrees are treated as opaque. New
206
+ dependency: `idiomorph` — loaded via dynamic `import()` only when the
207
+ flag is on, so it stays out of the served bytes when morph is off
208
+ (a hard dependency is required because bundlers must resolve the
209
+ specifier at build time even when the chunk is lazy).
210
+ - **Optional loading indicator (`router.loadingIndicator`)** — a minimal
211
+ top progress bar appears only when a navigation takes longer than
212
+ ~200 ms, driven by the lifecycle events; it honors
213
+ `prefers-reduced-motion` (static bar, no trickle) and never flashes on
214
+ cache hits.
215
+ - **Speculation Rules (`router.speculation`)** —
216
+ `defineConfig({ router: { speculation: "prefetch" | "prerender" } })`
217
+ emits a `<script type="speculationrules">` block with document rules
218
+ (`eagerness: "moderate"`, internal same-origin links excluding actions,
219
+ downloads, `target`/`data-no-router`/`data-no-speculation` links) on
220
+ statically built pages. Chromium-only progressive enhancement; other
221
+ browsers ignore it.
222
+
223
+ ### Deprecated
224
+
225
+ - **`createSsrServer`** (`@elurjs/kit`) — the legacy standalone SSR server
226
+ pipeline. All CLI commands (`dev`, `preview`, `start`) now run through
227
+ `createWebHandler`; the legacy server remains exported for backward
228
+ compatibility and will be removed in a future major release.
229
+ - **`renderStreamingPage`** — the legacy shell + client-fetch streaming
230
+ approach, superseded by `createStreamingResponse` (real streaming).
231
+
232
+ ### Changed
233
+
234
+ - **`elur-kit start` runs on the unified Web handler** — it previously used
235
+ the legacy `createSsrServer` pipeline. It now shares the dev/preview path:
236
+ startup banner with Local/Network URLs, busy-port fallback, per-request
237
+ structured logging with `Server-Timing`/`X-Request-ID`, security headers,
238
+ redirects/rewrites/route headers, pluggable ISR cache, user middleware, and
239
+ opt-in streaming SSR. `start` now requires a previous `elur-kit build`
240
+ (it fails fast with a clear message when `dist/` is missing) instead of
241
+ silently rendering everything on demand.
242
+ - **CLI output formatting** — build, dev, preview, and adapter messages
243
+ now use a consistent format (`✓` success, `→` info, `!` warning, `✗`
244
+ error, `[tag]` lifecycle events) with ANSI colors when stdout is a TTY
245
+ (disabled via `NO_COLOR` or when piped; no new dependencies). The build
246
+ summary now includes the total duration plus page/island/file counts.
247
+ The fatal request-error catch in the dev/preview server now logs through
248
+ the structured logger (with `path`, `method`, `error`, `stack`) instead
249
+ of a bare `console.error`.
250
+ - **Dev/preview/start startup banner** — the dev, preview and start servers
251
+ now print an Astro-style banner with the kit name and version and the
252
+ bound URLs:
253
+ `→ Local: http://…` plus `→ Network: http://<LAN-IP>:…` when the
254
+ machine has a non-internal IPv4 address. The banner reflects the port
255
+ actually bound after the busy-port fallback.
256
+ - **Build phase timings and file sizes** — `elur-kit build` now reports
257
+ each phase with a checkmark and its duration (`transform`, `scan`,
258
+ `pages`, `images`, `integrations`, `sitemap`, `manifest`,
259
+ `client bundle`; phases that don't run are omitted) via a new optional
260
+ `BuildConfig.onPhase(name, durationMs)` observer, and lists the
261
+ generated files with their sizes (aligned, Vite-style). With more than
262
+ 20 files, only the 10 largest are shown followed by a
263
+ `… and N more` summary. With `--quiet`, the client bundle step is now
264
+ silent too (new `quiet` option on `buildClientBundle`, which also sets
265
+ Vite's `logLevel: "silent"`).
266
+ - **ISR storage backend** — `createWebHandler` no longer uses the legacy
267
+ `getCachedHtml`/`setCachedHtml` module for page caching (it remains
268
+ exported for backward compatibility). Existing on-disk cache entries use
269
+ the same SHA-256-of-pathname key, but entries written by older versions
270
+ lack tag metadata, so they simply miss tag-based invalidation until
271
+ re-rendered. What gets cached (cacheability gates, TTL rules) is
272
+ unchanged.
273
+
274
+ ### Fixed
275
+
276
+ - **Dead legacy config lookup removed** — `findConfigFile` carried a legacy
277
+ config-name fallback that could never fire (its file list was identical to
278
+ the preferred one) and referenced pre-rename names that are no longer
279
+ supported. Config files are now exclusively `elur.config.ts/js/mjs`.
280
+ - **Missing type declarations for `@elurjs/kit/cache`** — the subpath
281
+ shipped its runtime bundle but no `index.d.ts`/`index.d.cts` because
282
+ `src/cache/index.ts` was absent from the declaration build inputs
283
+ (publint error). The `./cache` subpath is now fully typed (including
284
+ `redis-adapter`).
285
+ - **Infinite restart loop when the dev port is busy** — with port 3000
286
+ occupied, the dev supervisor restarted the crashed worker forever on the
287
+ same `EADDRINUSE`. The dev and preview servers now retry on the next
288
+ port (up to 20 candidates) with a clear warning
289
+ (`! Puerto 3000 ocupado, usando 3001`), and the startup message shows
290
+ the port actually bound. If the whole range is busy, the worker exits
291
+ with a dedicated code (78) that the supervisor treats as fatal instead
292
+ of restarting. Fallback also applies to an explicit `--port` (with the
293
+ same warning).
294
+ - **Actions defined with `defineAction()` crashed at the action
295
+ endpoint** — `handleActionRequest` invoked actions as `action(...args)`
296
+ without building an `ActionContext`, so any action using the default
297
+ `concurrency: "latest"` failed with `ctx undefined` (`ctx.signal`
298
+ access). Defined actions (detected via their `__elurAction` metadata)
299
+ now receive `(input, ctx)` with `ctx.request`, `ctx.signal` (from the
300
+ request), `ctx.idempotencyKey` (from the `Idempotency-Key` header),
301
+ and empty `params`/`locals`. Legacy plain actions keep the `(...args)`
302
+ call convention unchanged.
303
+ - **Head metadata lost on SPA navigations in production** — the
304
+ production render endpoint (`/__elur-js/render`) only returned
305
+ `{ title, body }`, so pages navigated via the client router kept stale
306
+ `<head>` tags and the consumed action-error cookie was never cleared.
307
+ The endpoint now ships the full payload and the cookie is relayed as
308
+ `Set-Cookie` (HTML mode) / `X-Elur-Action-Clear-Cookie` (JSON mode).
309
+ - **`directive: "load"` did not hydrate on load** — the generated client
310
+ entry wrapped the whole hydration pass in `requestIdleCallback`, so
311
+ `load` islands were really `idle` islands. The entry now calls
312
+ `hydrateIslands()` immediately (module scripts are already deferred);
313
+ `idle`/`visible` keep their deferred scheduling inside the hydrator.
314
+ - **Islands were disposed after the DOM swap** — the generated entry
315
+ listened only for `elur:rendered` (post-swap), so disposers ran on
316
+ detached DOM and could not read live state. Cleanup now runs on
317
+ `elur:before-render`, dispatched before `#app` is replaced, with a
318
+ compat fallback on `elur:rendered` for hosts that only emit the old
319
+ event (e.g. the streaming swap chunk).
320
+
8
321
  ## [2.4.10]
9
322
 
10
323
  ### Fixed