@elurjs/kit 2.4.10 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +313 -0
- package/README.md +248 -15
- package/dist/lib/action/index.cjs +1 -1
- package/dist/lib/action/index.js +1 -1
- package/dist/lib/{action-DZHihU3Z.cjs → action-BiE4tEty.cjs} +2 -2
- package/dist/lib/{action-DZHihU3Z.cjs.map → action-BiE4tEty.cjs.map} +1 -1
- package/dist/lib/{action-BB6N5yD-.js → action-CXVk8rWt.js} +3 -3
- package/dist/lib/{action-BB6N5yD-.js.map → action-CXVk8rWt.js.map} +1 -1
- package/dist/lib/adapter-DNsjXvq5.js +160 -0
- package/dist/lib/adapter-DNsjXvq5.js.map +1 -0
- package/dist/lib/adapter-Dz2hzR36.cjs +3 -0
- package/dist/lib/adapter-Dz2hzR36.cjs.map +1 -0
- package/dist/lib/adapters/bun.cjs +2 -2
- package/dist/lib/adapters/bun.cjs.map +1 -1
- package/dist/lib/adapters/bun.js +17 -9
- package/dist/lib/adapters/bun.js.map +1 -1
- package/dist/lib/adapters/index.d.cts +34 -0
- package/dist/lib/adapters/index.d.ts +34 -0
- package/dist/lib/adapters/netlify.cjs +1 -1
- package/dist/lib/adapters/netlify.js +7 -7
- package/dist/lib/adapters/node.cjs +5 -5
- package/dist/lib/adapters/node.cjs.map +1 -1
- package/dist/lib/adapters/node.js +20 -12
- package/dist/lib/adapters/node.js.map +1 -1
- package/dist/lib/adapters/vercel.cjs +1 -1
- package/dist/lib/adapters/vercel.js +8 -8
- package/dist/lib/build/build.d.cts +40 -0
- package/dist/lib/build/build.d.ts +40 -0
- package/dist/lib/build/document-shell.d.cts +47 -1
- package/dist/lib/build/document-shell.d.ts +47 -1
- package/dist/lib/build/vite-build.d.cts +25 -2
- package/dist/lib/build/vite-build.d.ts +25 -2
- package/dist/lib/cache/adapter.d.cts +41 -0
- package/dist/lib/cache/adapter.d.ts +41 -0
- package/dist/lib/cache/index.cjs +2 -0
- package/dist/lib/cache/index.cjs.map +1 -0
- package/dist/lib/cache/index.d.cts +5 -0
- package/dist/lib/cache/index.d.ts +5 -0
- package/dist/lib/cache/index.js +113 -0
- package/dist/lib/cache/index.js.map +1 -0
- package/dist/lib/cache/invalidation.d.cts +36 -0
- package/dist/lib/cache/invalidation.d.ts +36 -0
- package/dist/lib/cache/redis-adapter.d.cts +49 -0
- package/dist/lib/cache/redis-adapter.d.ts +49 -0
- package/dist/lib/cache-DGhTYyqh.js +50 -0
- package/dist/lib/cache-DGhTYyqh.js.map +1 -0
- package/dist/lib/cache-X1n4Ossu.cjs +2 -0
- package/dist/lib/cache-X1n4Ossu.cjs.map +1 -0
- package/dist/lib/cli/output.d.cts +58 -0
- package/dist/lib/cli/output.d.ts +58 -0
- package/dist/lib/cli/ports.d.cts +20 -0
- package/dist/lib/cli/ports.d.ts +20 -0
- package/dist/lib/cli.cjs +2559 -1488
- package/dist/lib/cli.cjs.map +1 -1
- package/dist/lib/cli.d.cts +21 -0
- package/dist/lib/cli.d.ts +21 -0
- package/dist/lib/cli.js +2558 -1488
- package/dist/lib/cli.js.map +1 -1
- package/dist/lib/config/index.cjs +1 -1
- package/dist/lib/config/index.cjs.map +1 -1
- package/dist/lib/config/index.d.cts +79 -0
- package/dist/lib/config/index.d.ts +79 -0
- package/dist/lib/config/index.js +21 -14
- package/dist/lib/config/index.js.map +1 -1
- package/dist/lib/{errors-ePYxDlhU.js → errors-j4cg9VN3.js} +24 -30
- package/dist/lib/{errors-ePYxDlhU.js.map → errors-j4cg9VN3.js.map} +1 -1
- package/dist/lib/errors-kSl9yLkg.cjs +2 -0
- package/dist/lib/{errors-0X7pCFNU.cjs.map → errors-kSl9yLkg.cjs.map} +1 -1
- package/dist/lib/hydrate-BSLuI0Nh.cjs +2 -0
- package/dist/lib/hydrate-BSLuI0Nh.cjs.map +1 -0
- package/dist/lib/hydrate-BaS7-d8C.js +133 -0
- package/dist/lib/hydrate-BaS7-d8C.js.map +1 -0
- package/dist/lib/idiomorph.esm-BOOTOg1r.cjs +2 -0
- package/dist/lib/idiomorph.esm-BOOTOg1r.cjs.map +1 -0
- package/dist/lib/idiomorph.esm-CXuY-EHB.js +371 -0
- package/dist/lib/idiomorph.esm-CXuY-EHB.js.map +1 -0
- package/dist/lib/index.cjs +1 -1
- package/dist/lib/index.cjs.map +1 -1
- package/dist/lib/index.d.cts +8 -6
- package/dist/lib/index.d.ts +8 -6
- package/dist/lib/index.js +1 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/interpolation-plugin-C0dTd69H.cjs +58 -0
- package/dist/lib/interpolation-plugin-C0dTd69H.cjs.map +1 -0
- package/dist/lib/{interpolation-plugin-Wgb1j4pT.js → interpolation-plugin-K2qE69tF.js} +95 -126
- package/dist/lib/interpolation-plugin-K2qE69tF.js.map +1 -0
- package/dist/lib/invalidation-CFpNQUv6.cjs +2 -0
- package/dist/lib/invalidation-CFpNQUv6.cjs.map +1 -0
- package/dist/lib/invalidation-DXhdE9wj.js +64 -0
- package/dist/lib/invalidation-DXhdE9wj.js.map +1 -0
- package/dist/lib/island/generate-entry.d.cts +40 -2
- package/dist/lib/island/generate-entry.d.ts +40 -2
- package/dist/lib/island/hydrate.d.cts +19 -6
- package/dist/lib/island/hydrate.d.ts +19 -6
- package/dist/lib/island/index.cjs +1 -1
- package/dist/lib/island/index.d.cts +2 -1
- package/dist/lib/island/index.d.ts +2 -1
- package/dist/lib/island/index.js +1 -1
- package/dist/lib/island/island.d.cts +15 -0
- package/dist/lib/island/island.d.ts +15 -0
- package/dist/lib/island-CTBnsOhI.js +59 -0
- package/dist/lib/island-CTBnsOhI.js.map +1 -0
- package/dist/lib/island-kfQEKRe_.cjs +2 -0
- package/dist/lib/island-kfQEKRe_.cjs.map +1 -0
- package/dist/lib/node-http-DVFcYeBS.js +723 -0
- package/dist/lib/node-http-DVFcYeBS.js.map +1 -0
- package/dist/lib/node-http-Uct7DhmN.cjs +13 -0
- package/dist/lib/node-http-Uct7DhmN.cjs.map +1 -0
- package/dist/lib/rolldown-runtime-BIInC43l.cjs +1 -0
- package/dist/lib/rolldown-runtime-Dy4uBu1J.js +11 -0
- package/dist/lib/router/client.cjs +1 -1
- package/dist/lib/router/client.cjs.map +1 -1
- package/dist/lib/router/client.d.cts +55 -8
- package/dist/lib/router/client.d.ts +55 -8
- package/dist/lib/router/client.js +298 -97
- package/dist/lib/router/client.js.map +1 -1
- package/dist/lib/router/redirects.d.cts +34 -0
- package/dist/lib/router/redirects.d.ts +34 -0
- package/dist/lib/runtime/handler.d.cts +59 -0
- package/dist/lib/runtime/handler.d.ts +59 -0
- package/dist/lib/runtime/index.cjs +1 -1
- package/dist/lib/runtime/index.d.cts +2 -1
- package/dist/lib/runtime/index.d.ts +2 -1
- package/dist/lib/runtime/index.js +4 -4
- package/dist/lib/runtime/logger.d.cts +47 -0
- package/dist/lib/runtime/logger.d.ts +47 -0
- package/dist/lib/runtime/node-http.d.cts +11 -1
- package/dist/lib/runtime/node-http.d.ts +11 -1
- package/dist/lib/runtime-ChE4flX3.cjs +2 -0
- package/dist/lib/runtime-ChE4flX3.cjs.map +1 -0
- package/dist/lib/runtime-OMyz5yE-.js +905 -0
- package/dist/lib/runtime-OMyz5yE-.js.map +1 -0
- package/dist/lib/seo/index.cjs +1 -10
- package/dist/lib/seo/index.d.cts +1 -0
- package/dist/lib/seo/index.d.ts +1 -0
- package/dist/lib/seo/index.js +2 -41
- package/dist/lib/seo/sitemap-from-routes.d.cts +24 -0
- package/dist/lib/seo/sitemap-from-routes.d.ts +24 -0
- package/dist/lib/{shared-CfqOLsKj.cjs → shared-CP0Dc3cp.cjs} +72 -47
- package/dist/lib/shared-CP0Dc3cp.cjs.map +1 -0
- package/dist/lib/{shared-DU3iQgr7.js → shared-D8w-neq2.js} +87 -58
- package/dist/lib/shared-D8w-neq2.js.map +1 -0
- package/dist/lib/sitemap-from-routes-CaBoiGvE.js +77 -0
- package/dist/lib/sitemap-from-routes-CaBoiGvE.js.map +1 -0
- package/dist/lib/sitemap-from-routes-DbcLLL4G.cjs +13 -0
- package/dist/lib/sitemap-from-routes-DbcLLL4G.cjs.map +1 -0
- package/dist/lib/ssr/render.d.cts +8 -2
- package/dist/lib/ssr/render.d.ts +8 -2
- package/dist/lib/ssr/server.d.cts +7 -0
- package/dist/lib/ssr/server.d.ts +7 -0
- package/dist/lib/ssr/stream-response.d.cts +30 -0
- package/dist/lib/ssr/stream-response.d.ts +30 -0
- package/dist/lib/ssr/stream.d.cts +16 -1
- package/dist/lib/ssr/stream.d.ts +16 -1
- package/dist/lib/{stream-DmZjYqHm.cjs → stream-Cg-xYG82.cjs} +3 -3
- package/dist/lib/stream-Cg-xYG82.cjs.map +1 -0
- package/dist/lib/stream-D-ytogdu.js +106 -0
- package/dist/lib/stream-D-ytogdu.js.map +1 -0
- package/dist/lib/vite/index.cjs +1 -1
- package/dist/lib/vite/index.cjs.map +1 -1
- package/dist/lib/vite/index.d.cts +11 -0
- package/dist/lib/vite/index.d.ts +11 -0
- package/dist/lib/vite/index.js +101 -94
- package/dist/lib/vite/index.js.map +1 -1
- package/package.json +25 -8
- package/dist/lib/errors-0X7pCFNU.cjs +0 -2
- package/dist/lib/hydrate-CJbUWdXu.js +0 -126
- package/dist/lib/hydrate-CJbUWdXu.js.map +0 -1
- package/dist/lib/hydrate-D6MR24mz.cjs +0 -2
- package/dist/lib/hydrate-D6MR24mz.cjs.map +0 -1
- package/dist/lib/interpolation-plugin-TWDch8nn.cjs +0 -39
- package/dist/lib/interpolation-plugin-TWDch8nn.cjs.map +0 -1
- package/dist/lib/interpolation-plugin-Wgb1j4pT.js.map +0 -1
- package/dist/lib/node-http-DRAUhO0c.js +0 -617
- package/dist/lib/node-http-DRAUhO0c.js.map +0 -1
- package/dist/lib/node-http-DhxguYyz.cjs +0 -13
- package/dist/lib/node-http-DhxguYyz.cjs.map +0 -1
- package/dist/lib/runtime-CrH5X-Es.cjs +0 -2
- package/dist/lib/runtime-CrH5X-Es.cjs.map +0 -1
- package/dist/lib/runtime-Ds2-wH6P.js +0 -520
- package/dist/lib/runtime-Ds2-wH6P.js.map +0 -1
- package/dist/lib/seo/index.cjs.map +0 -1
- package/dist/lib/seo/index.js.map +0 -1
- package/dist/lib/shared-CfqOLsKj.cjs.map +0 -1
- package/dist/lib/shared-DU3iQgr7.js.map +0 -1
- package/dist/lib/stream-CpOG052V.js +0 -104
- package/dist/lib/stream-CpOG052V.js.map +0 -1
- package/dist/lib/stream-DmZjYqHm.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@elurjs/kit)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
> Full-stack framework for Elur — file-based routing, SSG, SSR, ISR, streaming, islands, actions, content collections, cache adapters, and SPA-like navigation. Zero extra runtime dependencies on the client: Elur stays at ~15 KB. Optional build-time compiler lowers `
|
|
6
|
+
> Full-stack framework for Elur — file-based routing, SSG, SSR, ISR, streaming, islands, actions, content collections, cache adapters, and SPA-like navigation. Zero extra runtime dependencies on the client: Elur stays at ~15 KB. Optional build-time compiler lowers ``html` `` templates to imperative DOM code for ~25–44% faster renders.
|
|
7
7
|
|
|
8
8
|
## What is Elur Kit?
|
|
9
9
|
|
|
@@ -21,8 +21,8 @@ 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 `
|
|
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 `
|
|
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
|
+
- **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)
|
|
28
28
|
- **Cache**: `CacheAdapter` with filesystem, Redis, and Cloudflare KV storage, SHA-256 keys, atomic writes, single-flight, stale-while-revalidate, tag-based invalidation
|
|
@@ -48,7 +48,7 @@ npm install @elurjs/vite-plugin-elur
|
|
|
48
48
|
|
|
49
49
|
The plugin is an optional peer dependency. When installed, it activates:
|
|
50
50
|
|
|
51
|
-
- **Build-time compiler** — lowers `
|
|
51
|
+
- **Build-time compiler** — lowers ``html` `` to imperative DOM code
|
|
52
52
|
- **Partial attribute interpolation** — state-machine lexer (replaces the kit's legacy transform)
|
|
53
53
|
- **HMR with state preservation** — signals, stores, forms, routers survive hot updates
|
|
54
54
|
- **Scroll/focus preservation** — restored after re-mount
|
|
@@ -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,135 @@ 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
|
+
|
|
269
|
+
## What's new in v2.5
|
|
270
|
+
|
|
271
|
+
**0% JavaScript by default + next-generation router.**
|
|
272
|
+
|
|
273
|
+
- **Per-page JS gating** — the document shell scans the rendered body
|
|
274
|
+
for `data-elur-island` markers: pages without islands ship **0 KB of
|
|
275
|
+
JS** when `router.enabled: false`, or **only the router chunk** when
|
|
276
|
+
the router is on. Pages with islands get `entry-client.js` +
|
|
277
|
+
`router.js`, each with `<link rel="modulepreload">`. Escape hatch:
|
|
278
|
+
`defineConfig({ js: "legacy" })` restores the unconditional combined
|
|
279
|
+
entry. See [Per-page JavaScript emission](#per-page-javascript-emission).
|
|
280
|
+
- **Split client entries** — the generated entry is hydrate-only and
|
|
281
|
+
the router lives in `.elur/router.ts` → `/_elur/router.js`.
|
|
282
|
+
Single-input `vite.client.config.*` files keep working (router
|
|
283
|
+
embedded = legacy mode de facto).
|
|
284
|
+
- **Production head fix** — `/__elur-js/render` now returns
|
|
285
|
+
`{ title, body, head, data, actions, clearActionErrorCookie }` in
|
|
286
|
+
every runtime; head metadata updates on SPA navigations and the
|
|
287
|
+
action-error cookie is actually cleared.
|
|
288
|
+
- **Navigation lifecycle events** — `elur:navigate-start` /
|
|
289
|
+
`elur:navigate-end` / `elur:navigate-error` with
|
|
290
|
+
`{ pathname, search, fromCache, popstate }`, plus
|
|
291
|
+
`elur:before-render` before the swap (islands dispose while still
|
|
292
|
+
attached; `detail.persisted` lists survivors).
|
|
293
|
+
- **`data-elur-persist="key"`** — live DOM nodes are moved (not
|
|
294
|
+
re-rendered) across navigations via `Element.moveBefore()`
|
|
295
|
+
(`replaceWith` fallback); islands inside keep their state.
|
|
296
|
+
- **Router robustness** — bounded LRU prefetch cache (32 entries,
|
|
297
|
+
30 s), `history.scrollRestoration = "manual"` with per-entry scroll,
|
|
298
|
+
prefetch on `pointerenter`/`focus`/`pointerdown` + opt-in viewport
|
|
299
|
+
(`data-prefetch="viewport"`), Save-Data/2g-aware, inline `<script>`
|
|
300
|
+
re-execution after navigation, `#elur-data`/`#elur-actions` refresh.
|
|
301
|
+
- **Optional extras** — `router.morph` (idiomorph-based DOM morphing,
|
|
302
|
+
experimental), `router.loadingIndicator` (top progress bar, >200 ms
|
|
303
|
+
delay, reduced-motion aware), `router.speculation` (Speculation
|
|
304
|
+
Rules API on static pages).
|
|
305
|
+
- **`directive: "load"` is real now** — the generated entry calls
|
|
306
|
+
`hydrateIslands()` immediately; the global `requestIdleCallback`
|
|
307
|
+
wrapper is gone. `idle`/`visible` keep their deferred scheduling.
|
|
308
|
+
- **`startClientRouter(options)`** accepts
|
|
309
|
+
`{ prefetch, morph, loadingIndicator }`; `prefetch()` gains
|
|
310
|
+
`{ force: true }` to bypass network guards.
|
|
311
|
+
|
|
312
|
+
All changes are backwards-compatible; existing projects keep working
|
|
313
|
+
unchanged.
|
|
314
|
+
|
|
186
315
|
## What's new in v2.4
|
|
187
316
|
|
|
188
317
|
- **Fixed: image pipeline silently no-op** — the CLI bundle was
|
|
@@ -232,7 +361,15 @@ Options:
|
|
|
232
361
|
SSR errors are never silently swallowed — they propagate wrapped with
|
|
233
362
|
the island name and remediation hints.
|
|
234
363
|
|
|
235
|
-
|
|
364
|
+
### Using the `build` hook for sitemaps
|
|
365
|
+
|
|
366
|
+
When `site` is set in your config, the build **already generates
|
|
367
|
+
`sitemap.xml` automatically** from the scanned routes (dynamic routes,
|
|
368
|
+
error pages and internal namespaces excluded; sites over 50,000 URLs are
|
|
369
|
+
split into a sitemap index). You only need the manual hook below for full
|
|
370
|
+
control over the entries (per-URL `priority`/`changefreq`, dynamic-route
|
|
371
|
+
URLs, extra files). An existing `sitemap.xml` — from `public/` or written
|
|
372
|
+
by an integration — always takes precedence over the automatic one.
|
|
236
373
|
|
|
237
374
|
```ts
|
|
238
375
|
// elur.config.ts
|
|
@@ -305,7 +442,7 @@ When both plugins are installed:
|
|
|
305
442
|
| Feature | Kit only | Kit + Vite plugin |
|
|
306
443
|
| --- | --- | --- |
|
|
307
444
|
| Partial attr interpolation | Legacy transform (heuristic) | State-machine lexer (compile-time) |
|
|
308
|
-
| Build-time compiler | No | Yes (`
|
|
445
|
+
| Build-time compiler | No | Yes (``html` `` → imperative DOM) |
|
|
309
446
|
| HMR state preservation | No | Yes (signals, stores, forms, routers) |
|
|
310
447
|
| Scroll/focus preservation | No | Yes |
|
|
311
448
|
| SSR | Works (kit handles it) | Works (plugin skips compiler in SSR) |
|
|
@@ -423,6 +560,7 @@ receive the full transform pipeline.
|
|
|
423
560
|
| v2.4.2 | Integration `build` hook wired into `build()`, `BuildResult.outDir` for post-build artifacts ✅ |
|
|
424
561
|
| v2.4.3 | Client-only islands (`directive: "only"`, `options: { ssr: false, fallback }`), `isSSR()` export, SSR error wrapping ✅ |
|
|
425
562
|
| v2.4.4 | Fix: `"only"` directive now hydrates immediately like `"load"`. Fix: islands without SSR DOM use fresh `_render` mount instead of `hydrateTemplate` ✅ |
|
|
563
|
+
| v2.5 | 0% JS por defecto (gating por página + split entry/router + `js:"legacy"`), router de nueva generación (eventos de ciclo de vida, `data-elur-persist`, LRU+network-aware prefetch, morph opcional, speculation rules, loading indicator), fix head/payload del render endpoint en prod, `load` real ✅ |
|
|
426
564
|
|
|
427
565
|
## API
|
|
428
566
|
|
|
@@ -905,7 +1043,7 @@ export default defineConfig({
|
|
|
905
1043
|
|
|
906
1044
|
The Vite plugin activates:
|
|
907
1045
|
|
|
908
|
-
- **Build-time compiler** — lowers `
|
|
1046
|
+
- **Build-time compiler** — lowers ``html` `` templates to imperative DOM
|
|
909
1047
|
code (`firstChild`/`nextSibling` navigation, inline `setAttribute`,
|
|
910
1048
|
grouped effects, event delegation). Eliminates `detectContext`,
|
|
911
1049
|
`buildHTML`, and both `TreeWalker` passes in runtime.
|
|
@@ -1291,16 +1429,111 @@ export const config = {
|
|
|
1291
1429
|
};
|
|
1292
1430
|
```
|
|
1293
1431
|
|
|
1294
|
-
|
|
1432
|
+
The middleware runs in `dev`, `preview` and `start` through the unified Web
|
|
1433
|
+
handler: after redirects/rewrites and the internal endpoints, before routing.
|
|
1434
|
+
Returning a `Response` short-circuits the pipeline (security headers and
|
|
1435
|
+
`X-Request-ID` still apply); `next({ headers })` merges headers into the
|
|
1436
|
+
downstream request and `next({ locals })` is exposed to API routes as
|
|
1437
|
+
`ctx.locals`. The generated Node/Bun adapter servers do not run the
|
|
1438
|
+
middleware file yet.
|
|
1295
1439
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1440
|
+
### Redirects, rewrites and route headers
|
|
1441
|
+
|
|
1442
|
+
Declare redirects, rewrites and extra response headers in `elur.config.ts`.
|
|
1443
|
+
All patterns support `:param` segments, `*` wildcards and `:name*` catch-alls,
|
|
1444
|
+
with `:param` interpolation in destinations:
|
|
1445
|
+
|
|
1446
|
+
```ts
|
|
1447
|
+
import { defineConfig } from "@elurjs/kit";
|
|
1448
|
+
|
|
1449
|
+
export default defineConfig({
|
|
1450
|
+
redirects: [
|
|
1451
|
+
{ from: "/old-blog/:slug", to: "/blog/:slug", status: 301 },
|
|
1452
|
+
],
|
|
1453
|
+
rewrites: [
|
|
1454
|
+
{ from: "/api/legacy/*", to: "/api/v2/*" },
|
|
1455
|
+
],
|
|
1456
|
+
headers: [
|
|
1457
|
+
{ path: "/admin/*", headers: { "X-Robots-Tag": "noindex" } },
|
|
1458
|
+
],
|
|
1459
|
+
});
|
|
1460
|
+
```
|
|
1461
|
+
|
|
1462
|
+
Redirects are evaluated before any routing (default status `308`). Rewrites
|
|
1463
|
+
transparently change the pathname used for routing — API routes, static files
|
|
1464
|
+
and SSR all see the rewritten path. Route headers are matched against the
|
|
1465
|
+
original request path and applied to the response.
|
|
1466
|
+
|
|
1467
|
+
### Router, prefetch and navigation lifecycle
|
|
1468
|
+
|
|
1469
|
+
The SPA router prefetches pages on `pointerenter`, `focus` and `pointerdown`
|
|
1470
|
+
(the "tap" strategy — earlier than the click on touch devices), and on links
|
|
1471
|
+
that opt into viewport prefetching with `data-prefetch="viewport"`
|
|
1472
|
+
(IntersectionObserver, 200px root margin). Prefetched pages live in a
|
|
1473
|
+
bounded LRU cache (32 entries, 30 s TTL). Prefetching is skipped on
|
|
1474
|
+
constrained networks — `Save-Data` or `slow-2g`/`2g` effective types —
|
|
1475
|
+
unless a link forces it with `data-prefetch="always"`. Add
|
|
1476
|
+
`data-no-prefetch` to any link to opt out entirely.
|
|
1477
|
+
|
|
1478
|
+
Navigation lifecycle events (on `document`) carry
|
|
1479
|
+
`{ pathname, search, fromCache, popstate }`:
|
|
1480
|
+
|
|
1481
|
+
- `elur:navigate-start` — a navigation begins.
|
|
1482
|
+
- `elur:navigate-end` — the DOM swap completed.
|
|
1483
|
+
- `elur:navigate-error` — the navigation failed (e.g. render endpoint
|
|
1484
|
+
unavailable); the router falls back to a full page load.
|
|
1485
|
+
- `elur:before-render` — fires *before* the `#app` swap; the generated
|
|
1486
|
+
entry disposes islands here (except islands inside persisted nodes,
|
|
1487
|
+
listed in `detail.persisted`).
|
|
1488
|
+
- `elur:rendered` — fires after the swap; islands re-hydrate here.
|
|
1489
|
+
|
|
1490
|
+
Elements marked `data-elur-persist="key"` survive SPA navigations: the
|
|
1491
|
+
router moves the *same live DOM node* into its position in the new page
|
|
1492
|
+
(matched by the attribute value), using `Element.moveBefore()` when
|
|
1493
|
+
available — media playback, island state and scroll positions are
|
|
1494
|
+
preserved. Islands inside persisted subtrees are not disposed/rehydrated;
|
|
1495
|
+
if their incoming props differ, an `elur:persist-props-changed` event
|
|
1496
|
+
bubbles from the island marker.
|
|
1497
|
+
|
|
1498
|
+
Scroll is managed by the router (`history.scrollRestoration = "manual"`):
|
|
1499
|
+
the position is saved per history entry and restored on back/forward.
|
|
1299
1500
|
|
|
1300
1501
|
When the browser supports the View Transitions API, page transitions use
|
|
1301
1502
|
`document.startViewTransition()` for smooth cross-fade animations. This is
|
|
1302
1503
|
automatically disabled when the user has `prefers-reduced-motion: reduce`.
|
|
1303
1504
|
|
|
1505
|
+
Router options (`defineConfig`):
|
|
1506
|
+
|
|
1507
|
+
```ts
|
|
1508
|
+
export default defineConfig({
|
|
1509
|
+
router: {
|
|
1510
|
+
enabled: true, // SPA navigation; false → MPA + 0 KB JS on island-free pages
|
|
1511
|
+
prefetch: true, // link prefetching (network-aware)
|
|
1512
|
+
morph: false, // experimental: idiomorph DOM morphing for the swap
|
|
1513
|
+
loadingIndicator: false,// top progress bar on navigations slower than ~200 ms
|
|
1514
|
+
speculation: undefined, // "prefetch" | "prerender" → Speculation Rules on static pages
|
|
1515
|
+
},
|
|
1516
|
+
js: "modern", // "legacy" restores the unconditional combined entry
|
|
1517
|
+
});
|
|
1518
|
+
```
|
|
1519
|
+
|
|
1520
|
+
### Per-page JavaScript emission
|
|
1521
|
+
|
|
1522
|
+
With `js: "modern"` (default) the document shell inspects the rendered body
|
|
1523
|
+
and only ships what the page needs:
|
|
1524
|
+
|
|
1525
|
+
| Page contents | `router.enabled: true` | `router.enabled: false` |
|
|
1526
|
+
|---|---|---|
|
|
1527
|
+
| With islands | `entry-client.js` + `router.js` | `entry-client.js` only |
|
|
1528
|
+
| Without islands | `router.js` only | **0 KB of JS** |
|
|
1529
|
+
|
|
1530
|
+
The split requires the router to be its own bundle chunk — automatic when
|
|
1531
|
+
the project has no `vite.client.config.*` (the CLI synthesizes a default
|
|
1532
|
+
two-input config) or when that config declares `.elur/router.ts` as a
|
|
1533
|
+
second input. A single-input config keeps the router embedded in
|
|
1534
|
+
`entry-client.js` (emitted whenever islands exist *or* the router is on).
|
|
1535
|
+
Set `js: "legacy"` to restore the pre-split behavior unconditionally.
|
|
1536
|
+
|
|
1304
1537
|
## Project conventions
|
|
1305
1538
|
|
|
1306
1539
|
```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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-DZHihU3Z.cjs","names":[],"sources":["../../src/action/define.ts","../../src/action/index.ts"],"sourcesContent":["// --- defineAction(): typed server action definition (plan §9.2) ---\n//\n// Provides a typed wrapper for server actions with:\n// - optional input schema validation (without requiring Zod)\n// - AbortSignal propagation from the request\n// - idempotency key support\n// - concurrency modes (latest, queue, parallel)\n//\n// Legacy exported async functions continue to work: `defineAction()` is an\n// opt-in upgrade path, not a replacement.\n\nimport type { ActionFailure } from \"../errors.js\";\n\n// Capture AbortController at module load time — tests may temporarily remove\n// it from the global scope.\nconst AbortControllerCtor = globalThis.AbortController;\n\n/** Concurrency mode for actions called multiple times in rapid succession. */\nexport type ActionConcurrencyMode = \"latest\" | \"queue\" | \"parallel\";\n\n/** Options for defining a typed server action. */\nexport interface DefineActionOptions<TInput> {\n /**\n * Optional input validator. Can be a Zod schema, a plain function, or any\n * object with a `.parse()` method. If validation fails, the action returns\n * a 400 ActionFailure with the validation error.\n */\n input?: ActionInputValidator<TInput>;\n /** Concurrency mode when the same action is called multiple times. */\n concurrency?: ActionConcurrencyMode;\n /** Whether the action is idempotent (safe to retry). */\n idempotent?: boolean;\n /** Tags to invalidate from the cache after a successful action (§9.4). */\n invalidateTags?: string[];\n /** Paths to invalidate from the cache after a successful action (§9.4). */\n invalidatePaths?: string[];\n}\n\n/** A validator that has a `.parse()` method (Zod-compatible) or is a function. */\nexport interface ActionInputValidator<T> {\n parse(input: unknown): T;\n}\n\n/** Context passed to a defined action. */\nexport interface ActionContext {\n /** The original Web Request. */\n request: Request;\n /** AbortSignal from the request — aborts if the client disconnects. */\n signal: AbortSignal;\n /** Idempotency key from the request header, if present. */\n idempotencyKey?: string;\n /** Route params (for page-scoped actions). */\n params: Record<string, string | string[]>;\n /** Per-request locals (populated by middleware). */\n locals: Record<string, unknown>;\n}\n\n/** A defined action function. */\nexport type DefinedActionFn<TInput, TOutput> = (\n input: TInput,\n ctx: ActionContext,\n) => Promise<TOutput | ActionFailure<TOutput>>;\n\n/** The return type of defineAction(): a callable with metadata. */\nexport interface DefinedAction<TInput, TOutput> {\n (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>>;\n /** Metadata for the action (used by the runtime/manifest). */\n __elurAction: {\n name: string;\n concurrency: ActionConcurrencyMode;\n idempotent: boolean;\n invalidateTags: readonly string[];\n invalidatePaths: readonly string[];\n };\n}\n\n/**\n * Defines a typed server action with validation, abort support, and cache\n * invalidation metadata.\n *\n * ```ts\n * import { defineAction, fail } from \"@elurjs/kit/action\";\n *\n * export const submitContact = defineAction({\n * input: { parse: (v) => v as { name: string; email: string } },\n * invalidateTags: [\"contacts\"],\n * }, async (input, ctx) => {\n * if (!input.email.includes(\"@\")) return fail(400, { email: \"Invalid\" });\n * await saveContact(input);\n * return { success: true };\n * });\n * ```\n *\n * Legacy exported async functions (without `defineAction`) continue to work\n * as before — this is an opt-in upgrade.\n */\nexport function defineAction<TInput = unknown, TOutput = unknown>(\n options: DefineActionOptions<TInput>,\n handler: DefinedActionFn<TInput, TOutput>,\n): DefinedAction<TInput, TOutput> {\n const concurrency = options.concurrency ?? \"latest\";\n const idempotent = options.idempotent ?? false;\n const invalidateTags = options.invalidateTags ?? [];\n const invalidatePaths = options.invalidatePaths ?? [];\n\n // Track in-flight calls for concurrency control.\n let latestController: InstanceType<typeof AbortControllerCtor> | null = null;\n const queue: Array<() => void> = [];\n let running = 0;\n\n async function run(\n input: TInput,\n ctx: ActionContext,\n ): Promise<TOutput | ActionFailure<TOutput>> {\n // Validate input if a validator is configured.\n if (options.input) {\n try {\n const validated = options.input.parse(input);\n input = validated;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const { fail } = await import(\"../errors.js\");\n return fail(400, { validation: message }) as ActionFailure<TOutput>;\n }\n }\n\n return handler(input, ctx);\n }\n\n const fn = async (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>> => {\n if (concurrency === \"latest\") {\n // Cancel any previous in-flight call.\n if (latestController) latestController.abort();\n latestController = new AbortControllerCtor();\n // Combine the request signal with our cancellation signal.\n const combinedSignal = combineSignals(ctx.signal, latestController.signal);\n return run(input, { ...ctx, signal: combinedSignal });\n }\n\n if (concurrency === \"queue\") {\n // Wait for previous calls to finish.\n if (running > 0) {\n await new Promise<void>((resolve) => queue.push(resolve));\n }\n running++;\n try {\n return await run(input, ctx);\n } finally {\n running--;\n const next = queue.shift();\n if (next) next();\n }\n }\n\n // parallel: just run it.\n return run(input, ctx);\n };\n\n // Attach metadata.\n (fn as DefinedAction<TInput, TOutput>).__elurAction = {\n name: handler.name || \"anonymous\",\n concurrency,\n idempotent,\n invalidateTags,\n invalidatePaths,\n };\n\n return fn as DefinedAction<TInput, TOutput>;\n}\n\n/** Combines two AbortSignals into one that aborts when either does. */\nfunction combineSignals(...signals: (AbortSignal | undefined)[]): AbortSignal {\n const controller = new AbortControllerCtor();\n for (const signal of signals) {\n if (!signal) continue;\n if (signal.aborted) {\n controller.abort();\n break;\n }\n signal.addEventListener(\"abort\", () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","/**\n * Client-side helpers for invoking server actions.\n *\n * Server actions are defined in `page.action.ts` files next to `page.ts`.\n * They export async functions that run on the server. On the client, call them\n * by name using `callAction` or the higher-level `elurJsAction` helper:\n *\n * ```ts\n * import { callAction } from \"@elurjs/kit/action\";\n *\n * const result = await callAction(\"submitContact\", { name: \"Ada\" }, { page: \"/contact\" });\n * ```\n *\n * ```ts\n * import { elurJsAction } from \"@elurjs/kit/action\";\n *\n * const contact = elurJsAction(\"submitContact\", { page: \"/contact\" });\n * await contact.submit({ name: \"Ada\" });\n * console.log(contact.data.value, contact.error.value, contact.pending.value);\n * ```\n */\n\nimport { signal } from \"@elurjs/core\";\nimport { ActionFailure, RedirectResponse } from \"../errors.js\";\n\ninterface ActionFailurePayload {\n __elur_js_action_failure?: boolean;\n status?: number;\n data?: unknown;\n}\n\ninterface RedirectPayload {\n __elur_js_action_redirect?: boolean;\n status?: number;\n location?: string;\n}\n\nfunction isActionFailurePayload(value: unknown): value is ActionFailurePayload & { __elur_js_action_failure: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_failure === true;\n}\n\nfunction isRedirectPayload(value: unknown): value is RedirectPayload & { __elur_js_action_redirect: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_redirect === true;\n}\n\nexport interface ActionRequest {\n name: string;\n page?: string;\n args: unknown[];\n}\n\nexport interface CallActionOptions {\n /** Page URL path that scopes the action, e.g. `/contact`. */\n page?: string;\n}\n\n/**\n * Call a server action by name.\n *\n * The request is sent as a POST to `/__elur-js/actions` with the action name,\n * optional page scope, and serialized arguments. The server executes the\n * matching exported function from the scanned `page.action.ts` modules and\n * returns its JSON result.\n */\nexport async function callAction<T = unknown>(\n name: string,\n args: unknown = [],\n options: CallActionOptions = {},\n): Promise<T | ActionFailure<T> | RedirectResponse> {\n const argsArray = Array.isArray(args) ? args : [args];\n const res = await fetch(\"/__elur-js/actions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify({ name, page: options.page, args: argsArray } as ActionRequest),\n });\n\n const text = await res.text();\n if (!res.ok) {\n let payload: unknown;\n try {\n payload = JSON.parse(text);\n } catch {\n // not JSON, treat as plain error\n }\n if (isActionFailurePayload(payload) && payload.status !== undefined) {\n return new ActionFailure(payload.status, payload.data as T);\n }\n throw new Error(`Action \"${name}\" failed: ${text}`);\n }\n\n const payload: unknown = JSON.parse(text);\n if (isRedirectPayload(payload) && payload.status !== undefined && payload.location !== undefined) {\n return new RedirectResponse(payload.status, payload.location);\n }\n\n return payload as T;\n}\n\nexport interface ElurJsAction<TInput = unknown, TOutput = unknown> {\n /** Submit the action with the given input. */\n submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse>;\n /** Signal that is true while the action is running. */\n pending: { value: boolean };\n /** Signal with the last successful result, action failure, redirect, or null. */\n data: { value: TOutput | ActionFailure<TOutput> | RedirectResponse | null };\n /** Signal with the last error, or null. */\n error: { value: Error | null };\n}\n\n/**\n * Create a reactive handle for a server action.\n *\n * Returns a `submit` function and signals for `pending`, `data`, and `error`.\n * Useful for wiring actions to forms and islands without manual signal boilerplate.\n */\nexport function elurJsAction<TInput = unknown, TOutput = unknown>(\n name: string,\n options: CallActionOptions = {},\n): ElurJsAction<TInput, TOutput> {\n const pending = signal(false);\n const error = signal<Error | null>(null);\n const data = signal<TOutput | ActionFailure<TOutput> | RedirectResponse | null>(null);\n\n async function submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse> {\n pending.value = true;\n error.value = null;\n try {\n const result = await callAction<TOutput>(name, input, options);\n data.value = result;\n return result;\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err));\n throw err;\n } finally {\n pending.value = false;\n }\n }\n\n return {\n submit,\n pending,\n data,\n error,\n };\n}\n\n// Server-side: defineAction() for typed actions with validation/abort/cache.\nexport {\n defineAction,\n type DefineActionOptions,\n type DefinedAction,\n type DefinedActionFn,\n type ActionContext,\n type ActionInputValidator,\n type ActionConcurrencyMode,\n} from \"./define.js\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"action-BiE4tEty.cjs","names":[],"sources":["../../src/action/define.ts","../../src/action/index.ts"],"sourcesContent":["// --- defineAction(): typed server action definition (plan §9.2) ---\n//\n// Provides a typed wrapper for server actions with:\n// - optional input schema validation (without requiring Zod)\n// - AbortSignal propagation from the request\n// - idempotency key support\n// - concurrency modes (latest, queue, parallel)\n//\n// Legacy exported async functions continue to work: `defineAction()` is an\n// opt-in upgrade path, not a replacement.\n\nimport type { ActionFailure } from \"../errors.js\";\n\n// Capture AbortController at module load time — tests may temporarily remove\n// it from the global scope.\nconst AbortControllerCtor = globalThis.AbortController;\n\n/** Concurrency mode for actions called multiple times in rapid succession. */\nexport type ActionConcurrencyMode = \"latest\" | \"queue\" | \"parallel\";\n\n/** Options for defining a typed server action. */\nexport interface DefineActionOptions<TInput> {\n /**\n * Optional input validator. Can be a Zod schema, a plain function, or any\n * object with a `.parse()` method. If validation fails, the action returns\n * a 400 ActionFailure with the validation error.\n */\n input?: ActionInputValidator<TInput>;\n /** Concurrency mode when the same action is called multiple times. */\n concurrency?: ActionConcurrencyMode;\n /** Whether the action is idempotent (safe to retry). */\n idempotent?: boolean;\n /** Tags to invalidate from the cache after a successful action (§9.4). */\n invalidateTags?: string[];\n /** Paths to invalidate from the cache after a successful action (§9.4). */\n invalidatePaths?: string[];\n}\n\n/** A validator that has a `.parse()` method (Zod-compatible) or is a function. */\nexport interface ActionInputValidator<T> {\n parse(input: unknown): T;\n}\n\n/** Context passed to a defined action. */\nexport interface ActionContext {\n /** The original Web Request. */\n request: Request;\n /** AbortSignal from the request — aborts if the client disconnects. */\n signal: AbortSignal;\n /** Idempotency key from the request header, if present. */\n idempotencyKey?: string;\n /** Route params (for page-scoped actions). */\n params: Record<string, string | string[]>;\n /** Per-request locals (populated by middleware). */\n locals: Record<string, unknown>;\n}\n\n/** A defined action function. */\nexport type DefinedActionFn<TInput, TOutput> = (\n input: TInput,\n ctx: ActionContext,\n) => Promise<TOutput | ActionFailure<TOutput>>;\n\n/** The return type of defineAction(): a callable with metadata. */\nexport interface DefinedAction<TInput, TOutput> {\n (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>>;\n /** Metadata for the action (used by the runtime/manifest). */\n __elurAction: {\n name: string;\n concurrency: ActionConcurrencyMode;\n idempotent: boolean;\n invalidateTags: readonly string[];\n invalidatePaths: readonly string[];\n };\n}\n\n/**\n * Defines a typed server action with validation, abort support, and cache\n * invalidation metadata.\n *\n * ```ts\n * import { defineAction, fail } from \"@elurjs/kit/action\";\n *\n * export const submitContact = defineAction({\n * input: { parse: (v) => v as { name: string; email: string } },\n * invalidateTags: [\"contacts\"],\n * }, async (input, ctx) => {\n * if (!input.email.includes(\"@\")) return fail(400, { email: \"Invalid\" });\n * await saveContact(input);\n * return { success: true };\n * });\n * ```\n *\n * Legacy exported async functions (without `defineAction`) continue to work\n * as before — this is an opt-in upgrade.\n */\nexport function defineAction<TInput = unknown, TOutput = unknown>(\n options: DefineActionOptions<TInput>,\n handler: DefinedActionFn<TInput, TOutput>,\n): DefinedAction<TInput, TOutput> {\n const concurrency = options.concurrency ?? \"latest\";\n const idempotent = options.idempotent ?? false;\n const invalidateTags = options.invalidateTags ?? [];\n const invalidatePaths = options.invalidatePaths ?? [];\n\n // Track in-flight calls for concurrency control.\n let latestController: InstanceType<typeof AbortControllerCtor> | null = null;\n const queue: Array<() => void> = [];\n let running = 0;\n\n async function run(\n input: TInput,\n ctx: ActionContext,\n ): Promise<TOutput | ActionFailure<TOutput>> {\n // Validate input if a validator is configured.\n if (options.input) {\n try {\n const validated = options.input.parse(input);\n input = validated;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const { fail } = await import(\"../errors.js\");\n return fail(400, { validation: message }) as ActionFailure<TOutput>;\n }\n }\n\n return handler(input, ctx);\n }\n\n const fn = async (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>> => {\n if (concurrency === \"latest\") {\n // Cancel any previous in-flight call.\n if (latestController) latestController.abort();\n latestController = new AbortControllerCtor();\n // Combine the request signal with our cancellation signal.\n const combinedSignal = combineSignals(ctx.signal, latestController.signal);\n return run(input, { ...ctx, signal: combinedSignal });\n }\n\n if (concurrency === \"queue\") {\n // Wait for previous calls to finish.\n if (running > 0) {\n await new Promise<void>((resolve) => queue.push(resolve));\n }\n running++;\n try {\n return await run(input, ctx);\n } finally {\n running--;\n const next = queue.shift();\n if (next) next();\n }\n }\n\n // parallel: just run it.\n return run(input, ctx);\n };\n\n // Attach metadata.\n (fn as DefinedAction<TInput, TOutput>).__elurAction = {\n name: handler.name || \"anonymous\",\n concurrency,\n idempotent,\n invalidateTags,\n invalidatePaths,\n };\n\n return fn as DefinedAction<TInput, TOutput>;\n}\n\n/** Combines two AbortSignals into one that aborts when either does. */\nfunction combineSignals(...signals: (AbortSignal | undefined)[]): AbortSignal {\n const controller = new AbortControllerCtor();\n for (const signal of signals) {\n if (!signal) continue;\n if (signal.aborted) {\n controller.abort();\n break;\n }\n signal.addEventListener(\"abort\", () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","/**\n * Client-side helpers for invoking server actions.\n *\n * Server actions are defined in `page.action.ts` files next to `page.ts`.\n * They export async functions that run on the server. On the client, call them\n * by name using `callAction` or the higher-level `elurJsAction` helper:\n *\n * ```ts\n * import { callAction } from \"@elurjs/kit/action\";\n *\n * const result = await callAction(\"submitContact\", { name: \"Ada\" }, { page: \"/contact\" });\n * ```\n *\n * ```ts\n * import { elurJsAction } from \"@elurjs/kit/action\";\n *\n * const contact = elurJsAction(\"submitContact\", { page: \"/contact\" });\n * await contact.submit({ name: \"Ada\" });\n * console.log(contact.data.value, contact.error.value, contact.pending.value);\n * ```\n */\n\nimport { signal } from \"@elurjs/core\";\nimport { ActionFailure, RedirectResponse } from \"../errors.js\";\n\ninterface ActionFailurePayload {\n __elur_js_action_failure?: boolean;\n status?: number;\n data?: unknown;\n}\n\ninterface RedirectPayload {\n __elur_js_action_redirect?: boolean;\n status?: number;\n location?: string;\n}\n\nfunction isActionFailurePayload(value: unknown): value is ActionFailurePayload & { __elur_js_action_failure: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_failure === true;\n}\n\nfunction isRedirectPayload(value: unknown): value is RedirectPayload & { __elur_js_action_redirect: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_redirect === true;\n}\n\nexport interface ActionRequest {\n name: string;\n page?: string;\n args: unknown[];\n}\n\nexport interface CallActionOptions {\n /** Page URL path that scopes the action, e.g. `/contact`. */\n page?: string;\n}\n\n/**\n * Call a server action by name.\n *\n * The request is sent as a POST to `/__elur-js/actions` with the action name,\n * optional page scope, and serialized arguments. The server executes the\n * matching exported function from the scanned `page.action.ts` modules and\n * returns its JSON result.\n */\nexport async function callAction<T = unknown>(\n name: string,\n args: unknown = [],\n options: CallActionOptions = {},\n): Promise<T | ActionFailure<T> | RedirectResponse> {\n const argsArray = Array.isArray(args) ? args : [args];\n const res = await fetch(\"/__elur-js/actions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify({ name, page: options.page, args: argsArray } as ActionRequest),\n });\n\n const text = await res.text();\n if (!res.ok) {\n let payload: unknown;\n try {\n payload = JSON.parse(text);\n } catch {\n // not JSON, treat as plain error\n }\n if (isActionFailurePayload(payload) && payload.status !== undefined) {\n return new ActionFailure(payload.status, payload.data as T);\n }\n throw new Error(`Action \"${name}\" failed: ${text}`);\n }\n\n const payload: unknown = JSON.parse(text);\n if (isRedirectPayload(payload) && payload.status !== undefined && payload.location !== undefined) {\n return new RedirectResponse(payload.status, payload.location);\n }\n\n return payload as T;\n}\n\nexport interface ElurJsAction<TInput = unknown, TOutput = unknown> {\n /** Submit the action with the given input. */\n submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse>;\n /** Signal that is true while the action is running. */\n pending: { value: boolean };\n /** Signal with the last successful result, action failure, redirect, or null. */\n data: { value: TOutput | ActionFailure<TOutput> | RedirectResponse | null };\n /** Signal with the last error, or null. */\n error: { value: Error | null };\n}\n\n/**\n * Create a reactive handle for a server action.\n *\n * Returns a `submit` function and signals for `pending`, `data`, and `error`.\n * Useful for wiring actions to forms and islands without manual signal boilerplate.\n */\nexport function elurJsAction<TInput = unknown, TOutput = unknown>(\n name: string,\n options: CallActionOptions = {},\n): ElurJsAction<TInput, TOutput> {\n const pending = signal(false);\n const error = signal<Error | null>(null);\n const data = signal<TOutput | ActionFailure<TOutput> | RedirectResponse | null>(null);\n\n async function submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse> {\n pending.value = true;\n error.value = null;\n try {\n const result = await callAction<TOutput>(name, input, options);\n data.value = result;\n return result;\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err));\n throw err;\n } finally {\n pending.value = false;\n }\n }\n\n return {\n submit,\n pending,\n data,\n error,\n };\n}\n\n// Server-side: defineAction() for typed actions with validation/abort/cache.\nexport {\n defineAction,\n type DefineActionOptions,\n type DefinedAction,\n type DefinedActionFn,\n type ActionContext,\n type ActionInputValidator,\n type ActionConcurrencyMode,\n} from \"./define.js\";\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as e, t } from "./errors-
|
|
1
|
+
import { n as e, t } from "./errors-j4cg9VN3.js";
|
|
2
2
|
import { signal as n } from "@elurjs/core";
|
|
3
3
|
//#region src/action/define.ts
|
|
4
4
|
var r = globalThis.AbortController;
|
|
@@ -8,7 +8,7 @@ function i(e, t) {
|
|
|
8
8
|
if (e.input) try {
|
|
9
9
|
n = e.input.parse(n);
|
|
10
10
|
} catch (e) {
|
|
11
|
-
let t = e instanceof Error ? e.message : String(e), { fail: n } = await import("./errors-
|
|
11
|
+
let t = e instanceof Error ? e.message : String(e), { fail: n } = await import("./errors-j4cg9VN3.js").then((e) => e.r);
|
|
12
12
|
return n(400, { validation: t });
|
|
13
13
|
}
|
|
14
14
|
return t(n, r);
|
|
@@ -108,4 +108,4 @@ function l(e, t = {}) {
|
|
|
108
108
|
//#endregion
|
|
109
109
|
export { l as n, i as r, c as t };
|
|
110
110
|
|
|
111
|
-
//# sourceMappingURL=action-
|
|
111
|
+
//# sourceMappingURL=action-CXVk8rWt.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-BB6N5yD-.js","names":[],"sources":["../../src/action/define.ts","../../src/action/index.ts"],"sourcesContent":["// --- defineAction(): typed server action definition (plan §9.2) ---\n//\n// Provides a typed wrapper for server actions with:\n// - optional input schema validation (without requiring Zod)\n// - AbortSignal propagation from the request\n// - idempotency key support\n// - concurrency modes (latest, queue, parallel)\n//\n// Legacy exported async functions continue to work: `defineAction()` is an\n// opt-in upgrade path, not a replacement.\n\nimport type { ActionFailure } from \"../errors.js\";\n\n// Capture AbortController at module load time — tests may temporarily remove\n// it from the global scope.\nconst AbortControllerCtor = globalThis.AbortController;\n\n/** Concurrency mode for actions called multiple times in rapid succession. */\nexport type ActionConcurrencyMode = \"latest\" | \"queue\" | \"parallel\";\n\n/** Options for defining a typed server action. */\nexport interface DefineActionOptions<TInput> {\n /**\n * Optional input validator. Can be a Zod schema, a plain function, or any\n * object with a `.parse()` method. If validation fails, the action returns\n * a 400 ActionFailure with the validation error.\n */\n input?: ActionInputValidator<TInput>;\n /** Concurrency mode when the same action is called multiple times. */\n concurrency?: ActionConcurrencyMode;\n /** Whether the action is idempotent (safe to retry). */\n idempotent?: boolean;\n /** Tags to invalidate from the cache after a successful action (§9.4). */\n invalidateTags?: string[];\n /** Paths to invalidate from the cache after a successful action (§9.4). */\n invalidatePaths?: string[];\n}\n\n/** A validator that has a `.parse()` method (Zod-compatible) or is a function. */\nexport interface ActionInputValidator<T> {\n parse(input: unknown): T;\n}\n\n/** Context passed to a defined action. */\nexport interface ActionContext {\n /** The original Web Request. */\n request: Request;\n /** AbortSignal from the request — aborts if the client disconnects. */\n signal: AbortSignal;\n /** Idempotency key from the request header, if present. */\n idempotencyKey?: string;\n /** Route params (for page-scoped actions). */\n params: Record<string, string | string[]>;\n /** Per-request locals (populated by middleware). */\n locals: Record<string, unknown>;\n}\n\n/** A defined action function. */\nexport type DefinedActionFn<TInput, TOutput> = (\n input: TInput,\n ctx: ActionContext,\n) => Promise<TOutput | ActionFailure<TOutput>>;\n\n/** The return type of defineAction(): a callable with metadata. */\nexport interface DefinedAction<TInput, TOutput> {\n (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>>;\n /** Metadata for the action (used by the runtime/manifest). */\n __elurAction: {\n name: string;\n concurrency: ActionConcurrencyMode;\n idempotent: boolean;\n invalidateTags: readonly string[];\n invalidatePaths: readonly string[];\n };\n}\n\n/**\n * Defines a typed server action with validation, abort support, and cache\n * invalidation metadata.\n *\n * ```ts\n * import { defineAction, fail } from \"@elurjs/kit/action\";\n *\n * export const submitContact = defineAction({\n * input: { parse: (v) => v as { name: string; email: string } },\n * invalidateTags: [\"contacts\"],\n * }, async (input, ctx) => {\n * if (!input.email.includes(\"@\")) return fail(400, { email: \"Invalid\" });\n * await saveContact(input);\n * return { success: true };\n * });\n * ```\n *\n * Legacy exported async functions (without `defineAction`) continue to work\n * as before — this is an opt-in upgrade.\n */\nexport function defineAction<TInput = unknown, TOutput = unknown>(\n options: DefineActionOptions<TInput>,\n handler: DefinedActionFn<TInput, TOutput>,\n): DefinedAction<TInput, TOutput> {\n const concurrency = options.concurrency ?? \"latest\";\n const idempotent = options.idempotent ?? false;\n const invalidateTags = options.invalidateTags ?? [];\n const invalidatePaths = options.invalidatePaths ?? [];\n\n // Track in-flight calls for concurrency control.\n let latestController: InstanceType<typeof AbortControllerCtor> | null = null;\n const queue: Array<() => void> = [];\n let running = 0;\n\n async function run(\n input: TInput,\n ctx: ActionContext,\n ): Promise<TOutput | ActionFailure<TOutput>> {\n // Validate input if a validator is configured.\n if (options.input) {\n try {\n const validated = options.input.parse(input);\n input = validated;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const { fail } = await import(\"../errors.js\");\n return fail(400, { validation: message }) as ActionFailure<TOutput>;\n }\n }\n\n return handler(input, ctx);\n }\n\n const fn = async (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>> => {\n if (concurrency === \"latest\") {\n // Cancel any previous in-flight call.\n if (latestController) latestController.abort();\n latestController = new AbortControllerCtor();\n // Combine the request signal with our cancellation signal.\n const combinedSignal = combineSignals(ctx.signal, latestController.signal);\n return run(input, { ...ctx, signal: combinedSignal });\n }\n\n if (concurrency === \"queue\") {\n // Wait for previous calls to finish.\n if (running > 0) {\n await new Promise<void>((resolve) => queue.push(resolve));\n }\n running++;\n try {\n return await run(input, ctx);\n } finally {\n running--;\n const next = queue.shift();\n if (next) next();\n }\n }\n\n // parallel: just run it.\n return run(input, ctx);\n };\n\n // Attach metadata.\n (fn as DefinedAction<TInput, TOutput>).__elurAction = {\n name: handler.name || \"anonymous\",\n concurrency,\n idempotent,\n invalidateTags,\n invalidatePaths,\n };\n\n return fn as DefinedAction<TInput, TOutput>;\n}\n\n/** Combines two AbortSignals into one that aborts when either does. */\nfunction combineSignals(...signals: (AbortSignal | undefined)[]): AbortSignal {\n const controller = new AbortControllerCtor();\n for (const signal of signals) {\n if (!signal) continue;\n if (signal.aborted) {\n controller.abort();\n break;\n }\n signal.addEventListener(\"abort\", () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","/**\n * Client-side helpers for invoking server actions.\n *\n * Server actions are defined in `page.action.ts` files next to `page.ts`.\n * They export async functions that run on the server. On the client, call them\n * by name using `callAction` or the higher-level `elurJsAction` helper:\n *\n * ```ts\n * import { callAction } from \"@elurjs/kit/action\";\n *\n * const result = await callAction(\"submitContact\", { name: \"Ada\" }, { page: \"/contact\" });\n * ```\n *\n * ```ts\n * import { elurJsAction } from \"@elurjs/kit/action\";\n *\n * const contact = elurJsAction(\"submitContact\", { page: \"/contact\" });\n * await contact.submit({ name: \"Ada\" });\n * console.log(contact.data.value, contact.error.value, contact.pending.value);\n * ```\n */\n\nimport { signal } from \"@elurjs/core\";\nimport { ActionFailure, RedirectResponse } from \"../errors.js\";\n\ninterface ActionFailurePayload {\n __elur_js_action_failure?: boolean;\n status?: number;\n data?: unknown;\n}\n\ninterface RedirectPayload {\n __elur_js_action_redirect?: boolean;\n status?: number;\n location?: string;\n}\n\nfunction isActionFailurePayload(value: unknown): value is ActionFailurePayload & { __elur_js_action_failure: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_failure === true;\n}\n\nfunction isRedirectPayload(value: unknown): value is RedirectPayload & { __elur_js_action_redirect: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_redirect === true;\n}\n\nexport interface ActionRequest {\n name: string;\n page?: string;\n args: unknown[];\n}\n\nexport interface CallActionOptions {\n /** Page URL path that scopes the action, e.g. `/contact`. */\n page?: string;\n}\n\n/**\n * Call a server action by name.\n *\n * The request is sent as a POST to `/__elur-js/actions` with the action name,\n * optional page scope, and serialized arguments. The server executes the\n * matching exported function from the scanned `page.action.ts` modules and\n * returns its JSON result.\n */\nexport async function callAction<T = unknown>(\n name: string,\n args: unknown = [],\n options: CallActionOptions = {},\n): Promise<T | ActionFailure<T> | RedirectResponse> {\n const argsArray = Array.isArray(args) ? args : [args];\n const res = await fetch(\"/__elur-js/actions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify({ name, page: options.page, args: argsArray } as ActionRequest),\n });\n\n const text = await res.text();\n if (!res.ok) {\n let payload: unknown;\n try {\n payload = JSON.parse(text);\n } catch {\n // not JSON, treat as plain error\n }\n if (isActionFailurePayload(payload) && payload.status !== undefined) {\n return new ActionFailure(payload.status, payload.data as T);\n }\n throw new Error(`Action \"${name}\" failed: ${text}`);\n }\n\n const payload: unknown = JSON.parse(text);\n if (isRedirectPayload(payload) && payload.status !== undefined && payload.location !== undefined) {\n return new RedirectResponse(payload.status, payload.location);\n }\n\n return payload as T;\n}\n\nexport interface ElurJsAction<TInput = unknown, TOutput = unknown> {\n /** Submit the action with the given input. */\n submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse>;\n /** Signal that is true while the action is running. */\n pending: { value: boolean };\n /** Signal with the last successful result, action failure, redirect, or null. */\n data: { value: TOutput | ActionFailure<TOutput> | RedirectResponse | null };\n /** Signal with the last error, or null. */\n error: { value: Error | null };\n}\n\n/**\n * Create a reactive handle for a server action.\n *\n * Returns a `submit` function and signals for `pending`, `data`, and `error`.\n * Useful for wiring actions to forms and islands without manual signal boilerplate.\n */\nexport function elurJsAction<TInput = unknown, TOutput = unknown>(\n name: string,\n options: CallActionOptions = {},\n): ElurJsAction<TInput, TOutput> {\n const pending = signal(false);\n const error = signal<Error | null>(null);\n const data = signal<TOutput | ActionFailure<TOutput> | RedirectResponse | null>(null);\n\n async function submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse> {\n pending.value = true;\n error.value = null;\n try {\n const result = await callAction<TOutput>(name, input, options);\n data.value = result;\n return result;\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err));\n throw err;\n } finally {\n pending.value = false;\n }\n }\n\n return {\n submit,\n pending,\n data,\n error,\n };\n}\n\n// Server-side: defineAction() for typed actions with validation/abort/cache.\nexport {\n defineAction,\n type DefineActionOptions,\n type DefinedAction,\n type DefinedActionFn,\n type ActionContext,\n type ActionInputValidator,\n type ActionConcurrencyMode,\n} from \"./define.js\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"action-CXVk8rWt.js","names":[],"sources":["../../src/action/define.ts","../../src/action/index.ts"],"sourcesContent":["// --- defineAction(): typed server action definition (plan §9.2) ---\n//\n// Provides a typed wrapper for server actions with:\n// - optional input schema validation (without requiring Zod)\n// - AbortSignal propagation from the request\n// - idempotency key support\n// - concurrency modes (latest, queue, parallel)\n//\n// Legacy exported async functions continue to work: `defineAction()` is an\n// opt-in upgrade path, not a replacement.\n\nimport type { ActionFailure } from \"../errors.js\";\n\n// Capture AbortController at module load time — tests may temporarily remove\n// it from the global scope.\nconst AbortControllerCtor = globalThis.AbortController;\n\n/** Concurrency mode for actions called multiple times in rapid succession. */\nexport type ActionConcurrencyMode = \"latest\" | \"queue\" | \"parallel\";\n\n/** Options for defining a typed server action. */\nexport interface DefineActionOptions<TInput> {\n /**\n * Optional input validator. Can be a Zod schema, a plain function, or any\n * object with a `.parse()` method. If validation fails, the action returns\n * a 400 ActionFailure with the validation error.\n */\n input?: ActionInputValidator<TInput>;\n /** Concurrency mode when the same action is called multiple times. */\n concurrency?: ActionConcurrencyMode;\n /** Whether the action is idempotent (safe to retry). */\n idempotent?: boolean;\n /** Tags to invalidate from the cache after a successful action (§9.4). */\n invalidateTags?: string[];\n /** Paths to invalidate from the cache after a successful action (§9.4). */\n invalidatePaths?: string[];\n}\n\n/** A validator that has a `.parse()` method (Zod-compatible) or is a function. */\nexport interface ActionInputValidator<T> {\n parse(input: unknown): T;\n}\n\n/** Context passed to a defined action. */\nexport interface ActionContext {\n /** The original Web Request. */\n request: Request;\n /** AbortSignal from the request — aborts if the client disconnects. */\n signal: AbortSignal;\n /** Idempotency key from the request header, if present. */\n idempotencyKey?: string;\n /** Route params (for page-scoped actions). */\n params: Record<string, string | string[]>;\n /** Per-request locals (populated by middleware). */\n locals: Record<string, unknown>;\n}\n\n/** A defined action function. */\nexport type DefinedActionFn<TInput, TOutput> = (\n input: TInput,\n ctx: ActionContext,\n) => Promise<TOutput | ActionFailure<TOutput>>;\n\n/** The return type of defineAction(): a callable with metadata. */\nexport interface DefinedAction<TInput, TOutput> {\n (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>>;\n /** Metadata for the action (used by the runtime/manifest). */\n __elurAction: {\n name: string;\n concurrency: ActionConcurrencyMode;\n idempotent: boolean;\n invalidateTags: readonly string[];\n invalidatePaths: readonly string[];\n };\n}\n\n/**\n * Defines a typed server action with validation, abort support, and cache\n * invalidation metadata.\n *\n * ```ts\n * import { defineAction, fail } from \"@elurjs/kit/action\";\n *\n * export const submitContact = defineAction({\n * input: { parse: (v) => v as { name: string; email: string } },\n * invalidateTags: [\"contacts\"],\n * }, async (input, ctx) => {\n * if (!input.email.includes(\"@\")) return fail(400, { email: \"Invalid\" });\n * await saveContact(input);\n * return { success: true };\n * });\n * ```\n *\n * Legacy exported async functions (without `defineAction`) continue to work\n * as before — this is an opt-in upgrade.\n */\nexport function defineAction<TInput = unknown, TOutput = unknown>(\n options: DefineActionOptions<TInput>,\n handler: DefinedActionFn<TInput, TOutput>,\n): DefinedAction<TInput, TOutput> {\n const concurrency = options.concurrency ?? \"latest\";\n const idempotent = options.idempotent ?? false;\n const invalidateTags = options.invalidateTags ?? [];\n const invalidatePaths = options.invalidatePaths ?? [];\n\n // Track in-flight calls for concurrency control.\n let latestController: InstanceType<typeof AbortControllerCtor> | null = null;\n const queue: Array<() => void> = [];\n let running = 0;\n\n async function run(\n input: TInput,\n ctx: ActionContext,\n ): Promise<TOutput | ActionFailure<TOutput>> {\n // Validate input if a validator is configured.\n if (options.input) {\n try {\n const validated = options.input.parse(input);\n input = validated;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const { fail } = await import(\"../errors.js\");\n return fail(400, { validation: message }) as ActionFailure<TOutput>;\n }\n }\n\n return handler(input, ctx);\n }\n\n const fn = async (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>> => {\n if (concurrency === \"latest\") {\n // Cancel any previous in-flight call.\n if (latestController) latestController.abort();\n latestController = new AbortControllerCtor();\n // Combine the request signal with our cancellation signal.\n const combinedSignal = combineSignals(ctx.signal, latestController.signal);\n return run(input, { ...ctx, signal: combinedSignal });\n }\n\n if (concurrency === \"queue\") {\n // Wait for previous calls to finish.\n if (running > 0) {\n await new Promise<void>((resolve) => queue.push(resolve));\n }\n running++;\n try {\n return await run(input, ctx);\n } finally {\n running--;\n const next = queue.shift();\n if (next) next();\n }\n }\n\n // parallel: just run it.\n return run(input, ctx);\n };\n\n // Attach metadata.\n (fn as DefinedAction<TInput, TOutput>).__elurAction = {\n name: handler.name || \"anonymous\",\n concurrency,\n idempotent,\n invalidateTags,\n invalidatePaths,\n };\n\n return fn as DefinedAction<TInput, TOutput>;\n}\n\n/** Combines two AbortSignals into one that aborts when either does. */\nfunction combineSignals(...signals: (AbortSignal | undefined)[]): AbortSignal {\n const controller = new AbortControllerCtor();\n for (const signal of signals) {\n if (!signal) continue;\n if (signal.aborted) {\n controller.abort();\n break;\n }\n signal.addEventListener(\"abort\", () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","/**\n * Client-side helpers for invoking server actions.\n *\n * Server actions are defined in `page.action.ts` files next to `page.ts`.\n * They export async functions that run on the server. On the client, call them\n * by name using `callAction` or the higher-level `elurJsAction` helper:\n *\n * ```ts\n * import { callAction } from \"@elurjs/kit/action\";\n *\n * const result = await callAction(\"submitContact\", { name: \"Ada\" }, { page: \"/contact\" });\n * ```\n *\n * ```ts\n * import { elurJsAction } from \"@elurjs/kit/action\";\n *\n * const contact = elurJsAction(\"submitContact\", { page: \"/contact\" });\n * await contact.submit({ name: \"Ada\" });\n * console.log(contact.data.value, contact.error.value, contact.pending.value);\n * ```\n */\n\nimport { signal } from \"@elurjs/core\";\nimport { ActionFailure, RedirectResponse } from \"../errors.js\";\n\ninterface ActionFailurePayload {\n __elur_js_action_failure?: boolean;\n status?: number;\n data?: unknown;\n}\n\ninterface RedirectPayload {\n __elur_js_action_redirect?: boolean;\n status?: number;\n location?: string;\n}\n\nfunction isActionFailurePayload(value: unknown): value is ActionFailurePayload & { __elur_js_action_failure: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_failure === true;\n}\n\nfunction isRedirectPayload(value: unknown): value is RedirectPayload & { __elur_js_action_redirect: true } {\n return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__elur_js_action_redirect === true;\n}\n\nexport interface ActionRequest {\n name: string;\n page?: string;\n args: unknown[];\n}\n\nexport interface CallActionOptions {\n /** Page URL path that scopes the action, e.g. `/contact`. */\n page?: string;\n}\n\n/**\n * Call a server action by name.\n *\n * The request is sent as a POST to `/__elur-js/actions` with the action name,\n * optional page scope, and serialized arguments. The server executes the\n * matching exported function from the scanned `page.action.ts` modules and\n * returns its JSON result.\n */\nexport async function callAction<T = unknown>(\n name: string,\n args: unknown = [],\n options: CallActionOptions = {},\n): Promise<T | ActionFailure<T> | RedirectResponse> {\n const argsArray = Array.isArray(args) ? args : [args];\n const res = await fetch(\"/__elur-js/actions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify({ name, page: options.page, args: argsArray } as ActionRequest),\n });\n\n const text = await res.text();\n if (!res.ok) {\n let payload: unknown;\n try {\n payload = JSON.parse(text);\n } catch {\n // not JSON, treat as plain error\n }\n if (isActionFailurePayload(payload) && payload.status !== undefined) {\n return new ActionFailure(payload.status, payload.data as T);\n }\n throw new Error(`Action \"${name}\" failed: ${text}`);\n }\n\n const payload: unknown = JSON.parse(text);\n if (isRedirectPayload(payload) && payload.status !== undefined && payload.location !== undefined) {\n return new RedirectResponse(payload.status, payload.location);\n }\n\n return payload as T;\n}\n\nexport interface ElurJsAction<TInput = unknown, TOutput = unknown> {\n /** Submit the action with the given input. */\n submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse>;\n /** Signal that is true while the action is running. */\n pending: { value: boolean };\n /** Signal with the last successful result, action failure, redirect, or null. */\n data: { value: TOutput | ActionFailure<TOutput> | RedirectResponse | null };\n /** Signal with the last error, or null. */\n error: { value: Error | null };\n}\n\n/**\n * Create a reactive handle for a server action.\n *\n * Returns a `submit` function and signals for `pending`, `data`, and `error`.\n * Useful for wiring actions to forms and islands without manual signal boilerplate.\n */\nexport function elurJsAction<TInput = unknown, TOutput = unknown>(\n name: string,\n options: CallActionOptions = {},\n): ElurJsAction<TInput, TOutput> {\n const pending = signal(false);\n const error = signal<Error | null>(null);\n const data = signal<TOutput | ActionFailure<TOutput> | RedirectResponse | null>(null);\n\n async function submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse> {\n pending.value = true;\n error.value = null;\n try {\n const result = await callAction<TOutput>(name, input, options);\n data.value = result;\n return result;\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err));\n throw err;\n } finally {\n pending.value = false;\n }\n }\n\n return {\n submit,\n pending,\n data,\n error,\n };\n}\n\n// Server-side: defineAction() for typed actions with validation/abort/cache.\nexport {\n defineAction,\n type DefineActionOptions,\n type DefinedAction,\n type DefinedActionFn,\n type ActionContext,\n type ActionInputValidator,\n type ActionConcurrencyMode,\n} from \"./define.js\";\n"],"mappings":";;;AAeA,IAAM,IAAsB,WAAW;AAiFvC,SAAgB,EACd,GACA,GACgC;CAChC,IAAM,IAAc,EAAQ,eAAe,UACrC,IAAa,EAAQ,cAAc,IACnC,IAAiB,EAAQ,kBAAkB,CAAC,GAC5C,IAAkB,EAAQ,mBAAmB,CAAC,GAGhD,IAAoE,MAClE,IAA2B,CAAC,GAC9B,IAAU;CAEd,eAAe,EACb,GACA,GAC2C;EAE3C,IAAI,EAAQ,OACV,IAAI;GAEF,IADkB,EAAQ,MAAM,MAAM,CAC9B;EACV,SAAS,GAAK;GACZ,IAAM,IAAU,aAAe,QAAQ,EAAI,UAAU,OAAO,CAAG,GACzD,EAAE,YAAS,MAAM,OAAO,uBAAA,CAAA,MAAA,MAAA,EAAA,CAAA;GAC9B,OAAO,EAAK,KAAK,EAAE,YAAY,EAAQ,CAAC;EAC1C;EAGF,OAAO,EAAQ,GAAO,CAAG;CAC3B;CAEA,IAAM,IAAK,OAAO,GAAe,MAAkE;EACjG,IAAI,MAAgB,UAAU;GAG5B,AADI,KAAkB,EAAiB,MAAM,GAC7C,IAAmB,IAAI,EAAoB;GAE3C,IAAM,IAAiB,EAAe,EAAI,QAAQ,EAAiB,MAAM;GACzE,OAAO,EAAI,GAAO;IAAE,GAAG;IAAK,QAAQ;GAAe,CAAC;EACtD;EAEA,IAAI,MAAgB,SAAS;GAK3B,AAHI,IAAU,KACZ,MAAM,IAAI,SAAe,MAAY,EAAM,KAAK,CAAO,CAAC,GAE1D;GACA,IAAI;IACF,OAAO,MAAM,EAAI,GAAO,CAAG;GAC7B,UAAU;IACR;IACA,IAAM,IAAO,EAAM,MAAM;IACzB,AAAI,KAAM,EAAK;GACjB;EACF;EAGA,OAAO,EAAI,GAAO,CAAG;CACvB;CAWA,OARA,EAAuC,eAAe;EACpD,MAAM,EAAQ,QAAQ;EACtB;EACA;EACA;EACA;CACF,GAEO;AACT;AAGA,SAAS,EAAe,GAAG,GAAmD;CAC5E,IAAM,IAAa,IAAI,EAAoB;CAC3C,KAAK,IAAM,KAAU,GACd,OACL;MAAI,EAAO,SAAS;GAClB,EAAW,MAAM;GACjB;EACF;EACA,EAAO,iBAAiB,eAAe,EAAW,MAAM,GAAG,EAAE,MAAM,GAAK,CAAC;CADzE;CAGF,OAAO,EAAW;AACpB;;;ACjJA,SAAS,EAAuB,GAAoF;CAClH,OAAO,OAAO,KAAU,cAAY,KAAmB,EAAkC,6BAA6B;AACxH;AAEA,SAAS,EAAkB,GAAgF;CACzG,OAAO,OAAO,KAAU,cAAY,KAAmB,EAAkC,8BAA8B;AACzH;AAqBA,eAAsB,EACpB,GACA,IAAgB,CAAC,GACjB,IAA6B,CAAC,GACoB;CAClD,IAAM,IAAY,MAAM,QAAQ,CAAI,IAAI,IAAO,CAAC,CAAI,GAC9C,IAAM,MAAM,MAAM,sBAAsB;EAC5C,QAAQ;EACR,SAAS;GACP,gBAAgB;GAChB,QAAQ;EACV;EACA,MAAM,KAAK,UAAU;GAAE;GAAM,MAAM,EAAQ;GAAM,MAAM;EAAU,CAAkB;CACrF,CAAC,GAEK,IAAO,MAAM,EAAI,KAAK;CAC5B,IAAI,CAAC,EAAI,IAAI;EACX,IAAI;EACJ,IAAI;GACF,IAAU,KAAK,MAAM,CAAI;EAC3B,QAAQ,CAER;EACA,IAAI,EAAuB,CAAO,KAAK,EAAQ,WAAW,KAAA,GACxD,OAAO,IAAI,EAAc,EAAQ,QAAQ,EAAQ,IAAS;EAE5D,MAAU,MAAM,WAAW,EAAK,YAAY,GAAM;CACpD;CAEA,IAAM,IAAmB,KAAK,MAAM,CAAI;CAKxC,OAJI,EAAkB,CAAO,KAAK,EAAQ,WAAW,KAAA,KAAa,EAAQ,aAAa,KAAA,IAC9E,IAAI,EAAiB,EAAQ,QAAQ,EAAQ,QAAQ,IAGvD;AACT;AAmBA,SAAgB,EACd,GACA,IAA6B,CAAC,GACC;CAC/B,IAAM,IAAU,EAAO,EAAK,GACtB,IAAQ,EAAqB,IAAI,GACjC,IAAO,EAAmE,IAAI;CAEpF,eAAe,EAAO,GAA6E;EAEjG,AADA,EAAQ,QAAQ,IAChB,EAAM,QAAQ;EACd,IAAI;GACF,IAAM,IAAS,MAAM,EAAoB,GAAM,GAAO,CAAO;GAE7D,OADA,EAAK,QAAQ,GACN;EACT,SAAS,GAAK;GAEZ,MADA,EAAM,QAAQ,aAAe,QAAQ,IAAU,MAAM,OAAO,CAAG,CAAC,GAC1D;EACR,UAAU;GACR,EAAQ,QAAQ;EAClB;CACF;CAEA,OAAO;EACL;EACA;EACA;EACA;CACF;AACF"}
|