@elurjs/kit 2.4.10 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +298 -0
- package/README.md +195 -9
- package/dist/lib/action/index.cjs +1 -1
- package/dist/lib/action/index.js +1 -1
- package/dist/lib/{action-DZHihU3Z.cjs → action-BiE4tEty.cjs} +2 -2
- package/dist/lib/{action-DZHihU3Z.cjs.map → action-BiE4tEty.cjs.map} +1 -1
- package/dist/lib/{action-BB6N5yD-.js → action-CXVk8rWt.js} +3 -3
- package/dist/lib/{action-BB6N5yD-.js.map → action-CXVk8rWt.js.map} +1 -1
- package/dist/lib/adapter-DNsjXvq5.js +160 -0
- package/dist/lib/adapter-DNsjXvq5.js.map +1 -0
- package/dist/lib/adapter-Dz2hzR36.cjs +3 -0
- package/dist/lib/adapter-Dz2hzR36.cjs.map +1 -0
- package/dist/lib/adapters/bun.cjs +2 -2
- package/dist/lib/adapters/bun.cjs.map +1 -1
- package/dist/lib/adapters/bun.js +17 -9
- package/dist/lib/adapters/bun.js.map +1 -1
- package/dist/lib/adapters/index.d.cts +34 -0
- package/dist/lib/adapters/index.d.ts +34 -0
- package/dist/lib/adapters/netlify.cjs +1 -1
- package/dist/lib/adapters/netlify.js +7 -7
- package/dist/lib/adapters/node.cjs +5 -5
- package/dist/lib/adapters/node.cjs.map +1 -1
- package/dist/lib/adapters/node.js +20 -12
- package/dist/lib/adapters/node.js.map +1 -1
- package/dist/lib/adapters/vercel.cjs +1 -1
- package/dist/lib/adapters/vercel.js +8 -8
- package/dist/lib/build/build.d.cts +40 -0
- package/dist/lib/build/build.d.ts +40 -0
- package/dist/lib/build/document-shell.d.cts +47 -1
- package/dist/lib/build/document-shell.d.ts +47 -1
- package/dist/lib/build/vite-build.d.cts +25 -2
- package/dist/lib/build/vite-build.d.ts +25 -2
- package/dist/lib/cache/adapter.d.cts +41 -0
- package/dist/lib/cache/adapter.d.ts +41 -0
- package/dist/lib/cache/index.cjs +2 -0
- package/dist/lib/cache/index.cjs.map +1 -0
- package/dist/lib/cache/index.d.cts +5 -0
- package/dist/lib/cache/index.d.ts +5 -0
- package/dist/lib/cache/index.js +113 -0
- package/dist/lib/cache/index.js.map +1 -0
- package/dist/lib/cache/invalidation.d.cts +36 -0
- package/dist/lib/cache/invalidation.d.ts +36 -0
- package/dist/lib/cache/redis-adapter.d.cts +49 -0
- package/dist/lib/cache/redis-adapter.d.ts +49 -0
- package/dist/lib/cache-DGhTYyqh.js +50 -0
- package/dist/lib/cache-DGhTYyqh.js.map +1 -0
- package/dist/lib/cache-X1n4Ossu.cjs +2 -0
- package/dist/lib/cache-X1n4Ossu.cjs.map +1 -0
- package/dist/lib/cli/output.d.cts +58 -0
- package/dist/lib/cli/output.d.ts +58 -0
- package/dist/lib/cli/ports.d.cts +20 -0
- package/dist/lib/cli/ports.d.ts +20 -0
- package/dist/lib/cli.cjs +2568 -1489
- package/dist/lib/cli.cjs.map +1 -1
- package/dist/lib/cli.d.cts +21 -0
- package/dist/lib/cli.d.ts +21 -0
- package/dist/lib/cli.js +2567 -1489
- package/dist/lib/cli.js.map +1 -1
- package/dist/lib/config/index.cjs +1 -1
- package/dist/lib/config/index.cjs.map +1 -1
- package/dist/lib/config/index.d.cts +79 -0
- package/dist/lib/config/index.d.ts +79 -0
- package/dist/lib/config/index.js +21 -14
- package/dist/lib/config/index.js.map +1 -1
- package/dist/lib/{errors-ePYxDlhU.js → errors-j4cg9VN3.js} +24 -30
- package/dist/lib/{errors-ePYxDlhU.js.map → errors-j4cg9VN3.js.map} +1 -1
- package/dist/lib/errors-kSl9yLkg.cjs +2 -0
- package/dist/lib/{errors-0X7pCFNU.cjs.map → errors-kSl9yLkg.cjs.map} +1 -1
- package/dist/lib/hydrate-BSLuI0Nh.cjs +2 -0
- package/dist/lib/hydrate-BSLuI0Nh.cjs.map +1 -0
- package/dist/lib/hydrate-BaS7-d8C.js +133 -0
- package/dist/lib/hydrate-BaS7-d8C.js.map +1 -0
- package/dist/lib/idiomorph.esm-BOOTOg1r.cjs +2 -0
- package/dist/lib/idiomorph.esm-BOOTOg1r.cjs.map +1 -0
- package/dist/lib/idiomorph.esm-CXuY-EHB.js +371 -0
- package/dist/lib/idiomorph.esm-CXuY-EHB.js.map +1 -0
- package/dist/lib/index.cjs +1 -1
- package/dist/lib/index.cjs.map +1 -1
- package/dist/lib/index.d.cts +8 -6
- package/dist/lib/index.d.ts +8 -6
- package/dist/lib/index.js +1 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/interpolation-plugin-C0dTd69H.cjs +58 -0
- package/dist/lib/interpolation-plugin-C0dTd69H.cjs.map +1 -0
- package/dist/lib/{interpolation-plugin-Wgb1j4pT.js → interpolation-plugin-K2qE69tF.js} +95 -126
- package/dist/lib/interpolation-plugin-K2qE69tF.js.map +1 -0
- package/dist/lib/invalidation-CFpNQUv6.cjs +2 -0
- package/dist/lib/invalidation-CFpNQUv6.cjs.map +1 -0
- package/dist/lib/invalidation-DXhdE9wj.js +64 -0
- package/dist/lib/invalidation-DXhdE9wj.js.map +1 -0
- package/dist/lib/island/generate-entry.d.cts +40 -2
- package/dist/lib/island/generate-entry.d.ts +40 -2
- package/dist/lib/island/hydrate.d.cts +19 -6
- package/dist/lib/island/hydrate.d.ts +19 -6
- package/dist/lib/island/index.cjs +1 -1
- package/dist/lib/island/index.d.cts +2 -1
- package/dist/lib/island/index.d.ts +2 -1
- package/dist/lib/island/index.js +1 -1
- package/dist/lib/island/island.d.cts +15 -0
- package/dist/lib/island/island.d.ts +15 -0
- package/dist/lib/island-CTBnsOhI.js +59 -0
- package/dist/lib/island-CTBnsOhI.js.map +1 -0
- package/dist/lib/island-kfQEKRe_.cjs +2 -0
- package/dist/lib/island-kfQEKRe_.cjs.map +1 -0
- package/dist/lib/node-http-DVFcYeBS.js +723 -0
- package/dist/lib/node-http-DVFcYeBS.js.map +1 -0
- package/dist/lib/node-http-Uct7DhmN.cjs +13 -0
- package/dist/lib/node-http-Uct7DhmN.cjs.map +1 -0
- package/dist/lib/rolldown-runtime-BIInC43l.cjs +1 -0
- package/dist/lib/rolldown-runtime-Dy4uBu1J.js +11 -0
- package/dist/lib/router/client.cjs +1 -1
- package/dist/lib/router/client.cjs.map +1 -1
- package/dist/lib/router/client.d.cts +55 -8
- package/dist/lib/router/client.d.ts +55 -8
- package/dist/lib/router/client.js +298 -97
- package/dist/lib/router/client.js.map +1 -1
- package/dist/lib/router/redirects.d.cts +34 -0
- package/dist/lib/router/redirects.d.ts +34 -0
- package/dist/lib/runtime/handler.d.cts +59 -0
- package/dist/lib/runtime/handler.d.ts +59 -0
- package/dist/lib/runtime/index.cjs +1 -1
- package/dist/lib/runtime/index.d.cts +2 -1
- package/dist/lib/runtime/index.d.ts +2 -1
- package/dist/lib/runtime/index.js +4 -4
- package/dist/lib/runtime/logger.d.cts +47 -0
- package/dist/lib/runtime/logger.d.ts +47 -0
- package/dist/lib/runtime/node-http.d.cts +11 -1
- package/dist/lib/runtime/node-http.d.ts +11 -1
- package/dist/lib/runtime-ChE4flX3.cjs +2 -0
- package/dist/lib/runtime-ChE4flX3.cjs.map +1 -0
- package/dist/lib/runtime-OMyz5yE-.js +905 -0
- package/dist/lib/runtime-OMyz5yE-.js.map +1 -0
- package/dist/lib/seo/index.cjs +1 -10
- package/dist/lib/seo/index.d.cts +1 -0
- package/dist/lib/seo/index.d.ts +1 -0
- package/dist/lib/seo/index.js +2 -41
- package/dist/lib/seo/sitemap-from-routes.d.cts +24 -0
- package/dist/lib/seo/sitemap-from-routes.d.ts +24 -0
- package/dist/lib/{shared-CfqOLsKj.cjs → shared-CP0Dc3cp.cjs} +72 -47
- package/dist/lib/shared-CP0Dc3cp.cjs.map +1 -0
- package/dist/lib/{shared-DU3iQgr7.js → shared-D8w-neq2.js} +87 -58
- package/dist/lib/shared-D8w-neq2.js.map +1 -0
- package/dist/lib/sitemap-from-routes-CaBoiGvE.js +77 -0
- package/dist/lib/sitemap-from-routes-CaBoiGvE.js.map +1 -0
- package/dist/lib/sitemap-from-routes-DbcLLL4G.cjs +13 -0
- package/dist/lib/sitemap-from-routes-DbcLLL4G.cjs.map +1 -0
- package/dist/lib/ssr/render.d.cts +8 -2
- package/dist/lib/ssr/render.d.ts +8 -2
- package/dist/lib/ssr/server.d.cts +7 -0
- package/dist/lib/ssr/server.d.ts +7 -0
- package/dist/lib/ssr/stream-response.d.cts +30 -0
- package/dist/lib/ssr/stream-response.d.ts +30 -0
- package/dist/lib/ssr/stream.d.cts +16 -1
- package/dist/lib/ssr/stream.d.ts +16 -1
- package/dist/lib/{stream-DmZjYqHm.cjs → stream-Cg-xYG82.cjs} +3 -3
- package/dist/lib/stream-Cg-xYG82.cjs.map +1 -0
- package/dist/lib/stream-D-ytogdu.js +106 -0
- package/dist/lib/stream-D-ytogdu.js.map +1 -0
- package/dist/lib/vite/index.cjs +1 -1
- package/dist/lib/vite/index.cjs.map +1 -1
- package/dist/lib/vite/index.d.cts +11 -0
- package/dist/lib/vite/index.d.ts +11 -0
- package/dist/lib/vite/index.js +101 -94
- package/dist/lib/vite/index.js.map +1 -1
- package/package.json +26 -9
- package/dist/lib/errors-0X7pCFNU.cjs +0 -2
- package/dist/lib/hydrate-CJbUWdXu.js +0 -126
- package/dist/lib/hydrate-CJbUWdXu.js.map +0 -1
- package/dist/lib/hydrate-D6MR24mz.cjs +0 -2
- package/dist/lib/hydrate-D6MR24mz.cjs.map +0 -1
- package/dist/lib/interpolation-plugin-TWDch8nn.cjs +0 -39
- package/dist/lib/interpolation-plugin-TWDch8nn.cjs.map +0 -1
- package/dist/lib/interpolation-plugin-Wgb1j4pT.js.map +0 -1
- package/dist/lib/node-http-DRAUhO0c.js +0 -617
- package/dist/lib/node-http-DRAUhO0c.js.map +0 -1
- package/dist/lib/node-http-DhxguYyz.cjs +0 -13
- package/dist/lib/node-http-DhxguYyz.cjs.map +0 -1
- package/dist/lib/runtime-CrH5X-Es.cjs +0 -2
- package/dist/lib/runtime-CrH5X-Es.cjs.map +0 -1
- package/dist/lib/runtime-Ds2-wH6P.js +0 -520
- package/dist/lib/runtime-Ds2-wH6P.js.map +0 -1
- package/dist/lib/seo/index.cjs.map +0 -1
- package/dist/lib/seo/index.js.map +0 -1
- package/dist/lib/shared-CfqOLsKj.cjs.map +0 -1
- package/dist/lib/shared-DU3iQgr7.js.map +0 -1
- package/dist/lib/stream-CpOG052V.js +0 -104
- package/dist/lib/stream-CpOG052V.js.map +0 -1
- package/dist/lib/stream-DmZjYqHm.cjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,304 @@ 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.0]
|
|
9
|
+
|
|
10
|
+
All changes are additive and backwards-compatible: existing projects work
|
|
11
|
+
unchanged without touching their config.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Structured request logging** — the unified web handler
|
|
16
|
+
(`createWebHandler`, used by the dev server, preview server, and the
|
|
17
|
+
generated Node/Bun servers) now creates a `StructuredLogger` per request
|
|
18
|
+
instead of bare `console.error` calls. Error logs are emitted as JSON in
|
|
19
|
+
production (readable `[LEVEL]` text in dev) with structured fields
|
|
20
|
+
(`path`, `method`, `route`, `error`, `stack`), sensitive-header
|
|
21
|
+
redaction, and a request ID taken from the incoming `X-Request-ID`
|
|
22
|
+
header or generated per request. Configure via
|
|
23
|
+
`defineConfig({ logger: { level: "debug" | "info" | "warn" | "error" } })`
|
|
24
|
+
(default: `debug` in dev, `info` in production).
|
|
25
|
+
- **`X-Request-ID` response header** on every handler response, enabling
|
|
26
|
+
request correlation between clients, proxies, and logs.
|
|
27
|
+
- **`Server-Timing` response header** with per-phase timings (`action`,
|
|
28
|
+
`render-endpoint`, `api`, `ssr`), visible in browser DevTools → Network
|
|
29
|
+
→ Server Timing.
|
|
30
|
+
- **Redirects, rewrites, and route headers** — the previously internal
|
|
31
|
+
`src/router/redirects.ts` module is now wired into the handler pipeline.
|
|
32
|
+
Configure via `defineConfig({ redirects, rewrites, headers })`:
|
|
33
|
+
- `redirects: [{ from, to, status? }]` are evaluated before all routing
|
|
34
|
+
and return a `Location` response (default status `308`; also `301`,
|
|
35
|
+
`302`, `307`).
|
|
36
|
+
- `rewrites: [{ from, to }]` rewrite the pathname transparently (the
|
|
37
|
+
browser URL does not change); the rewritten path drives static
|
|
38
|
+
serving, SSR, API routes, and the ISR cache key.
|
|
39
|
+
- `headers: [{ path, headers }]` merge custom headers into responses
|
|
40
|
+
matching the public (pre-rewrite) path; they may override security
|
|
41
|
+
headers, while `Server-Timing`/`X-Request-ID` always win.
|
|
42
|
+
- Patterns support literal segments, `:param`, `*` wildcards, and
|
|
43
|
+
`:param*` catch-alls, with `:param` interpolation in destinations.
|
|
44
|
+
- Works in dev, preview, and the generated Node/Bun servers. The
|
|
45
|
+
Vercel/Netlify bundled handlers do not pick this up yet.
|
|
46
|
+
- **Public exports** — `StructuredLogger`, `createRequestLogger`, and the
|
|
47
|
+
`LogLevel`/`LogEntry`/`ServerTimingMetric` types are now exported from
|
|
48
|
+
the package root.
|
|
49
|
+
- **Pluggable ISR cache adapters in the handler** — the unified web
|
|
50
|
+
handler now stores rendered pages through the `CacheAdapter` interface
|
|
51
|
+
instead of the legacy cache module. Configure via
|
|
52
|
+
`defineConfig({ cache: { adapter } })` (e.g. Redis or Cloudflare KV);
|
|
53
|
+
when omitted, a filesystem adapter rooted at `cache.dir` is created and
|
|
54
|
+
shared per directory. Page entries are keyed with `cacheKey(pathname)`
|
|
55
|
+
(SHA-256), the same scheme used by path-based invalidation.
|
|
56
|
+
- **Cache policy tags are now honored** — `tags` declared in a route's
|
|
57
|
+
`cache` policy (`page.data.ts`) are stored with the cache entry, so
|
|
58
|
+
tag-based invalidation works end to end.
|
|
59
|
+
- **Automatic cache invalidation from actions** — after a successful
|
|
60
|
+
action run, `invalidateTags`/`invalidatePaths` declared via
|
|
61
|
+
`defineAction()` are dispatched to the connected cache adapter
|
|
62
|
+
(`defaultInvalidator`). Actions that return `fail(...)` do not
|
|
63
|
+
invalidate anything.
|
|
64
|
+
- **Stale-while-revalidate for ISR pages** — stale cache entries are
|
|
65
|
+
served immediately while the page re-renders in the background, instead
|
|
66
|
+
of blocking the request on a full re-render.
|
|
67
|
+
- **New `@elurjs/kit/cache` subpath** — exports `CacheAdapter`,
|
|
68
|
+
`createFsCacheAdapter`, `createRedisCacheAdapter`,
|
|
69
|
+
`createCloudflareKVCacheAdapter`, `getWithSWR`, `cacheKey`, the cache
|
|
70
|
+
policy helpers, and the invalidation primitives.
|
|
71
|
+
- **Automatic sitemap generation in `build`** — when `site` is configured
|
|
72
|
+
in `defineConfig`, the build now writes `sitemap.xml` from the scanned
|
|
73
|
+
route manifest (dynamic routes, error pages and internal namespaces are
|
|
74
|
+
excluded; sites over 50,000 URLs are split into `sitemap-N.xml` files
|
|
75
|
+
with a `sitemap.xml` index). An existing `sitemap.xml` — copied from
|
|
76
|
+
`public/` or written by an integration `build` hook — always takes
|
|
77
|
+
precedence and is never overwritten. `generateSitemapFromRoutes` and its
|
|
78
|
+
options type are re-exported from `@elurjs/kit/seo`.
|
|
79
|
+
- **`--verbose` / `--quiet` CLI flags** — override `logger.level` from the
|
|
80
|
+
config: `--verbose` maps to `debug`, `--quiet` maps to `error` (only
|
|
81
|
+
errors are printed; `--quiet` wins if both are passed).
|
|
82
|
+
- **Real streaming SSR in the unified handler (opt-in, experimental)** —
|
|
83
|
+
`defineConfig({ streaming: true })` enables true streaming for dynamic
|
|
84
|
+
routes that declare a `loading` boundary: the document shell + loading
|
|
85
|
+
fallback are flushed immediately, the page render continues in the
|
|
86
|
+
background, and the resolved content arrives as a `<template>` chunk that
|
|
87
|
+
swaps the boundary in-place. Details:
|
|
88
|
+
- Wired into `createWebHandler` (new `streaming` and `capabilities`
|
|
89
|
+
options), so `dev`, `preview`, `start` and the generated Node/Bun
|
|
90
|
+
servers all stream through the same code path. Hosts that declare
|
|
91
|
+
`capabilities.streaming: false` degrade to buffered rendering, and the
|
|
92
|
+
CLI `adapter` command validates the combination at build time.
|
|
93
|
+
- Streamed responses send `Content-Type: text/html` early, no
|
|
94
|
+
`Content-Length`, `X-Accel-Buffering: no` (nginx must not buffer) and
|
|
95
|
+
`Cache-Control: no-store`. `Server-Timing`'s `ssr` metric measures
|
|
96
|
+
time-to-shell.
|
|
97
|
+
- **ISR interaction**: streamed pages bypass the cache entirely (never
|
|
98
|
+
read, never written); buffered routes keep the existing ISR behavior.
|
|
99
|
+
- **Abort handling**: client disconnects cancel the stream via
|
|
100
|
+
`request.signal`; late background renders are discarded. The Node
|
|
101
|
+
server now writes responses with the new `sendWebResponse()` helper
|
|
102
|
+
(`@elurjs/kit/runtime`), which forwards chunks as they are produced
|
|
103
|
+
with backpressure and cancels the upstream stream on socket close.
|
|
104
|
+
- **Mid-stream failures**: a loader redirect `Response` navigates via an
|
|
105
|
+
inline script chunk; a render error swaps the boundary for an
|
|
106
|
+
inline-styled `role="alert"` notice instead of a dead spinner.
|
|
107
|
+
- `documentShell` now delimits the `#app` content with explicit
|
|
108
|
+
`<!--elur:app:start-->`/`<!--elur:app:end-->` markers (invisible HTML
|
|
109
|
+
comments), replacing the fragile `<div id="app">` regex extraction;
|
|
110
|
+
`extractAppBody()` is exported for adapters. `createStreamingResponse`
|
|
111
|
+
and `createBufferedResponse` are now exported from the package root.
|
|
112
|
+
- **User middleware in the unified handler** — `src/middleware.ts` now runs
|
|
113
|
+
in `dev`, `preview` and `start` (previously only the legacy SSR server ran
|
|
114
|
+
it). Semantics: after redirects/rewrites and the internal endpoints,
|
|
115
|
+
before routing; a returned `Response` short-circuits through the standard
|
|
116
|
+
finalize step (security headers, `X-Request-ID`, `Server-Timing` still
|
|
117
|
+
apply); `next({ headers })` merges into the downstream request and
|
|
118
|
+
`next({ locals })` is exposed to API routes as `ctx.locals`. Middleware
|
|
119
|
+
errors return a sanitized 500 instead of crashing the request. The
|
|
120
|
+
generated Node/Bun adapter servers do not run the middleware file yet.
|
|
121
|
+
- **`RedirectRule` / `RewriteRule` / `RouteHeadersRule` type exports** from
|
|
122
|
+
the package root, so config rule arrays can be annotated outside
|
|
123
|
+
`defineConfig`.
|
|
124
|
+
- **0% JavaScript by default (per-page script gating)** — `documentShell`
|
|
125
|
+
no longer emits the client entry unconditionally. In the default
|
|
126
|
+
`js: "modern"` mode the shell inspects the rendered body for
|
|
127
|
+
`data-elur-island` markers: pages without islands ship **no client JS**
|
|
128
|
+
when the router is disabled, or **only the router chunk** when it is on.
|
|
129
|
+
Streaming shells are exempt (the body is not known when the shell is
|
|
130
|
+
sent, so streamed routes always emit the entry). Opt out entirely with
|
|
131
|
+
`defineConfig({ js: "legacy" })`, which restores the unconditional
|
|
132
|
+
combined entry.
|
|
133
|
+
- **Split client entries (hydration vs router)** — the generated client
|
|
134
|
+
entry is now hydrate-only and the router lives in its own module
|
|
135
|
+
(`.elur/router.ts` → `/_elur/router.js`). The CLI decides the layout
|
|
136
|
+
before rendering pages: projects with a two-input
|
|
137
|
+
`vite.client.config.*` (or no client config at all — a default
|
|
138
|
+
two-input config is now synthesized) get the split; single-input
|
|
139
|
+
configs keep the router embedded in `entry-client.js` and the shell
|
|
140
|
+
emits it whenever islands exist *or* the router is enabled. Wired
|
|
141
|
+
through `ElurConfig.router`, `BuildConfig.router`,
|
|
142
|
+
`GenerateEntryOptions.router`, `WebHandlerOptions.router`,
|
|
143
|
+
`AdapterOptions.router`/`js`, `documentShell` (`routerEntry`,
|
|
144
|
+
`routerEnabled`), and the generated adapter SSR entries. The example
|
|
145
|
+
`vite.client.config.ts` now uses two named inputs.
|
|
146
|
+
- **`<link rel="modulepreload">` for emitted entries** — every module
|
|
147
|
+
script the shell emits (hydration entry and/or router chunk) also gets
|
|
148
|
+
a `modulepreload` link so the fetch starts during HTML parsing.
|
|
149
|
+
- **Navigation lifecycle events** — the client router dispatches
|
|
150
|
+
`elur:navigate-start`, `elur:navigate-end` and `elur:navigate-error`
|
|
151
|
+
with `{ pathname, search, fromCache, popstate }`, plus
|
|
152
|
+
`elur:before-render` right before the `#app` swap (with
|
|
153
|
+
`detail.persisted` listing the nodes that will survive). Islands are
|
|
154
|
+
now disposed on `elur:before-render` — while still attached — instead
|
|
155
|
+
of on `elur:rendered`.
|
|
156
|
+
- **`data-elur-persist` element persistence** — mark an element with
|
|
157
|
+
`data-elur-persist="key"` and the router moves the *same live node*
|
|
158
|
+
into the matching position of the new page (matched by attribute
|
|
159
|
+
value) using `Element.moveBefore()` when available
|
|
160
|
+
(`replaceWith` fallback). Island state, media playback and scroll
|
|
161
|
+
positions survive navigations; islands inside persisted subtrees are
|
|
162
|
+
skipped by the navigation cleanup. When an island inside a persisted
|
|
163
|
+
node would receive different props, an `elur:persist-props-changed`
|
|
164
|
+
event bubbles from its marker. `cleanupHydratedIslands({ except })`
|
|
165
|
+
supports exclusion lists.
|
|
166
|
+
- **Complete SPA render payload** — `/__elur-js/render` now returns
|
|
167
|
+
`{ title, body, head, data, actions, clearActionErrorCookie }` in every
|
|
168
|
+
runtime (unified handler, generated Node/Bun adapters, dev server and
|
|
169
|
+
the legacy `createSsrServer`): `head` carries the managed
|
|
170
|
+
`data-elur-head` tags (title/meta/OG/Twitter), `data`/`actions` carry
|
|
171
|
+
the serialized contents of `#elur-data`/`#elur-actions`, and
|
|
172
|
+
`clearActionErrorCookie` is also relayed as the
|
|
173
|
+
`X-Elur-Action-Clear-Cookie` response header. Loader-thrown
|
|
174
|
+
`Response`s propagate as first-class responses. After a navigation the
|
|
175
|
+
router refreshes the inert `#elur-data`/`#elur-actions` scripts, merges
|
|
176
|
+
the managed head tags, and re-executes inline `<script>` elements in
|
|
177
|
+
the new body (external `src` scripts are deduplicated across
|
|
178
|
+
navigations; `data-elur-no-reload` opts out).
|
|
179
|
+
- **Router robustness** — the prefetch cache is now a bounded LRU (32
|
|
180
|
+
entries, 30 s TTL), `history.scrollRestoration` is set to `manual`
|
|
181
|
+
with per-entry scroll save/restore on back/forward, prefetching starts
|
|
182
|
+
on `pointerdown` in addition to hover/focus (and opt-in viewport via
|
|
183
|
+
`data-prefetch="viewport"`), and constrained networks are respected:
|
|
184
|
+
`Save-Data` and `slow-2g`/`2g` effective connection types skip prefetch
|
|
185
|
+
unless the link forces it with `data-prefetch="always"` (or
|
|
186
|
+
`prefetch(path, search, { force: true })` is called directly).
|
|
187
|
+
- **Optional DOM morphing (`router.morph`)** — `defineConfig({ router: {
|
|
188
|
+
morph: true } })` swaps `#app` with idiomorph-based DOM morphing
|
|
189
|
+
instead of a wholesale `replaceChildren` (experimental). Hydrated
|
|
190
|
+
islands and `data-elur-persist` subtrees are treated as opaque. New
|
|
191
|
+
dependency: `idiomorph` — loaded via dynamic `import()` only when the
|
|
192
|
+
flag is on, so it stays out of the served bytes when morph is off
|
|
193
|
+
(a hard dependency is required because bundlers must resolve the
|
|
194
|
+
specifier at build time even when the chunk is lazy).
|
|
195
|
+
- **Optional loading indicator (`router.loadingIndicator`)** — a minimal
|
|
196
|
+
top progress bar appears only when a navigation takes longer than
|
|
197
|
+
~200 ms, driven by the lifecycle events; it honors
|
|
198
|
+
`prefers-reduced-motion` (static bar, no trickle) and never flashes on
|
|
199
|
+
cache hits.
|
|
200
|
+
- **Speculation Rules (`router.speculation`)** —
|
|
201
|
+
`defineConfig({ router: { speculation: "prefetch" | "prerender" } })`
|
|
202
|
+
emits a `<script type="speculationrules">` block with document rules
|
|
203
|
+
(`eagerness: "moderate"`, internal same-origin links excluding actions,
|
|
204
|
+
downloads, `target`/`data-no-router`/`data-no-speculation` links) on
|
|
205
|
+
statically built pages. Chromium-only progressive enhancement; other
|
|
206
|
+
browsers ignore it.
|
|
207
|
+
|
|
208
|
+
### Deprecated
|
|
209
|
+
|
|
210
|
+
- **`createSsrServer`** (`@elurjs/kit`) — the legacy standalone SSR server
|
|
211
|
+
pipeline. All CLI commands (`dev`, `preview`, `start`) now run through
|
|
212
|
+
`createWebHandler`; the legacy server remains exported for backward
|
|
213
|
+
compatibility and will be removed in a future major release.
|
|
214
|
+
- **`renderStreamingPage`** — the legacy shell + client-fetch streaming
|
|
215
|
+
approach, superseded by `createStreamingResponse` (real streaming).
|
|
216
|
+
|
|
217
|
+
### Changed
|
|
218
|
+
|
|
219
|
+
- **`elur-kit start` runs on the unified Web handler** — it previously used
|
|
220
|
+
the legacy `createSsrServer` pipeline. It now shares the dev/preview path:
|
|
221
|
+
startup banner with Local/Network URLs, busy-port fallback, per-request
|
|
222
|
+
structured logging with `Server-Timing`/`X-Request-ID`, security headers,
|
|
223
|
+
redirects/rewrites/route headers, pluggable ISR cache, user middleware, and
|
|
224
|
+
opt-in streaming SSR. `start` now requires a previous `elur-kit build`
|
|
225
|
+
(it fails fast with a clear message when `dist/` is missing) instead of
|
|
226
|
+
silently rendering everything on demand.
|
|
227
|
+
- **CLI output formatting** — build, dev, preview, and adapter messages
|
|
228
|
+
now use a consistent format (`✓` success, `→` info, `!` warning, `✗`
|
|
229
|
+
error, `[tag]` lifecycle events) with ANSI colors when stdout is a TTY
|
|
230
|
+
(disabled via `NO_COLOR` or when piped; no new dependencies). The build
|
|
231
|
+
summary now includes the total duration plus page/island/file counts.
|
|
232
|
+
The fatal request-error catch in the dev/preview server now logs through
|
|
233
|
+
the structured logger (with `path`, `method`, `error`, `stack`) instead
|
|
234
|
+
of a bare `console.error`.
|
|
235
|
+
- **Dev/preview/start startup banner** — the dev, preview and start servers
|
|
236
|
+
now print an Astro-style banner with the kit name and version and the
|
|
237
|
+
bound URLs:
|
|
238
|
+
`→ Local: http://…` plus `→ Network: http://<LAN-IP>:…` when the
|
|
239
|
+
machine has a non-internal IPv4 address. The banner reflects the port
|
|
240
|
+
actually bound after the busy-port fallback.
|
|
241
|
+
- **Build phase timings and file sizes** — `elur-kit build` now reports
|
|
242
|
+
each phase with a checkmark and its duration (`transform`, `scan`,
|
|
243
|
+
`pages`, `images`, `integrations`, `sitemap`, `manifest`,
|
|
244
|
+
`client bundle`; phases that don't run are omitted) via a new optional
|
|
245
|
+
`BuildConfig.onPhase(name, durationMs)` observer, and lists the
|
|
246
|
+
generated files with their sizes (aligned, Vite-style). With more than
|
|
247
|
+
20 files, only the 10 largest are shown followed by a
|
|
248
|
+
`… and N more` summary. With `--quiet`, the client bundle step is now
|
|
249
|
+
silent too (new `quiet` option on `buildClientBundle`, which also sets
|
|
250
|
+
Vite's `logLevel: "silent"`).
|
|
251
|
+
- **ISR storage backend** — `createWebHandler` no longer uses the legacy
|
|
252
|
+
`getCachedHtml`/`setCachedHtml` module for page caching (it remains
|
|
253
|
+
exported for backward compatibility). Existing on-disk cache entries use
|
|
254
|
+
the same SHA-256-of-pathname key, but entries written by older versions
|
|
255
|
+
lack tag metadata, so they simply miss tag-based invalidation until
|
|
256
|
+
re-rendered. What gets cached (cacheability gates, TTL rules) is
|
|
257
|
+
unchanged.
|
|
258
|
+
|
|
259
|
+
### Fixed
|
|
260
|
+
|
|
261
|
+
- **Dead legacy config lookup removed** — `findConfigFile` carried a legacy
|
|
262
|
+
config-name fallback that could never fire (its file list was identical to
|
|
263
|
+
the preferred one) and referenced pre-rename names that are no longer
|
|
264
|
+
supported. Config files are now exclusively `elur.config.ts/js/mjs`.
|
|
265
|
+
- **Missing type declarations for `@elurjs/kit/cache`** — the subpath
|
|
266
|
+
shipped its runtime bundle but no `index.d.ts`/`index.d.cts` because
|
|
267
|
+
`src/cache/index.ts` was absent from the declaration build inputs
|
|
268
|
+
(publint error). The `./cache` subpath is now fully typed (including
|
|
269
|
+
`redis-adapter`).
|
|
270
|
+
- **Infinite restart loop when the dev port is busy** — with port 3000
|
|
271
|
+
occupied, the dev supervisor restarted the crashed worker forever on the
|
|
272
|
+
same `EADDRINUSE`. The dev and preview servers now retry on the next
|
|
273
|
+
port (up to 20 candidates) with a clear warning
|
|
274
|
+
(`! Puerto 3000 ocupado, usando 3001`), and the startup message shows
|
|
275
|
+
the port actually bound. If the whole range is busy, the worker exits
|
|
276
|
+
with a dedicated code (78) that the supervisor treats as fatal instead
|
|
277
|
+
of restarting. Fallback also applies to an explicit `--port` (with the
|
|
278
|
+
same warning).
|
|
279
|
+
- **Actions defined with `defineAction()` crashed at the action
|
|
280
|
+
endpoint** — `handleActionRequest` invoked actions as `action(...args)`
|
|
281
|
+
without building an `ActionContext`, so any action using the default
|
|
282
|
+
`concurrency: "latest"` failed with `ctx undefined` (`ctx.signal`
|
|
283
|
+
access). Defined actions (detected via their `__elurAction` metadata)
|
|
284
|
+
now receive `(input, ctx)` with `ctx.request`, `ctx.signal` (from the
|
|
285
|
+
request), `ctx.idempotencyKey` (from the `Idempotency-Key` header),
|
|
286
|
+
and empty `params`/`locals`. Legacy plain actions keep the `(...args)`
|
|
287
|
+
call convention unchanged.
|
|
288
|
+
- **Head metadata lost on SPA navigations in production** — the
|
|
289
|
+
production render endpoint (`/__elur-js/render`) only returned
|
|
290
|
+
`{ title, body }`, so pages navigated via the client router kept stale
|
|
291
|
+
`<head>` tags and the consumed action-error cookie was never cleared.
|
|
292
|
+
The endpoint now ships the full payload and the cookie is relayed as
|
|
293
|
+
`Set-Cookie` (HTML mode) / `X-Elur-Action-Clear-Cookie` (JSON mode).
|
|
294
|
+
- **`directive: "load"` did not hydrate on load** — the generated client
|
|
295
|
+
entry wrapped the whole hydration pass in `requestIdleCallback`, so
|
|
296
|
+
`load` islands were really `idle` islands. The entry now calls
|
|
297
|
+
`hydrateIslands()` immediately (module scripts are already deferred);
|
|
298
|
+
`idle`/`visible` keep their deferred scheduling inside the hydrator.
|
|
299
|
+
- **Islands were disposed after the DOM swap** — the generated entry
|
|
300
|
+
listened only for `elur:rendered` (post-swap), so disposers ran on
|
|
301
|
+
detached DOM and could not read live state. Cleanup now runs on
|
|
302
|
+
`elur:before-render`, dispatched before `#app` is replaced, with a
|
|
303
|
+
compat fallback on `elur:rendered` for hosts that only emit the old
|
|
304
|
+
event (e.g. the streaming swap chunk).
|
|
305
|
+
|
|
8
306
|
## [2.4.10]
|
|
9
307
|
|
|
10
308
|
### Fixed
|
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Elur Kit is a framework built on top of [Elur](https://elur.dev/). It brings con
|
|
|
21
21
|
### Key features
|
|
22
22
|
|
|
23
23
|
- **Routing**: file-based with dynamic segments, optional catch-all `[[...slug]]`, route conflict detection, safe URL decoding, redirects/rewrites/route headers
|
|
24
|
-
- **Rendering**: SSG, SSR, ISR with explicit cache policy (public/private/dynamic), streaming with `
|
|
24
|
+
- **Rendering**: SSG, SSR, ISR with explicit cache policy (public/private/dynamic), opt-in streaming SSR with `loading.ts` boundaries (**experimental** — ver [Streaming SSR](#streaming-ssr-experimental))
|
|
25
25
|
- **Build-time compiler** (optional, recommended): integrates [`@elurjs/core-compiler`](https://www.npmjs.com/package/@elurjs/core-compiler) via [`@elurjs/vite-plugin-elur`](https://www.npmjs.com/package/@elurjs/vite-plugin-elur) to lower `html\`\`` templates to imperative DOM code at build time — eliminates `detectContext`, `buildHTML`, and both `TreeWalker` passes in runtime
|
|
26
26
|
- **Partial attribute interpolation**: `class="btn ${size}"` works out of the box via the Vite plugin's state-machine lexer (or the kit's legacy transform as fallback)
|
|
27
27
|
- **Actions**: typed `defineAction()` with input validation, AbortSignal, idempotency, concurrency modes (latest/queue/parallel)
|
|
@@ -160,10 +160,10 @@ Options:
|
|
|
160
160
|
|
|
161
161
|
- **File-based routing** — `src/app/page.ts` maps to URLs with dynamic segments (`[slug]`), catch-all (`[...slug]`), optional catch-all (`[[...slug]]`), route groups `(group)`, and route conflict detection.
|
|
162
162
|
- **SSG, SSR, ISR** — static generation, on-demand SSR, and incremental static regeneration with explicit cache policy (`public`/`private`/`dynamic`), SHA-256 cache keys, atomic writes, single-flight, and tag-based invalidation.
|
|
163
|
-
- **Streaming (experimental)** —
|
|
163
|
+
- **Streaming (experimental)** — real streaming SSR in the unified handler: opt-in via `defineConfig({ streaming: true })`, routes with `loading.ts` stream the shell + fallback immediately and swap in the resolved content with a `<template>` chunk. See [Streaming SSR (experimental)](#streaming-ssr-experimental).
|
|
164
164
|
- **Server actions** — typed `defineAction()` with input validation (`.parse()`), AbortSignal propagation, idempotency metadata, concurrency modes (`latest`/`queue`/`parallel`), and progressive enhancement (plain HTML forms).
|
|
165
165
|
- **RequestContext** — per-request context with `params`, `locals`, `cookies` (CookieJar), `signal` (AbortSignal), `requestId`, `platform`, `route`, and mutable `response` state (headers, Set-Cookie, status). Aligned with runtime-security §4.
|
|
166
|
-
- **Unified Web handler** — `createWebHandler()` is the single entry point for all runtimes (Node, Bun, Vercel, Netlify, Vite dev **y el CLI `dev`/`preview`**). Every runtime is a thin wrapper; no duplicated routing/actions/static pipelines.
|
|
166
|
+
- **Unified Web handler** — `createWebHandler()` is the single entry point for all runtimes (Node, Bun, Vercel, Netlify, Vite dev **y el CLI `dev`/`preview`/`start`**). Every runtime is a thin wrapper; no duplicated routing/actions/static pipelines.
|
|
167
167
|
- **Cache security** — `shouldCachePublic()` rejects requests with cookies/Authorization. `isResultCacheable()` rejects HTML with action error markers. No personalized ISR cache leakage.
|
|
168
168
|
- **Public error sanitization** — production 500s use `toPublicErrorInfo()`/`publicErrorResponse()` (JSON, `no-store`), never exposing stacks, paths or secrets; request id is kept in internal logs.
|
|
169
169
|
- **CSRF protection** — `verifyOrigin()` checks `Origin`, `Referer`, `Host`, and `Sec-Fetch-Site` with allow-list and `strictOrigin` mode.
|
|
@@ -173,8 +173,8 @@ Options:
|
|
|
173
173
|
- **SEO** — sitemap generation from route manifest, sitemap index for >50,000 URLs, robots.txt, JSON-LD with safe escaping (`<`, `>`, `&`, U+2028, U+2029).
|
|
174
174
|
- **Image optimization** — manifest-driven `<picture>` with content-addressed hashed variants, `<source>` per format, real dimensions, no upscales, Sharp optional.
|
|
175
175
|
- **Islands** — lazy `import()` per island, null/error isolation, `load`/`idle`/`visible` directives, auto-scan of `src/islands/`.
|
|
176
|
-
- **Client router** —
|
|
177
|
-
- **Middleware** — `src/middleware.ts` with path matchers, `next()` carries
|
|
176
|
+
- **Client router** — navigation lifecycle events (`elur:navigate-*`, `elur:before-render`), per-page JS gating (0 KB on island-free pages), `data-elur-persist` element persistence, bounded LRU prefetch cache (network-aware, `pointerdown`), `history.scrollRestoration = "manual"`, head/payload/script refresh on navigation, optional idiomorph morphing (`router.morph`), optional loading indicator, Speculation Rules (`router.speculation`), head/assets merge, aria-live announcer, canonical URL, View Transitions with reduced-motion fallback.
|
|
177
|
+
- **Middleware** — `src/middleware.ts` with path matchers, `next()` carries headers/locals into the request pipeline, cleanup in `finally`, runs in `dev`/`preview`/`start` (the unified Web handler).
|
|
178
178
|
- **Integrations** — typed hooks for `elur-i18n`, `elur-auth`, `elur-query`, `elur-testing` without adding them as dependencies.
|
|
179
179
|
- **CLI** — `dev`, `build`, `preview`, `start`, `check`, `routes`, `doctor`, `adapter` with reliable exit codes.
|
|
180
180
|
- **Observability** — structured logger with request ID, Server-Timing, sensitive data redaction (cookies, auth, tokens).
|
|
@@ -183,6 +183,89 @@ Options:
|
|
|
183
183
|
- **`throw new Response()`** — first-class HTTP control flow from loaders and layout loaders (redirects, 404, etc.).
|
|
184
184
|
- **HMAC-signed action errors** — action error cookies signed with SHA-256, rejects tampered/forged values.
|
|
185
185
|
|
|
186
|
+
## Streaming SSR (experimental)
|
|
187
|
+
|
|
188
|
+
Real streaming for dynamic routes, wired into the unified Web handler
|
|
189
|
+
(`createWebHandler`) — so it works identically in `dev`, `preview`, `start`
|
|
190
|
+
and the generated Node/Bun servers.
|
|
191
|
+
|
|
192
|
+
### Enabling it
|
|
193
|
+
|
|
194
|
+
Streaming is **opt-in** (default `false` — every route renders buffered
|
|
195
|
+
exactly as before unless you enable it):
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
// elur.config.ts
|
|
199
|
+
import { defineConfig } from "@elurjs/kit";
|
|
200
|
+
|
|
201
|
+
export default defineConfig({
|
|
202
|
+
streaming: true,
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Requirements:
|
|
207
|
+
|
|
208
|
+
- The route must define a **loading boundary** (`src/app/<route>/loading.ts`).
|
|
209
|
+
Routes without one render buffered, unchanged.
|
|
210
|
+
- The host adapter must declare `capabilities.streaming: true`. Node, Bun and
|
|
211
|
+
the CLI dev/preview servers do; hosts that cannot stream fall back to
|
|
212
|
+
buffered rendering automatically instead of breaking. The CLI `adapter`
|
|
213
|
+
command validates the combination at build time.
|
|
214
|
+
|
|
215
|
+
### What you get
|
|
216
|
+
|
|
217
|
+
1. The browser receives the **document shell + loading fallback immediately**
|
|
218
|
+
(low TTFB), with status 200 and `Content-Type: text/html; charset=utf-8`.
|
|
219
|
+
2. The page render (loaders included) runs **in the background**.
|
|
220
|
+
3. When it finishes, the server appends a `<template>` chunk plus a small
|
|
221
|
+
script that **swaps the loading boundary in-place** (`replaceWith`), then
|
|
222
|
+
dispatches `elur:rendered`. No full re-render, no client fetch.
|
|
223
|
+
|
|
224
|
+
Emitted headers on streamed responses:
|
|
225
|
+
|
|
226
|
+
| Header | Why |
|
|
227
|
+
| --- | --- |
|
|
228
|
+
| `Content-Type: text/html; charset=utf-8` | sent early so the browser parses progressively |
|
|
229
|
+
| `Transfer-Encoding: chunked` | no `Content-Length`; each chunk flushes as produced |
|
|
230
|
+
| `X-Accel-Buffering: no` | tells nginx and similar reverse proxies not to buffer the stream |
|
|
231
|
+
| `Cache-Control: no-store` | a half-sent dynamic stream must never be cached by intermediaries |
|
|
232
|
+
|
|
233
|
+
The usual observability headers (`X-Request-ID`, `Server-Timing`,
|
|
234
|
+
security headers, route headers) apply as always. Note that `Server-Timing`'s
|
|
235
|
+
`ssr` metric measures **time-to-shell**, not the full background render.
|
|
236
|
+
|
|
237
|
+
### Redirects, errors and disconnects mid-stream
|
|
238
|
+
|
|
239
|
+
- A loader throwing a redirect `Response` emits a
|
|
240
|
+
`<script>window.location.href=…</script>` chunk — the browser navigates
|
|
241
|
+
even though the shell was already sent.
|
|
242
|
+
- A loader failure after the shell went out swaps the boundary for a sober,
|
|
243
|
+
inline-styled error notice (`role="alert"`) and logs the detail via
|
|
244
|
+
`console.error`.
|
|
245
|
+
- If the client disconnects, the request's `AbortSignal` cancels the stream;
|
|
246
|
+
a render completing late is discarded and never writes to a dead stream.
|
|
247
|
+
The Node server also cancels the upstream stream when the socket closes.
|
|
248
|
+
|
|
249
|
+
### Interaction with the ISR cache
|
|
250
|
+
|
|
251
|
+
**Streamed pages bypass the ISR cache entirely** — they are not read from it
|
|
252
|
+
nor written to it, and render live on every request. Nobody caches a stream
|
|
253
|
+
mid-flight; if a route is cacheable, buffered + ISR is the better delivery
|
|
254
|
+
mode. Keep `streaming: false` (or remove the `loading.ts` boundary) for
|
|
255
|
+
routes you want cached.
|
|
256
|
+
|
|
257
|
+
### Limitations
|
|
258
|
+
|
|
259
|
+
- **Experimental**: the streaming/buffered parity matrix across hosts is not
|
|
260
|
+
complete yet, and the API may change.
|
|
261
|
+
- The swap script is inline: a strict `Content-Security-Policy` without
|
|
262
|
+
`unsafe-inline` (or a nonce) blocks it. Configure CSP accordingly.
|
|
263
|
+
- Verified on Node (dev/preview/`start`, generated Node server) and Bun
|
|
264
|
+
(`Bun.serve` streams natively). The Vercel/Netlify adapters are out of
|
|
265
|
+
scope for now.
|
|
266
|
+
- Behind a reverse proxy, make sure buffering and gzip buffering are disabled
|
|
267
|
+
for streamed routes (the `X-Accel-Buffering: no` header covers nginx).
|
|
268
|
+
|
|
186
269
|
## What's new in v2.4
|
|
187
270
|
|
|
188
271
|
- **Fixed: image pipeline silently no-op** — the CLI bundle was
|
|
@@ -234,6 +317,14 @@ Options:
|
|
|
234
317
|
|
|
235
318
|
#### Using the `build` hook for sitemaps
|
|
236
319
|
|
|
320
|
+
When `site` is set in your config, the build **already generates
|
|
321
|
+
`sitemap.xml` automatically** from the scanned routes (dynamic routes,
|
|
322
|
+
error pages and internal namespaces excluded; sites over 50,000 URLs are
|
|
323
|
+
split into a sitemap index). You only need the manual hook below for full
|
|
324
|
+
control over the entries (per-URL `priority`/`changefreq`, dynamic-route
|
|
325
|
+
URLs, extra files). An existing `sitemap.xml` — from `public/` or written
|
|
326
|
+
by an integration — always takes precedence over the automatic one.
|
|
327
|
+
|
|
237
328
|
```ts
|
|
238
329
|
// elur.config.ts
|
|
239
330
|
import { defineConfig } from "@elurjs/kit";
|
|
@@ -1291,16 +1382,111 @@ export const config = {
|
|
|
1291
1382
|
};
|
|
1292
1383
|
```
|
|
1293
1384
|
|
|
1294
|
-
|
|
1385
|
+
The middleware runs in `dev`, `preview` and `start` through the unified Web
|
|
1386
|
+
handler: after redirects/rewrites and the internal endpoints, before routing.
|
|
1387
|
+
Returning a `Response` short-circuits the pipeline (security headers and
|
|
1388
|
+
`X-Request-ID` still apply); `next({ headers })` merges headers into the
|
|
1389
|
+
downstream request and `next({ locals })` is exposed to API routes as
|
|
1390
|
+
`ctx.locals`. The generated Node/Bun adapter servers do not run the
|
|
1391
|
+
middleware file yet.
|
|
1295
1392
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1393
|
+
### Redirects, rewrites and route headers
|
|
1394
|
+
|
|
1395
|
+
Declare redirects, rewrites and extra response headers in `elur.config.ts`.
|
|
1396
|
+
All patterns support `:param` segments, `*` wildcards and `:name*` catch-alls,
|
|
1397
|
+
with `:param` interpolation in destinations:
|
|
1398
|
+
|
|
1399
|
+
```ts
|
|
1400
|
+
import { defineConfig } from "@elurjs/kit";
|
|
1401
|
+
|
|
1402
|
+
export default defineConfig({
|
|
1403
|
+
redirects: [
|
|
1404
|
+
{ from: "/old-blog/:slug", to: "/blog/:slug", status: 301 },
|
|
1405
|
+
],
|
|
1406
|
+
rewrites: [
|
|
1407
|
+
{ from: "/api/legacy/*", to: "/api/v2/*" },
|
|
1408
|
+
],
|
|
1409
|
+
headers: [
|
|
1410
|
+
{ path: "/admin/*", headers: { "X-Robots-Tag": "noindex" } },
|
|
1411
|
+
],
|
|
1412
|
+
});
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
Redirects are evaluated before any routing (default status `308`). Rewrites
|
|
1416
|
+
transparently change the pathname used for routing — API routes, static files
|
|
1417
|
+
and SSR all see the rewritten path. Route headers are matched against the
|
|
1418
|
+
original request path and applied to the response.
|
|
1419
|
+
|
|
1420
|
+
### Router, prefetch and navigation lifecycle
|
|
1421
|
+
|
|
1422
|
+
The SPA router prefetches pages on `pointerenter`, `focus` and `pointerdown`
|
|
1423
|
+
(the "tap" strategy — earlier than the click on touch devices), and on links
|
|
1424
|
+
that opt into viewport prefetching with `data-prefetch="viewport"`
|
|
1425
|
+
(IntersectionObserver, 200px root margin). Prefetched pages live in a
|
|
1426
|
+
bounded LRU cache (32 entries, 30 s TTL). Prefetching is skipped on
|
|
1427
|
+
constrained networks — `Save-Data` or `slow-2g`/`2g` effective types —
|
|
1428
|
+
unless a link forces it with `data-prefetch="always"`. Add
|
|
1429
|
+
`data-no-prefetch` to any link to opt out entirely.
|
|
1430
|
+
|
|
1431
|
+
Navigation lifecycle events (on `document`) carry
|
|
1432
|
+
`{ pathname, search, fromCache, popstate }`:
|
|
1433
|
+
|
|
1434
|
+
- `elur:navigate-start` — a navigation begins.
|
|
1435
|
+
- `elur:navigate-end` — the DOM swap completed.
|
|
1436
|
+
- `elur:navigate-error` — the navigation failed (e.g. render endpoint
|
|
1437
|
+
unavailable); the router falls back to a full page load.
|
|
1438
|
+
- `elur:before-render` — fires *before* the `#app` swap; the generated
|
|
1439
|
+
entry disposes islands here (except islands inside persisted nodes,
|
|
1440
|
+
listed in `detail.persisted`).
|
|
1441
|
+
- `elur:rendered` — fires after the swap; islands re-hydrate here.
|
|
1442
|
+
|
|
1443
|
+
Elements marked `data-elur-persist="key"` survive SPA navigations: the
|
|
1444
|
+
router moves the *same live DOM node* into its position in the new page
|
|
1445
|
+
(matched by the attribute value), using `Element.moveBefore()` when
|
|
1446
|
+
available — media playback, island state and scroll positions are
|
|
1447
|
+
preserved. Islands inside persisted subtrees are not disposed/rehydrated;
|
|
1448
|
+
if their incoming props differ, an `elur:persist-props-changed` event
|
|
1449
|
+
bubbles from the island marker.
|
|
1450
|
+
|
|
1451
|
+
Scroll is managed by the router (`history.scrollRestoration = "manual"`):
|
|
1452
|
+
the position is saved per history entry and restored on back/forward.
|
|
1299
1453
|
|
|
1300
1454
|
When the browser supports the View Transitions API, page transitions use
|
|
1301
1455
|
`document.startViewTransition()` for smooth cross-fade animations. This is
|
|
1302
1456
|
automatically disabled when the user has `prefers-reduced-motion: reduce`.
|
|
1303
1457
|
|
|
1458
|
+
Router options (`defineConfig`):
|
|
1459
|
+
|
|
1460
|
+
```ts
|
|
1461
|
+
export default defineConfig({
|
|
1462
|
+
router: {
|
|
1463
|
+
enabled: true, // SPA navigation; false → MPA + 0 KB JS on island-free pages
|
|
1464
|
+
prefetch: true, // link prefetching (network-aware)
|
|
1465
|
+
morph: false, // experimental: idiomorph DOM morphing for the swap
|
|
1466
|
+
loadingIndicator: false,// top progress bar on navigations slower than ~200 ms
|
|
1467
|
+
speculation: undefined, // "prefetch" | "prerender" → Speculation Rules on static pages
|
|
1468
|
+
},
|
|
1469
|
+
js: "modern", // "legacy" restores the unconditional combined entry
|
|
1470
|
+
});
|
|
1471
|
+
```
|
|
1472
|
+
|
|
1473
|
+
### Per-page JavaScript emission
|
|
1474
|
+
|
|
1475
|
+
With `js: "modern"` (default) the document shell inspects the rendered body
|
|
1476
|
+
and only ships what the page needs:
|
|
1477
|
+
|
|
1478
|
+
| Page contents | `router.enabled: true` | `router.enabled: false` |
|
|
1479
|
+
|---|---|---|
|
|
1480
|
+
| With islands | `entry-client.js` + `router.js` | `entry-client.js` only |
|
|
1481
|
+
| Without islands | `router.js` only | **0 KB of JS** |
|
|
1482
|
+
|
|
1483
|
+
The split requires the router to be its own bundle chunk — automatic when
|
|
1484
|
+
the project has no `vite.client.config.*` (the CLI synthesizes a default
|
|
1485
|
+
two-input config) or when that config declares `.elur/router.ts` as a
|
|
1486
|
+
second input. A single-input config keeps the router embedded in
|
|
1487
|
+
`entry-client.js` (emitted whenever islands exist *or* the router is on).
|
|
1488
|
+
Set `js: "legacy"` to restore the pre-split behavior unconditionally.
|
|
1489
|
+
|
|
1304
1490
|
## Project conventions
|
|
1305
1491
|
|
|
1306
1492
|
```text
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../action-
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../action-BiE4tEty.cjs");exports.callAction=e.t,exports.defineAction=e.r,exports.elurJsAction=e.n;
|
package/dist/lib/action/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as e, r as t, t as n } from "../action-
|
|
1
|
+
import { n as e, r as t, t as n } from "../action-CXVk8rWt.js";
|
|
2
2
|
export { n as callAction, t as defineAction, e as elurJsAction };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./errors-
|
|
2
|
-
//# sourceMappingURL=action-
|
|
1
|
+
const e=require("./errors-kSl9yLkg.cjs");let t=require("@elurjs/core");var n=globalThis.AbortController;function r(e,t){let r=e.concurrency??`latest`,a=e.idempotent??!1,o=e.invalidateTags??[],s=e.invalidatePaths??[],c=null,l=[],u=0;async function d(n,r){if(e.input)try{n=e.input.parse(n)}catch(e){let t=e instanceof Error?e.message:String(e),{fail:n}=await Promise.resolve().then(()=>require("./errors-kSl9yLkg.cjs")).then(e=>e.r);return n(400,{validation:t})}return t(n,r)}let f=async(e,t)=>{if(r===`latest`){c&&c.abort(),c=new n;let r=i(t.signal,c.signal);return d(e,{...t,signal:r})}if(r===`queue`){u>0&&await new Promise(e=>l.push(e)),u++;try{return await d(e,t)}finally{u--;let e=l.shift();e&&e()}}return d(e,t)};return f.__elurAction={name:t.name||`anonymous`,concurrency:r,idempotent:a,invalidateTags:o,invalidatePaths:s},f}function i(...e){let t=new n;for(let n of e)if(n){if(n.aborted){t.abort();break}n.addEventListener(`abort`,()=>t.abort(),{once:!0})}return t.signal}function a(e){return typeof e==`object`&&!!e&&e.__elur_js_action_failure===!0}function o(e){return typeof e==`object`&&!!e&&e.__elur_js_action_redirect===!0}async function s(t,n=[],r={}){let i=Array.isArray(n)?n:[n],s=await fetch(`/__elur-js/actions`,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({name:t,page:r.page,args:i})}),c=await s.text();if(!s.ok){let n;try{n=JSON.parse(c)}catch{}if(a(n)&&n.status!==void 0)return new e.t(n.status,n.data);throw Error(`Action "${t}" failed: ${c}`)}let l=JSON.parse(c);return o(l)&&l.status!==void 0&&l.location!==void 0?new e.n(l.status,l.location):l}function c(e,n={}){let r=(0,t.signal)(!1),i=(0,t.signal)(null),a=(0,t.signal)(null);async function o(t){r.value=!0,i.value=null;try{let r=await s(e,t,n);return a.value=r,r}catch(e){throw i.value=e instanceof Error?e:Error(String(e)),e}finally{r.value=!1}}return{submit:o,pending:r,data:a,error:i}}Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return c}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return s}});
|
|
2
|
+
//# sourceMappingURL=action-BiE4tEty.cjs.map
|