@ilha/router 0.9.1 → 0.10.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/README.md +70 -33
- package/dist/head.d.ts +67 -0
- package/dist/http.d.ts +28 -0
- package/dist/index.d.ts +64 -69
- package/dist/index.js +3 -2247
- package/dist/{plugin-DogkskcY.js → plugin-CmI3Brr2.js} +107 -102
- package/dist/plugin.d.ts +18 -1
- package/dist/route-match.d.ts +22 -0
- package/dist/{rspack.d.ts → rsbuild.d.ts} +2 -2
- package/dist/rsbuild.js +10 -0
- package/dist/server-island.d.ts +17 -2
- package/dist/server-island.js +13 -25
- package/dist/server-islands.d.ts +16 -2
- package/dist/snapshot-CsEaY6h_.js +337 -0
- package/dist/snapshot.d.ts +1 -0
- package/dist/src-B5dHU24f.js +2161 -0
- package/dist/ssr-BxrcUYy5.js +498 -0
- package/dist/ssr.d.ts +182 -0
- package/dist/ssr.js +2 -159
- package/dist/vite.js +1 -1
- package/package.json +7 -14
- package/dist/request-scope-C4reU4v0.js +0 -34
- package/dist/rolldown.d.ts +0 -6
- package/dist/rolldown.js +0 -10
- package/dist/rspack.js +0 -10
- package/dist/server-island-registry.d.ts +0 -77
- package/dist/server-island-registry.js +0 -134
package/README.md
CHANGED
|
@@ -227,7 +227,7 @@ Renders `<div data-router-empty></div>` when no route matches.
|
|
|
227
227
|
|
|
228
228
|
---
|
|
229
229
|
|
|
230
|
-
#### `.renderHydratable(
|
|
230
|
+
#### `.renderHydratable(urlOrRequest, registry, options?, request?)` — server / SSR
|
|
231
231
|
|
|
232
232
|
Async variant of `.render()` that outputs HTML with `data-ilha` hydration markers so the client can rehydrate without a full re-render. If a loader is registered for the matched route, it runs first and its return value is serialized into `data-ilha-props`.
|
|
233
233
|
|
|
@@ -236,19 +236,23 @@ const html = await router().route("/", HomePage).renderHydratable("/", registry)
|
|
|
236
236
|
// → '<div data-router-view><div data-ilha="Home">…</div></div>'
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
+
All server render APIs accept a `Request` as the first argument — route, origin, headers, and loader context derive from it, so server handlers can pass the real request directly.
|
|
240
|
+
|
|
241
|
+
> **Redirects.** For callers using the string API, a loader redirect is encoded as a `<meta http-equiv="refresh">` tag. This is deprecated: it can't set a real HTTP status. Prefer `.renderResponse()` or `.respond()` to emit a proper 302.
|
|
242
|
+
|
|
239
243
|
If the active island is not found in the registry, falls back to plain SSR and emits a `console.warn`.
|
|
240
244
|
|
|
241
245
|
**Options** extend `HydratableOptions` from `ilha`:
|
|
242
246
|
|
|
243
247
|
| Option | Type | Default | Description |
|
|
244
248
|
| ---------- | --------- | ------- | ----------------------------------------------------- |
|
|
245
|
-
| `snapshot` | `boolean` | `
|
|
249
|
+
| `snapshot` | `boolean` | `false` | Embed island state as `data-ilha-state` for hydration |
|
|
246
250
|
|
|
247
251
|
---
|
|
248
252
|
|
|
249
|
-
#### `.renderResponse(
|
|
253
|
+
#### `.renderResponse(urlOrRequest, registry, options?, request?)` — server / SSR
|
|
250
254
|
|
|
251
|
-
Structured-envelope variant of `.renderHydratable()`. Returns a `RenderResponse` discriminated union instead of a raw HTML string, so the host server can emit proper HTTP status codes for redirects and loader errors.
|
|
255
|
+
Structured-envelope variant of `.renderHydratable()`. Returns a `RenderResponse` discriminated union instead of a raw HTML string, so the host server can emit proper HTTP status codes for redirects and loader errors. Accepts a `Request` as the first argument.
|
|
252
256
|
|
|
253
257
|
```ts
|
|
254
258
|
const res = await router()
|
|
@@ -270,11 +274,25 @@ return new Response(res.html, { headers: { "content-type": "text/html" } });
|
|
|
270
274
|
| `"redirect"` | `to: string`, `status: number` | Loader called `redirect()` |
|
|
271
275
|
| `"error"` | `status: number`, `message: string`, `html: string` | Loader called `error()` or threw |
|
|
272
276
|
|
|
277
|
+
#### `.respond(urlOrRequest, registry, options?)` — server / SSR
|
|
278
|
+
|
|
279
|
+
Renders a route to a ready-to-send HTTP `Response`, handling redirects, loader errors, and security headers (`Content-Type`, `X-Content-Type-Options: nosniff`, `Referrer-Policy`, `Cache-Control: no-store`, and an optional CSP nonce). Pass a `shell` to inject the serialized `<head>` into a document shell.
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
const response = await router()
|
|
283
|
+
.route("/", HomePage)
|
|
284
|
+
.respond(new Request(request.url), registry, {
|
|
285
|
+
cspNonce,
|
|
286
|
+
shell: (head, html) =>
|
|
287
|
+
`<!doctype html><html lang="en"><head>${head.headTags}</head><body>${html}</body></html>`,
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
273
291
|
---
|
|
274
292
|
|
|
275
|
-
#### `.runLoader(
|
|
293
|
+
#### `.runLoader(urlOrRequest, request?)` — server / SSR
|
|
276
294
|
|
|
277
|
-
Runs the loader chain for the matched route without rendering any HTML. Returns a discriminated union result. Used by the `/__ilha/loader` endpoint the Vite plugin exposes for client-side navigation.
|
|
295
|
+
Runs the loader chain for the matched route without rendering any HTML. Returns a discriminated union result. Used by the `/__ilha/loader` endpoint the Vite plugin exposes for client-side navigation — the originating `Request` (cookies, identity, abort signal) is forwarded to the loader through both the endpoint and this method.
|
|
278
296
|
|
|
279
297
|
```ts
|
|
280
298
|
const result = await router().route("/user/:id", userPage, userLoader).runLoader("/user/42");
|
|
@@ -378,10 +396,17 @@ router()
|
|
|
378
396
|
Attaches the route's `+error` boundary so **loader** errors render through it — on the server (`renderResponse` returns the boundary's HTML with the error status) and on client navigations. Render errors are already handled by `wrapError` inside the island; this closes the gap for errors thrown before rendering starts. The FS-routing codegen wires the nearest `+error.ts` automatically; manual routers can call it directly. A throwing boundary falls back to the minimal inline error.
|
|
379
397
|
|
|
380
398
|
```ts
|
|
399
|
+
import { router } from "@ilha/router";
|
|
400
|
+
import { ilha, html } from "ilha";
|
|
401
|
+
|
|
381
402
|
router()
|
|
382
403
|
.route("/user/:id", userPage, userLoader)
|
|
383
404
|
.errorBoundary("/user/:id", (err, route) =>
|
|
384
|
-
ilha
|
|
405
|
+
ilha(
|
|
406
|
+
() =>
|
|
407
|
+
html`<h1>${err.status ?? 500}</h1>
|
|
408
|
+
<p>${err.message}</p>`,
|
|
409
|
+
),
|
|
385
410
|
);
|
|
386
411
|
```
|
|
387
412
|
|
|
@@ -425,8 +450,9 @@ Reactive — `true` while a client navigation (loader fetch + view swap) is in f
|
|
|
425
450
|
|
|
426
451
|
```ts
|
|
427
452
|
import { navigating } from "@ilha/router";
|
|
453
|
+
import { ilha, html } from "ilha";
|
|
428
454
|
|
|
429
|
-
const Spinner = ilha
|
|
455
|
+
const Spinner = ilha(() => (navigating() ? html`<div class="bar" />` : ""));
|
|
430
456
|
```
|
|
431
457
|
|
|
432
458
|
---
|
|
@@ -556,10 +582,11 @@ Returns reactive signal accessors for the current route state. Safe to call insi
|
|
|
556
582
|
|
|
557
583
|
```ts
|
|
558
584
|
import { useRoute } from "@ilha/router";
|
|
585
|
+
import { ilha, html } from "ilha";
|
|
559
586
|
|
|
560
|
-
const MyPage = ilha
|
|
587
|
+
const MyPage = ilha(() => {
|
|
561
588
|
const { path, params, search, hash } = useRoute();
|
|
562
|
-
return `<p>user id: ${params().id}</p>`;
|
|
589
|
+
return html`<p>user id: ${params().id}</p>`;
|
|
563
590
|
});
|
|
564
591
|
```
|
|
565
592
|
|
|
@@ -667,10 +694,10 @@ A typed helper that returns the layout function as-is. Use it instead of the `sa
|
|
|
667
694
|
```ts
|
|
668
695
|
// src/pages/+layout.ts
|
|
669
696
|
import { defineLayout } from "@ilha/router";
|
|
670
|
-
import ilha,
|
|
697
|
+
import { ilha, html } from "ilha";
|
|
671
698
|
|
|
672
699
|
export default defineLayout((children) =>
|
|
673
|
-
ilha
|
|
700
|
+
ilha(
|
|
674
701
|
() => html`
|
|
675
702
|
<nav>
|
|
676
703
|
<a href="/">Home</a>
|
|
@@ -696,7 +723,7 @@ import { wrapError } from "@ilha/router";
|
|
|
696
723
|
const safe = wrapError(myErrorHandler, myPage);
|
|
697
724
|
```
|
|
698
725
|
|
|
699
|
-
> **Note:** Error boundaries wrap the _page island's render_, not the loader. Loader errors (thrown via `error()`)
|
|
726
|
+
> **Note:** Error boundaries wrap the _page island's render_, not the loader. Loader errors (thrown via `error()`) route through the nearest `+error.ts` / `.errorBoundary()` boundary — on the server `renderResponse` returns the boundary's HTML with the error status, and on client navigations the boundary renders in place. `wrapError` covers render/mount throws inside the island only.
|
|
700
727
|
|
|
701
728
|
---
|
|
702
729
|
|
|
@@ -725,8 +752,16 @@ interface LoaderContext {
|
|
|
725
752
|
|
|
726
753
|
type Loader<T> = (ctx: LoaderContext) => Promise<T> | T;
|
|
727
754
|
|
|
728
|
-
//
|
|
729
|
-
type InferLoader<L> = L extends Loader<infer T>
|
|
755
|
+
// Infer the complete page props produced by a loader
|
|
756
|
+
type InferLoader<L> = L extends Loader<infer T>
|
|
757
|
+
? {
|
|
758
|
+
load: {
|
|
759
|
+
loading: boolean;
|
|
760
|
+
value: Awaited<T>;
|
|
761
|
+
error: Error | undefined;
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
: never;
|
|
730
765
|
|
|
731
766
|
// Merge multiple loader return types — later loaders win on key collision
|
|
732
767
|
type MergeLoaders<Ls extends readonly Loader<any>[]> = /* … */;
|
|
@@ -882,10 +917,10 @@ A `+layout.ts` wraps every page in its directory and all subdirectories. Layouts
|
|
|
882
917
|
```ts
|
|
883
918
|
// src/pages/+layout.ts
|
|
884
919
|
import { defineLayout } from "@ilha/router";
|
|
885
|
-
import ilha,
|
|
920
|
+
import { ilha, html } from "ilha";
|
|
886
921
|
|
|
887
922
|
export default defineLayout((children) =>
|
|
888
|
-
ilha
|
|
923
|
+
ilha(
|
|
889
924
|
() => html`
|
|
890
925
|
<nav>
|
|
891
926
|
<a href="/">Home</a>
|
|
@@ -902,10 +937,10 @@ Alternatively, using the explicit type annotation:
|
|
|
902
937
|
```ts
|
|
903
938
|
// src/pages/+layout.ts — using satisfies (equivalent)
|
|
904
939
|
import type { LayoutHandler } from "@ilha/router/vite";
|
|
905
|
-
import ilha,
|
|
940
|
+
import { ilha, html } from "ilha";
|
|
906
941
|
|
|
907
942
|
export default ((children) =>
|
|
908
|
-
ilha
|
|
943
|
+
ilha(
|
|
909
944
|
() => html`
|
|
910
945
|
<nav>
|
|
911
946
|
<a href="/">Home</a>
|
|
@@ -935,14 +970,14 @@ A page file can export a `load` function declared with the `loader()` helper. Th
|
|
|
935
970
|
```ts
|
|
936
971
|
// src/pages/user/[id].ts
|
|
937
972
|
import { loader } from "@ilha/router";
|
|
938
|
-
import ilha from "ilha";
|
|
973
|
+
import { ilha, html } from "ilha";
|
|
939
974
|
|
|
940
975
|
export const load = loader(async ({ params }) => {
|
|
941
976
|
const user = await fetchUser(params.id);
|
|
942
977
|
return { user };
|
|
943
978
|
});
|
|
944
979
|
|
|
945
|
-
export default ilha
|
|
980
|
+
export default ilha<{ user: User }>(({ user }) => html`<h1>${user.name}</h1>`);
|
|
946
981
|
```
|
|
947
982
|
|
|
948
983
|
The `load` export must be declared with the `loader()` helper so the Vite plugin can identify it via export name.
|
|
@@ -971,14 +1006,14 @@ A page or layout can export a `clientLoad` function that runs **in the browser**
|
|
|
971
1006
|
```ts
|
|
972
1007
|
// src/pages/dashboard.ts
|
|
973
1008
|
import { loader } from "@ilha/router";
|
|
974
|
-
import ilha from "ilha";
|
|
1009
|
+
import { ilha } from "ilha";
|
|
975
1010
|
|
|
976
1011
|
export const clientLoad = loader(async ({ signal }) => {
|
|
977
1012
|
const stats = await fetch("/api/stats", { signal }).then((r) => r.json());
|
|
978
1013
|
return { stats };
|
|
979
1014
|
});
|
|
980
1015
|
|
|
981
|
-
export default ilha
|
|
1016
|
+
export default ilha<{ stats: Stats }>(({ stats }) => html`<pre>${JSON.stringify(stats)}</pre>`);
|
|
982
1017
|
```
|
|
983
1018
|
|
|
984
1019
|
Rules and caveats:
|
|
@@ -996,17 +1031,17 @@ A `+error.ts` catches any error thrown during rendering of pages in its director
|
|
|
996
1031
|
```ts
|
|
997
1032
|
// src/pages/+error.ts
|
|
998
1033
|
import type { ErrorHandler } from "@ilha/router/vite";
|
|
999
|
-
import ilha from "ilha";
|
|
1034
|
+
import { ilha, html } from "ilha";
|
|
1000
1035
|
|
|
1001
1036
|
export default ((error, route) =>
|
|
1002
|
-
ilha
|
|
1003
|
-
() => `
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1037
|
+
ilha(
|
|
1038
|
+
() => html`
|
|
1039
|
+
<div class="error">
|
|
1040
|
+
<h1>${error.status ?? 500}</h1>
|
|
1041
|
+
<p>${error.message}</p>
|
|
1042
|
+
<p>Path: ${route.path}</p>
|
|
1043
|
+
</div>
|
|
1044
|
+
`,
|
|
1010
1045
|
)) satisfies ErrorHandler;
|
|
1011
1046
|
```
|
|
1012
1047
|
|
|
@@ -1102,7 +1137,9 @@ Or use the one-liner: `pageRouter.hydrate(registry)`.
|
|
|
1102
1137
|
|
|
1103
1138
|
On the **server**, loaders run inside `.renderHydratable()` / `.renderResponse()`. Their return value is serialized into `data-ilha-props` on the island element so the client can rehydrate without re-fetching.
|
|
1104
1139
|
|
|
1105
|
-
On the **client**, navigations resolve loader data before mounting the next island. Routes with a loader registered in the browser — a manual `.route(path, island, loader)` or an FS-routing `clientLoad` export — run that loader locally, with no network round-trip. Routes with only a server loader (`markLoader()` / a `load` export) fetch from the `/__ilha/loader` endpoint, served automatically by the Vite plugin (dev) and the server adapter (production).
|
|
1140
|
+
On the **client**, navigations resolve loader data before mounting the next island. Routes with a loader registered in the browser — a manual `.route(path, island, loader)` or an FS-routing `clientLoad` export — run that loader locally, with no network round-trip. Routes with only a server loader (`markLoader()` / a `load` export) fetch from the `/__ilha/loader` endpoint, served automatically by the Vite plugin (dev) and the server adapter (production). The originating `Request` (cookies, identity, abort signal) is forwarded to the loader and the island-request scope, so `ctx.request` and `useContext().request` behave in client navigations exactly as they do during SSR.
|
|
1141
|
+
|
|
1142
|
+
Like `/__ilha/frame`, the loader endpoint is **denied by default** in production when no guard is registered — gate it with `setLoaderGuard()` (or the shared frame guard / `defaultAction: "open"` policy) or client navigations to server-loader routes return 403.
|
|
1106
1143
|
|
|
1107
1144
|
```
|
|
1108
1145
|
server client (navigation)
|
package/dist/head.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render-scoped head collection and serialization.
|
|
3
|
+
*
|
|
4
|
+
* Loaders and render-time `head()` calls push `HeadInput` entries into a
|
|
5
|
+
* store scoped to the current render (`withHeadStore`); `serializeHead`
|
|
6
|
+
* turns the collected entries into document-shell fragments.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Serializable description of `<head>` (and html/body attributes) contributed
|
|
10
|
+
* by a loader or a render-time `head()` call. Deliberately a plain POJO — Tier
|
|
11
|
+
* 1 head management is SSR-only, so there is no reactive wrapper. Dedup keys
|
|
12
|
+
* mirror unhead so a later move to a runtime head manager stays a drop-in.
|
|
13
|
+
*/
|
|
14
|
+
export interface HeadInput {
|
|
15
|
+
title?: string;
|
|
16
|
+
/** Wrap the resolved title. The last template in merge order wins. */
|
|
17
|
+
titleTemplate?: string | ((title?: string) => string);
|
|
18
|
+
meta?: Array<Record<string, string>>;
|
|
19
|
+
link?: Array<Record<string, string>>;
|
|
20
|
+
/**
|
|
21
|
+
* Inline script bodies are emitted raw in SSR (`serializeHead`). Must be trusted
|
|
22
|
+
* app code and must not contain a literal `</script>` sequence.
|
|
23
|
+
*/
|
|
24
|
+
script?: Array<Record<string, string> & {
|
|
25
|
+
children?: string;
|
|
26
|
+
}>;
|
|
27
|
+
htmlAttrs?: Record<string, string>;
|
|
28
|
+
bodyAttrs?: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
/** Serialized head fragments ready to inject into a document shell. */
|
|
31
|
+
export interface SerializedHead {
|
|
32
|
+
/** Markup for inside `<head>` (title, meta, link, script). */
|
|
33
|
+
headTags: string;
|
|
34
|
+
/** Attribute string for the `<html>` tag (leading space included). */
|
|
35
|
+
htmlAttrs: string;
|
|
36
|
+
/** Attribute string for the `<body>` tag (leading space included). */
|
|
37
|
+
bodyAttrs: string;
|
|
38
|
+
}
|
|
39
|
+
export interface HeadStore {
|
|
40
|
+
entries: HeadInput[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Contribute `<head>` data from inside an island's `.render()` body or a
|
|
44
|
+
* layout. During SSR this collects into the active render window; on the
|
|
45
|
+
* client, entries are collected when the router re-renders a route inside
|
|
46
|
+
* `withHeadStore` and then applied to `document`. Prefer a loader's `ctx.head`
|
|
47
|
+
* for data that depends on the request.
|
|
48
|
+
*/
|
|
49
|
+
export declare function head(input: HeadInput): void;
|
|
50
|
+
export declare function cssEscapeAttr(value: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* Apply merged head entries on client navigations. Updates `document.title` and
|
|
53
|
+
* managed meta/link nodes (`data-ilha-head`). Script tags from HeadInput are
|
|
54
|
+
* SSR-only and are not re-injected here. Removes managed tags from the previous
|
|
55
|
+
* route that are not part of this navigation's set.
|
|
56
|
+
*/
|
|
57
|
+
export declare function applyHeadEntriesToDocument(entries: HeadInput[]): void;
|
|
58
|
+
export declare function withHeadStore<T>(store: HeadStore, fn: () => T | Promise<T>): Promise<T>;
|
|
59
|
+
export declare function escapeHeadAttr(value: unknown): string;
|
|
60
|
+
/** Escape text content for inline HTML (loader error messages etc.). */
|
|
61
|
+
export declare function escapeHtml(value: unknown): string;
|
|
62
|
+
/**
|
|
63
|
+
* Merge head entries in contribution order (loader first as the base, then
|
|
64
|
+
* render-time outer→inner layouts, then the page) and serialize. Later entries
|
|
65
|
+
* win on collision; the last `titleTemplate` wraps the resolved title.
|
|
66
|
+
*/
|
|
67
|
+
export declare function serializeHead(entries: HeadInput[]): SerializedHead;
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone HTTP response helper for custom server handlers.
|
|
3
|
+
*/
|
|
4
|
+
import type { SerializedHead } from "./head";
|
|
5
|
+
export interface HttpResponseOptions {
|
|
6
|
+
status?: number;
|
|
7
|
+
headers?: HeadersInit;
|
|
8
|
+
/**
|
|
9
|
+
* CSP nonce for inline scripts. When set (and `contentSecurityPolicy` is
|
|
10
|
+
* not), a conservative default CSP is emitted with `'nonce-${nonce}'` for
|
|
11
|
+
* `script-src` — pass the same nonce to head `<script nonce=…>` tags.
|
|
12
|
+
*/
|
|
13
|
+
cspNonce?: string;
|
|
14
|
+
/** Full `Content-Security-Policy` string; overrides the nonce-derived default. */
|
|
15
|
+
contentSecurityPolicy?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build an HTTP `Response` for SSR output with sensible security headers:
|
|
19
|
+
* `Content-Type: text/html`, `X-Content-Type-Options: nosniff`,
|
|
20
|
+
* `Referrer-Policy`, `Cache-Control: no-store`, and an optional CSP. This is
|
|
21
|
+
* a low-level helper — prefer {@link RouterBuilder.respond} for the full
|
|
22
|
+
* render+head+headers pipeline.
|
|
23
|
+
*/
|
|
24
|
+
export declare function httpResponse(body: string | null, options?: HttpResponseOptions): Response;
|
|
25
|
+
export declare const EMPTY_HEAD: SerializedHead;
|
|
26
|
+
/**
|
|
27
|
+
* Options for {@link RouterBuilder.respond}.
|
|
28
|
+
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Island, HydratableOptions } from "ilha";
|
|
2
|
+
import { head, type HeadInput, type SerializedHead } from "./head";
|
|
3
|
+
import { type HttpResponseOptions } from "./http";
|
|
2
4
|
export { setHistoryMode, getHistoryMode } from "./hash";
|
|
3
5
|
export type { HistoryMode } from "./hash";
|
|
4
6
|
export interface RouteRecord {
|
|
5
7
|
pattern: string;
|
|
6
|
-
island: Island<any
|
|
8
|
+
island: Island<any>;
|
|
7
9
|
/** Merged loader chain (layouts outer→inner, then page) — `undefined` if no loaders. */
|
|
8
10
|
loader?: Loader<any>;
|
|
9
11
|
/** True when the route has a server-side loader, even if the client only has a marker. */
|
|
@@ -20,39 +22,8 @@ export interface AppError {
|
|
|
20
22
|
status?: number;
|
|
21
23
|
stack?: string;
|
|
22
24
|
}
|
|
23
|
-
export type LayoutHandler = (children: Island<any
|
|
24
|
-
export type ErrorHandler = (error: AppError, route: RouteSnapshot) => Island<any
|
|
25
|
-
/**
|
|
26
|
-
* Serializable description of `<head>` (and html/body attributes) contributed
|
|
27
|
-
* by a loader or a render-time `head()` call. Deliberately a plain POJO — Tier
|
|
28
|
-
* 1 head management is SSR-only, so there is no reactive wrapper. Dedup keys
|
|
29
|
-
* mirror unhead so a later move to a runtime head manager stays a drop-in.
|
|
30
|
-
*/
|
|
31
|
-
export interface HeadInput {
|
|
32
|
-
title?: string;
|
|
33
|
-
/** Wrap the resolved title. The last template in merge order wins. */
|
|
34
|
-
titleTemplate?: string | ((title?: string) => string);
|
|
35
|
-
meta?: Array<Record<string, string>>;
|
|
36
|
-
link?: Array<Record<string, string>>;
|
|
37
|
-
/**
|
|
38
|
-
* Inline script bodies are emitted raw in SSR (`serializeHead`). Must be trusted
|
|
39
|
-
* app code and must not contain a literal `</script>` sequence.
|
|
40
|
-
*/
|
|
41
|
-
script?: Array<Record<string, string> & {
|
|
42
|
-
children?: string;
|
|
43
|
-
}>;
|
|
44
|
-
htmlAttrs?: Record<string, string>;
|
|
45
|
-
bodyAttrs?: Record<string, string>;
|
|
46
|
-
}
|
|
47
|
-
/** Serialized head fragments ready to inject into a document shell. */
|
|
48
|
-
export interface SerializedHead {
|
|
49
|
-
/** Markup for inside `<head>` (title, meta, link, script). */
|
|
50
|
-
headTags: string;
|
|
51
|
-
/** Attribute string for the `<html>` tag (leading space included). */
|
|
52
|
-
htmlAttrs: string;
|
|
53
|
-
/** Attribute string for the `<body>` tag (leading space included). */
|
|
54
|
-
bodyAttrs: string;
|
|
55
|
-
}
|
|
25
|
+
export type LayoutHandler = (children: Island<any>) => Island<any>;
|
|
26
|
+
export type ErrorHandler = (error: AppError, route: RouteSnapshot) => Island<any>;
|
|
56
27
|
export interface LoaderContext {
|
|
57
28
|
params: Record<string, string>;
|
|
58
29
|
request: Request;
|
|
@@ -74,8 +45,15 @@ export declare function loader<T>(fn: Loader<T>): Loader<T>;
|
|
|
74
45
|
export declare namespace loader {
|
|
75
46
|
var client: <T>(fn: Loader<T>) => Loader<T>;
|
|
76
47
|
}
|
|
77
|
-
/**
|
|
78
|
-
export type InferLoader<L> = L extends Loader<infer T> ?
|
|
48
|
+
/** Infer the page props produced by a loader. */
|
|
49
|
+
export type InferLoader<L> = L extends Loader<infer T> ? {
|
|
50
|
+
load: {
|
|
51
|
+
loading: boolean;
|
|
52
|
+
value: Awaited<T>;
|
|
53
|
+
error: Error | undefined;
|
|
54
|
+
};
|
|
55
|
+
} : never;
|
|
56
|
+
type InferLoaderValue<L> = L extends Loader<infer T> ? Awaited<T> : never;
|
|
79
57
|
/**
|
|
80
58
|
* Merge multiple loader return types into a single object type.
|
|
81
59
|
* Later loaders override earlier ones on key collision — matching runtime merge.
|
|
@@ -86,7 +64,7 @@ export type InferLoader<L> = L extends Loader<infer T> ? Awaited<T> : never;
|
|
|
86
64
|
export type MergeLoaders<Ls extends readonly Loader<any>[]> = Ls extends readonly [
|
|
87
65
|
infer First extends Loader<any>,
|
|
88
66
|
...infer Rest extends readonly Loader<any>[]
|
|
89
|
-
] ? Rest extends readonly [] ?
|
|
67
|
+
] ? Rest extends readonly [] ? InferLoaderValue<First> : Omit<InferLoaderValue<First>, keyof MergeLoaders<Rest>> & MergeLoaders<Rest> : {};
|
|
90
68
|
export declare class Redirect {
|
|
91
69
|
readonly __ilhaRedirect: true;
|
|
92
70
|
readonly to: string;
|
|
@@ -111,8 +89,8 @@ export declare function error(status: number, message: string): never;
|
|
|
111
89
|
* `Redirect` or `LoaderError`, the composed loader re-throws it unchanged.
|
|
112
90
|
*/
|
|
113
91
|
export declare function composeLoaders<Ls extends readonly Loader<any>[]>(loaders: Ls): Loader<MergeLoaders<Ls>>;
|
|
114
|
-
export declare function wrapLayout(layout: LayoutHandler, page: Island<any
|
|
115
|
-
export declare function wrapError(handler: ErrorHandler, page: Island<any
|
|
92
|
+
export declare function wrapLayout(layout: LayoutHandler, page: Island<any>): Island<any>;
|
|
93
|
+
export declare function wrapError(handler: ErrorHandler, page: Island<any>): Island<any>;
|
|
116
94
|
export declare function defineLayout(layout: LayoutHandler): LayoutHandler;
|
|
117
95
|
export interface NavigateOptions {
|
|
118
96
|
replace?: boolean;
|
|
@@ -144,7 +122,7 @@ export interface RouterOptions {
|
|
|
144
122
|
* Island rendered when no route matches the current URL — both on the
|
|
145
123
|
* server (with a 404 status) and in the client `RouterView`.
|
|
146
124
|
*/
|
|
147
|
-
notFound?: Island<any
|
|
125
|
+
notFound?: Island<any>;
|
|
148
126
|
/**
|
|
149
127
|
* Allow loader `redirect()` targets pointing at other origins. When `false`
|
|
150
128
|
* (default), absolute cross-origin redirect targets are rejected with a 500
|
|
@@ -184,7 +162,7 @@ export interface HydrateOptions {
|
|
|
184
162
|
}
|
|
185
163
|
export interface MountOptions {
|
|
186
164
|
hydrate?: boolean;
|
|
187
|
-
registry?: Record<string, Island<any
|
|
165
|
+
registry?: Record<string, Island<any>>;
|
|
188
166
|
/**
|
|
189
167
|
* When `true` (default), internal `<a>` clicks are intercepted for
|
|
190
168
|
* client-side navigation. Set to `false` for MPA-style full-page navigations.
|
|
@@ -214,7 +192,7 @@ export interface RouterBuilder {
|
|
|
214
192
|
* (layout loaders outer→inner followed by the page loader) produced by
|
|
215
193
|
* the FS-routing codegen.
|
|
216
194
|
*/
|
|
217
|
-
route(pattern: string, island: Island<any
|
|
195
|
+
route(pattern: string, island: Island<any>, loader?: Loader<any>): RouterBuilder;
|
|
218
196
|
/**
|
|
219
197
|
* Attach (or replace) a loader on an already-registered route pattern.
|
|
220
198
|
* Used by the `ilha:loaders` virtual module to wire server-only loaders
|
|
@@ -253,20 +231,20 @@ export interface RouterBuilder {
|
|
|
253
231
|
prime(): void;
|
|
254
232
|
mount(target: string | Element, options?: MountOptions): () => void;
|
|
255
233
|
render(url: string | URL): string;
|
|
256
|
-
renderHydratable(
|
|
234
|
+
renderHydratable(urlOrRequest: string | URL | Request, registry: Record<string, Island<any>>, options?: HydratableRenderOptions, request?: Request): Promise<string>;
|
|
257
235
|
/**
|
|
258
236
|
* Like `renderHydratable` but surfaces loader redirects and errors as
|
|
259
237
|
* structured responses instead of baking them into HTML. Prefer this from
|
|
260
238
|
* host server code so you can emit proper 302 / 4xx responses.
|
|
261
239
|
*/
|
|
262
|
-
renderResponse(
|
|
240
|
+
renderResponse(urlOrRequest: string | URL | Request, registry: Record<string, Island<any>>, options?: HydratableRenderOptions, request?: Request): Promise<RenderResponse>;
|
|
263
241
|
/**
|
|
264
242
|
* Run the loader chain for a given URL without rendering. Backs the
|
|
265
243
|
* `/__ilha/loader` endpoint that the host server handler
|
|
266
244
|
* serves as JSON for client-side navigation. Returns the raw loader result, a
|
|
267
245
|
* redirect sentinel, or an error sentinel.
|
|
268
246
|
*/
|
|
269
|
-
runLoader(
|
|
247
|
+
runLoader(urlOrRequest: string | URL | Request, request?: Request): Promise<{
|
|
270
248
|
kind: "data";
|
|
271
249
|
data: Record<string, unknown>;
|
|
272
250
|
head?: SerializedHead;
|
|
@@ -281,19 +259,25 @@ export interface RouterBuilder {
|
|
|
281
259
|
} | {
|
|
282
260
|
kind: "not-found";
|
|
283
261
|
}>;
|
|
262
|
+
/**
|
|
263
|
+
* Render a route to a ready-to-send HTTP `Response`, handling redirects,
|
|
264
|
+
* loader errors, and security headers. `request` (or a URL string) selects
|
|
265
|
+
* the route; the optional `shell` injects `head` tags into a document.
|
|
266
|
+
*/
|
|
267
|
+
respond(urlOrRequest: string | URL | Request, registry: Record<string, Island<any>>, options?: RespondOptions): Promise<Response>;
|
|
284
268
|
/**
|
|
285
269
|
* Hydrate the application - combines prime(), mount(), and router.mount() into one call.
|
|
286
270
|
* @param registry - The island registry from ilha:registry
|
|
287
271
|
* @param options - Optional root element (defaults to document.body) and router target (defaults to root)
|
|
288
272
|
* @returns Cleanup function
|
|
289
273
|
*/
|
|
290
|
-
hydrate(registry: Record<string, Island<any
|
|
274
|
+
hydrate(registry: Record<string, Island<any>>, options?: HydrateOptions): () => void;
|
|
291
275
|
/**
|
|
292
276
|
* Hydrate islands on the current pre-rendered page without mounting a route
|
|
293
277
|
* view or enabling client navigation. Intended for `static` mode: each page
|
|
294
278
|
* is a self-contained HTML file; only interactive islands need activation.
|
|
295
279
|
*/
|
|
296
|
-
hydrateStatic(registry: Record<string, Island<any
|
|
280
|
+
hydrateStatic(registry: Record<string, Island<any>>, options?: {
|
|
297
281
|
root?: Element;
|
|
298
282
|
}): () => void;
|
|
299
283
|
}
|
|
@@ -305,9 +289,13 @@ export declare const LOADER_ENDPOINT = "/__ilha/loader";
|
|
|
305
289
|
* navigation) or is superseded by another prefetch.
|
|
306
290
|
*/
|
|
307
291
|
export declare function prefetch(pathWithSearch: string): void;
|
|
292
|
+
/** @internal Internal reactive route-state accessor; use the aggregate `useRoute()` for the public surface. */
|
|
308
293
|
export declare function routePath(value?: string): string;
|
|
294
|
+
/** @internal Internal reactive route-state accessor; use the aggregate `useRoute()` for the public surface. */
|
|
309
295
|
export declare function routeParams(value?: Record<string, string>): Record<string, string>;
|
|
296
|
+
/** @internal Internal reactive route-state accessor; use the aggregate `useRoute()` for the public surface. */
|
|
310
297
|
export declare function routeSearch(value?: string): string;
|
|
298
|
+
/** @internal Internal reactive route-state accessor; use the aggregate `useRoute()` for the public surface. */
|
|
311
299
|
export declare function routeHash(value?: string): string;
|
|
312
300
|
/** Reactive: `true` while a client navigation (loader fetch + view swap) is in flight. */
|
|
313
301
|
export declare function navigating(): boolean;
|
|
@@ -384,12 +372,11 @@ export interface LinkInterceptionOptions {
|
|
|
384
372
|
prefetch?: boolean;
|
|
385
373
|
}
|
|
386
374
|
export declare function enableLinkInterception(root?: Element | Document, options?: LinkInterceptionOptions): () => void;
|
|
387
|
-
export declare const RouterView: Island<
|
|
388
|
-
[x: string]: unknown;
|
|
389
|
-
}, {}>;
|
|
375
|
+
export declare const RouterView: Island<unknown>;
|
|
390
376
|
export declare const RouterLink: Island<{
|
|
391
|
-
|
|
392
|
-
|
|
377
|
+
href?: string;
|
|
378
|
+
label?: string;
|
|
379
|
+
}>;
|
|
393
380
|
export interface IsActiveOptions {
|
|
394
381
|
/**
|
|
395
382
|
* When `false`, `isActive("/docs")` also matches nested paths like
|
|
@@ -399,20 +386,27 @@ export interface IsActiveOptions {
|
|
|
399
386
|
exact?: boolean;
|
|
400
387
|
}
|
|
401
388
|
export declare function isActive(pattern: string, options?: IsActiveOptions): boolean;
|
|
389
|
+
export interface RespondOptions extends HydratableRenderOptions, HttpResponseOptions {
|
|
390
|
+
/**
|
|
391
|
+
* Wrap the rendered body with a document shell. Receives the serialized
|
|
392
|
+
* head (title/meta/link/script tags + html/body attributes) and the inner
|
|
393
|
+
* HTML; return the full document.
|
|
394
|
+
*/
|
|
395
|
+
shell?: (head: SerializedHead, html: string) => string;
|
|
396
|
+
}
|
|
402
397
|
/**
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*/
|
|
409
|
-
export declare function head(input: HeadInput): void;
|
|
410
|
-
/**
|
|
411
|
-
* Merge head entries in contribution order (loader first as the base, then
|
|
412
|
-
* render-time outer→inner layouts, then the page) and serialize. Later entries
|
|
413
|
-
* win on collision; the last `titleTemplate` wraps the resolved title.
|
|
398
|
+
* Validate a loader redirect target. Relative paths always pass; same-origin
|
|
399
|
+
* absolute URLs collapse to a path; cross-origin targets (including
|
|
400
|
+
* protocol-relative `//host` URLs) are rejected unless `allowExternal` is set.
|
|
401
|
+
* Non-http(s) schemes, backslash/control-char smuggling, and unparsable
|
|
402
|
+
* targets are always rejected.
|
|
414
403
|
*/
|
|
415
|
-
export declare function
|
|
404
|
+
export declare function resolveRedirectTarget(to: string, base: URL, allowExternal: boolean): {
|
|
405
|
+
ok: true;
|
|
406
|
+
to: string;
|
|
407
|
+
} | {
|
|
408
|
+
ok: false;
|
|
409
|
+
};
|
|
416
410
|
export declare function router(options?: RouterOptions): RouterBuilder;
|
|
417
411
|
declare const _default: {
|
|
418
412
|
router: typeof router;
|
|
@@ -424,12 +418,11 @@ declare const _default: {
|
|
|
424
418
|
prefetch: typeof prefetch;
|
|
425
419
|
beforeNavigate: typeof beforeNavigate;
|
|
426
420
|
afterNavigate: typeof afterNavigate;
|
|
427
|
-
RouterView: Island<
|
|
428
|
-
[x: string]: unknown;
|
|
429
|
-
}, {}>;
|
|
421
|
+
RouterView: Island<unknown>;
|
|
430
422
|
RouterLink: Island<{
|
|
431
|
-
|
|
432
|
-
|
|
423
|
+
href?: string;
|
|
424
|
+
label?: string;
|
|
425
|
+
}>;
|
|
433
426
|
loader: typeof loader;
|
|
434
427
|
redirect: typeof redirect;
|
|
435
428
|
error: typeof error;
|
|
@@ -437,3 +430,5 @@ declare const _default: {
|
|
|
437
430
|
head: typeof head;
|
|
438
431
|
};
|
|
439
432
|
export default _default;
|
|
433
|
+
export { head, serializeHead, type HeadInput, type SerializedHead } from "./head";
|
|
434
|
+
export { httpResponse, type HttpResponseOptions } from "./http";
|