@ilha/router 0.1.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/README.md +547 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.js +342 -0
- package/dist/vite.d.ts +9816 -0
- package/dist/vite.js +205 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
# `@ilha/router`
|
|
2
|
+
|
|
3
|
+
A lightweight, isomorphic router for [Ilha](https://github.com/ilhajs/ilha) islands. Runs in the browser with full reactivity and on the server as a synchronous HTML string renderer. Pairs natively with [Nitro](https://nitro.build/) and includes a Vite plugin for file-system based routing.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @ilha/router
|
|
11
|
+
# or Bun
|
|
12
|
+
bun add @ilha/router
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### Client-side
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { router } from "@ilha/router";
|
|
23
|
+
import { homePage, aboutPage, userPage, notFound } from "./pages";
|
|
24
|
+
|
|
25
|
+
router()
|
|
26
|
+
.route("/", homePage)
|
|
27
|
+
.route("/about", aboutPage)
|
|
28
|
+
.route("/user/:id", userPage)
|
|
29
|
+
.route("/**", notFound)
|
|
30
|
+
.mount("#app");
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Server-side (SSR)
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { router } from "@ilha/router";
|
|
37
|
+
import { homePage, aboutPage, userPage, notFound } from "./pages";
|
|
38
|
+
|
|
39
|
+
export default defineEventHandler((event) => {
|
|
40
|
+
const html = router()
|
|
41
|
+
.route("/", homePage)
|
|
42
|
+
.route("/about", aboutPage)
|
|
43
|
+
.route("/user/:id", userPage)
|
|
44
|
+
.route("/**", notFound)
|
|
45
|
+
.render(event.node.req.url ?? "/");
|
|
46
|
+
|
|
47
|
+
return new Response(`<!doctype html><html><body>${html}</body></html>`, {
|
|
48
|
+
headers: { "content-type": "text/html" },
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### SSR + Client Hydration (recommended)
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// routes/[...].ts — Nitro handler
|
|
57
|
+
import { pageRouter } from "ilha:pages";
|
|
58
|
+
import { registry } from "ilha:registry";
|
|
59
|
+
|
|
60
|
+
export default defineEventHandler(async (event) => {
|
|
61
|
+
const html = await pageRouter.renderHydratable(event.node.req.url ?? "/", registry);
|
|
62
|
+
return new Response(`<!doctype html><html><body>${html}</body></html>`, {
|
|
63
|
+
headers: { "content-type": "text/html" },
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// src/client.ts — browser entry
|
|
70
|
+
import { pageRouter } from "ilha:pages";
|
|
71
|
+
import { registry } from "ilha:registry";
|
|
72
|
+
|
|
73
|
+
pageRouter.hydrate(registry);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Core API
|
|
79
|
+
|
|
80
|
+
### `router()`
|
|
81
|
+
|
|
82
|
+
Creates a new router instance and **resets the route registry**. Always call `router()` fresh — never share instances across server requests.
|
|
83
|
+
|
|
84
|
+
Returns a `RouterBuilder`.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
#### `.route(pattern, island)`
|
|
89
|
+
|
|
90
|
+
Registers a route. Patterns are matched in **declaration order** — first match wins. Uses [rou3](https://github.com/h3js/rou3) for matching, the same engine as Nitro.
|
|
91
|
+
|
|
92
|
+
| Pattern | Matches | `routeParams()` |
|
|
93
|
+
| --------------- | ------------------- | --------------------------------- |
|
|
94
|
+
| `/` | `/` | `{}` |
|
|
95
|
+
| `/about` | `/about` | `{}` |
|
|
96
|
+
| `/user/:id` | `/user/42` | `{ id: "42" }` |
|
|
97
|
+
| `/:org/:repo` | `/ilha/router` | `{ org: "ilha", repo: "router" }` |
|
|
98
|
+
| `/docs/**:slug` | `/docs/guide/intro` | `{ slug: "guide/intro" }` |
|
|
99
|
+
| `/**` | anything | `{}` |
|
|
100
|
+
|
|
101
|
+
> Static segments take priority over `:param` segments — `/user/me` will match before `/user/:id`.
|
|
102
|
+
|
|
103
|
+
Returns the same `RouterBuilder` for chaining.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
#### `.mount(target, options?)` — browser only
|
|
108
|
+
|
|
109
|
+
Mounts the router into a DOM element or CSS selector. Sets up `popstate` listening and intercepts internal `<a>` clicks automatically.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const unmount = router().route("/", homePage).mount("#app");
|
|
113
|
+
|
|
114
|
+
// later:
|
|
115
|
+
unmount();
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Options:**
|
|
119
|
+
|
|
120
|
+
| Option | Type | Default | Description |
|
|
121
|
+
| ---------- | ------------------------ | ----------- | ---------------------------------------------------------- |
|
|
122
|
+
| `hydrate` | `boolean` | `false` | Preserve SSR DOM on first mount (no destructive re-render) |
|
|
123
|
+
| `registry` | `Record<string, Island>` | `undefined` | Island registry for interactive hydration on navigation |
|
|
124
|
+
|
|
125
|
+
When `hydrate: true`, `.mount()` does **not** wipe existing SSR HTML. It instead mounts a hidden navigation handler that re-renders routes with hydration on subsequent navigations.
|
|
126
|
+
|
|
127
|
+
No-op with a console warning when called outside a browser environment.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
#### `.render(url)` — server / SSR
|
|
132
|
+
|
|
133
|
+
Resolves the given URL against the route registry and returns a synchronous HTML string. Accepts a path string, full URL string, or `URL` object. Populates all route signals identically to the browser. Percent-encoded params are decoded automatically.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const html = router().route("/", homePage).route("/**", notFound).render("/");
|
|
137
|
+
// → '<div data-router-view><p>home</p></div>'
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Renders `<div data-router-empty></div>` when no route matches.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
#### `.renderHydratable(url, registry, options?)` — server / SSR
|
|
145
|
+
|
|
146
|
+
Async variant of `.render()` that outputs HTML with `data-ilha` hydration markers so the client can rehydrate without a full re-render.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const html = await router().route("/", homePage).renderHydratable("/", registry);
|
|
150
|
+
// → '<div data-router-view><div data-ilha="home">…</div></div>'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
If the active island is not found in the registry, falls back to plain SSR and emits a `console.warn`.
|
|
154
|
+
|
|
155
|
+
**Options** extend `HydratableOptions` from `ilha`:
|
|
156
|
+
|
|
157
|
+
| Option | Type | Default | Description |
|
|
158
|
+
| ---------- | --------- | ------- | ----------------------------------------------------- |
|
|
159
|
+
| `snapshot` | `boolean` | `true` | Embed island state as `data-ilha-state` for hydration |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
#### `.prime()` — browser only
|
|
164
|
+
|
|
165
|
+
Primes route context signals from the current `window.location` **before** `ilha.mount()` runs. This prevents a signal mismatch that would destroy hydrated bindings.
|
|
166
|
+
|
|
167
|
+
Call this after all routes are registered and before mounting islands for interactivity:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { mount } from "ilha";
|
|
171
|
+
import { pageRouter } from "ilha:pages";
|
|
172
|
+
import { registry } from "ilha:registry";
|
|
173
|
+
|
|
174
|
+
pageRouter.prime(); // ← sync signals first
|
|
175
|
+
mount(registry, { root: … }); // ← then hydrate islands
|
|
176
|
+
pageRouter.mount("#app", { hydrate: true, registry });
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
#### `.hydrate(registry, options?)` — browser only
|
|
182
|
+
|
|
183
|
+
Convenience method that combines `.prime()`, `ilha.mount()`, and `.mount()` into a single call. Use this as the recommended client entry point.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
pageRouter.hydrate(registry);
|
|
187
|
+
|
|
188
|
+
// With options:
|
|
189
|
+
pageRouter.hydrate(registry, {
|
|
190
|
+
root: document.getElementById("root"), // defaults to document.body
|
|
191
|
+
target: "#app", // defaults to root
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Returns an `unmount` function that tears down all listeners and hydrated islands.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### `navigate(to, options?)`
|
|
200
|
+
|
|
201
|
+
Programmatically navigate to a path. Updates the URL, history stack, and all reactive signals. Duplicate navigations (same URL) are no-ops.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { navigate } from "@ilha/router";
|
|
205
|
+
|
|
206
|
+
navigate("/about");
|
|
207
|
+
navigate("/about", { replace: true }); // replaces instead of pushing
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
No-op on the server.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### `prime()`
|
|
215
|
+
|
|
216
|
+
Standalone export of the same signal-priming function available as `.prime()` on the builder. Useful when managing the priming step separately from the router instance.
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import { prime } from "@ilha/router";
|
|
220
|
+
|
|
221
|
+
prime();
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### `useRoute()`
|
|
227
|
+
|
|
228
|
+
Returns reactive signal accessors for the current route state. Safe to call inside any island render function on both client and server.
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import { useRoute } from "@ilha/router";
|
|
232
|
+
|
|
233
|
+
const MyPage = ilha.render(() => {
|
|
234
|
+
const { path, params, search, hash } = useRoute();
|
|
235
|
+
return `<p>user id: ${params().id}</p>`;
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
### `routePath` · `routeParams` · `routeSearch` · `routeHash`
|
|
242
|
+
|
|
243
|
+
The underlying context signals — use these outside of islands when you need direct signal access.
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { routePath, routeParams, routeSearch, routeHash } from "@ilha/router";
|
|
247
|
+
|
|
248
|
+
routePath(); // → "/user/42"
|
|
249
|
+
routeParams(); // → { id: "42" }
|
|
250
|
+
routeSearch(); // → "?tab=docs"
|
|
251
|
+
routeHash(); // → "#section"
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
### `isActive(pattern)`
|
|
257
|
+
|
|
258
|
+
Returns `true` if the current path matches the given registered pattern. Uses O(1) reverse island lookup internally.
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import { isActive } from "@ilha/router";
|
|
262
|
+
|
|
263
|
+
isActive("/about"); // → true / false
|
|
264
|
+
isActive("/user/:id"); // → true when on any /user/* path
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
### `enableLinkInterception(root?)`
|
|
270
|
+
|
|
271
|
+
Attaches a delegated click listener to `root` (defaults to `document`) that intercepts `<a>` clicks and routes them client-side. Called automatically by `.mount()`.
|
|
272
|
+
|
|
273
|
+
Skips links that are external, `target="_blank"`, anchor-only (`#hash`), or modified (`Ctrl`/`Meta`/`Shift`). Also skips events already handled (`e.defaultPrevented`).
|
|
274
|
+
|
|
275
|
+
Returns a cleanup function.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
const stop = enableLinkInterception(myContainer);
|
|
279
|
+
stop(); // remove listener
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
No-op on the server.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
### `RouterView`
|
|
287
|
+
|
|
288
|
+
The outlet island rendered by `.mount()` and `.render()`. Wraps the active island in `<div data-router-view>`, or renders `<div data-router-empty></div>` when no route matches.
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import { RouterView } from "@ilha/router";
|
|
292
|
+
|
|
293
|
+
RouterView.toString(); // SSR
|
|
294
|
+
RouterView.mount(el); // client
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
### `RouterLink`
|
|
300
|
+
|
|
301
|
+
A declarative link island that calls `navigate()` on click.
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import { RouterLink } from "@ilha/router";
|
|
305
|
+
|
|
306
|
+
RouterLink.toString({ href: "/about", label: "About" });
|
|
307
|
+
// → '<a data-link href="/about">About</a>'
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
### `wrapLayout(layout, page)`
|
|
313
|
+
|
|
314
|
+
Wraps a page island with a layout handler. Used internally by the Vite plugin codegen — also available for manual composition.
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
import { wrapLayout } from "@ilha/router";
|
|
318
|
+
|
|
319
|
+
const wrapped = wrapLayout(myLayout, myPage);
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
### `wrapError(handler, page)`
|
|
325
|
+
|
|
326
|
+
Wraps a page island with an error boundary. If the page throws during SSR (`.toString()`), the `handler` receives the error and current route snapshot and returns a fallback island. Also intercepts errors during `.mount()` for client-side resilience.
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import { wrapError } from "@ilha/router";
|
|
330
|
+
|
|
331
|
+
const safe = wrapError(myErrorHandler, myPage);
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
The nearest (innermost) `wrapError` boundary catches first. If the inner handler re-throws, the next outer boundary takes over.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## TypeScript Types
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
interface RouteSnapshot {
|
|
342
|
+
path: string;
|
|
343
|
+
params: Record<string, string>;
|
|
344
|
+
search: string;
|
|
345
|
+
hash: string;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
interface AppError {
|
|
349
|
+
message: string;
|
|
350
|
+
status?: number;
|
|
351
|
+
stack?: string;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
type LayoutHandler = (children: Island) => Island;
|
|
355
|
+
type ErrorHandler = (error: AppError, route: RouteSnapshot) => Island;
|
|
356
|
+
|
|
357
|
+
interface NavigateOptions {
|
|
358
|
+
replace?: boolean;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
interface MountOptions {
|
|
362
|
+
hydrate?: boolean;
|
|
363
|
+
registry?: Record<string, Island>;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
interface HydrateOptions {
|
|
367
|
+
root?: Element;
|
|
368
|
+
target?: string | Element;
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## File-system Routing
|
|
375
|
+
|
|
376
|
+
`@ilha/router` includes a Vite plugin that scans `src/pages/`, resolves layout and error boundary chains, and generates a ready-to-use router — no manual route registration needed.
|
|
377
|
+
|
|
378
|
+
### Setup
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
// vite.config.ts
|
|
382
|
+
import { pages } from "@ilha/router/vite";
|
|
383
|
+
|
|
384
|
+
export default defineConfig({
|
|
385
|
+
plugins: [pages()],
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Add `.ilha/` (or your custom `generated` path) to `.gitignore`.
|
|
390
|
+
|
|
391
|
+
### Directory structure
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
src/pages/
|
|
395
|
+
+layout.ts ← root layout (wraps all pages)
|
|
396
|
+
+error.ts ← root error boundary
|
|
397
|
+
index.ts → /
|
|
398
|
+
about.ts → /about
|
|
399
|
+
user/
|
|
400
|
+
+layout.ts ← nested layout (wraps user/* only)
|
|
401
|
+
+error.ts ← nested error boundary
|
|
402
|
+
[id].ts → /user/:id
|
|
403
|
+
[id]/
|
|
404
|
+
settings.ts → /user/:id/settings
|
|
405
|
+
[...slug].ts → /**:slug
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Filename → pattern mapping
|
|
409
|
+
|
|
410
|
+
| File | Pattern |
|
|
411
|
+
| ----------------- | ------------- |
|
|
412
|
+
| `index.ts` | `/` |
|
|
413
|
+
| `about.ts` | `/about` |
|
|
414
|
+
| `[id].ts` | `/:id` |
|
|
415
|
+
| `user/[id].ts` | `/user/:id` |
|
|
416
|
+
| `[org]/[repo].ts` | `/:org/:repo` |
|
|
417
|
+
| `[...slug].ts` | `/**:slug` |
|
|
418
|
+
|
|
419
|
+
`.test.ts`, `.spec.ts`, and `.d.ts` files are automatically excluded.
|
|
420
|
+
|
|
421
|
+
### Route sorting
|
|
422
|
+
|
|
423
|
+
Routes are sorted automatically by specificity — no need to order files manually:
|
|
424
|
+
|
|
425
|
+
1. **Static** paths (`/about`) — highest priority
|
|
426
|
+
2. **Parameterised** paths (`/user/:id`)
|
|
427
|
+
3. **Wildcard** paths (`/**:slug`) — lowest priority
|
|
428
|
+
|
|
429
|
+
Within the same tier, longer segment counts and alphabetical order act as tiebreakers for determinism.
|
|
430
|
+
|
|
431
|
+
### Layouts
|
|
432
|
+
|
|
433
|
+
A `+layout.ts` wraps every page in its directory and all subdirectories. Layouts compose **inside-out** — the nearest layout is innermost, the root layout is outermost.
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
// src/pages/+layout.ts
|
|
437
|
+
import { html } from "ilha";
|
|
438
|
+
import type { LayoutHandler } from "@ilha/router/vite";
|
|
439
|
+
|
|
440
|
+
export default ((children) =>
|
|
441
|
+
ilha.render(
|
|
442
|
+
() => html`
|
|
443
|
+
<nav>
|
|
444
|
+
<a href="/">Home</a>
|
|
445
|
+
<a href="/about">About</a>
|
|
446
|
+
</nav>
|
|
447
|
+
<main>${children}</main>
|
|
448
|
+
`,
|
|
449
|
+
)) satisfies LayoutHandler;
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Error boundaries
|
|
453
|
+
|
|
454
|
+
A `+error.ts` catches any error thrown during rendering of pages in its directory and all subdirectories. The nearest boundary wins. If an inner boundary re-throws, the next outer boundary takes over. Receives the error and the current route snapshot.
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
// src/pages/+error.ts
|
|
458
|
+
import type { ErrorHandler } from "@ilha/router/vite";
|
|
459
|
+
|
|
460
|
+
export default ((error, route) =>
|
|
461
|
+
ilha.render(
|
|
462
|
+
() => `
|
|
463
|
+
<div class="error">
|
|
464
|
+
<h1>${error.status ?? 500}</h1>
|
|
465
|
+
<p>${error.message}</p>
|
|
466
|
+
<p>Path: ${route.path}</p>
|
|
467
|
+
</div>
|
|
468
|
+
`,
|
|
469
|
+
)) satisfies ErrorHandler;
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Virtual modules
|
|
473
|
+
|
|
474
|
+
The plugin exposes two virtual modules:
|
|
475
|
+
|
|
476
|
+
| Module | Export | Description |
|
|
477
|
+
| --------------- | ------------ | -------------------------------------------- |
|
|
478
|
+
| `ilha:pages` | `pageRouter` | A `RouterBuilder` with all routes registered |
|
|
479
|
+
| `ilha:registry` | `registry` | `Record<string, Island>` for hydration |
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
// routes/[...].ts — Nitro catch-all handler
|
|
483
|
+
import { pageRouter } from "ilha:pages";
|
|
484
|
+
import { registry } from "ilha:registry";
|
|
485
|
+
|
|
486
|
+
export default defineEventHandler(async (event) => {
|
|
487
|
+
const html = await pageRouter.renderHydratable(event.node.req.url ?? "/", registry);
|
|
488
|
+
return new Response(`<!doctype html><html><body>${html}</body></html>`, {
|
|
489
|
+
headers: { "content-type": "text/html" },
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
// src/client.ts — browser entry
|
|
496
|
+
import { pageRouter } from "ilha:pages";
|
|
497
|
+
import { registry } from "ilha:registry";
|
|
498
|
+
|
|
499
|
+
pageRouter.hydrate(registry);
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Plugin options
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
pages({
|
|
506
|
+
dir: "src/pages", // pages directory (default: "src/pages")
|
|
507
|
+
generated: ".ilha/routes.ts", // generated file output (default: ".ilha/routes.ts")
|
|
508
|
+
});
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
The plugin regenerates the routes file only when content actually changes — avoiding unnecessary HMR invalidations. Structural changes (file add/remove, `+layout.ts`/`+error.ts` edits) trigger full HMR. Regular page content edits are handled by Vite's normal module HMR.
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## SSR + Hydration
|
|
516
|
+
|
|
517
|
+
The same route config runs on both sides. Signals (`routePath`, `routeParams`, etc.) are populated identically by `.render()`/`.renderHydratable()` on the server and `.mount()`/`.hydrate()` on the client.
|
|
518
|
+
|
|
519
|
+
```ts
|
|
520
|
+
// server: resolves URL → hydratable HTML string
|
|
521
|
+
await pageRouter.renderHydratable("/user/42", registry);
|
|
522
|
+
routeParams(); // → { id: "42" }
|
|
523
|
+
|
|
524
|
+
// client: hydrates SSR DOM, sets up navigation
|
|
525
|
+
pageRouter.hydrate(registry);
|
|
526
|
+
navigate("/user/99");
|
|
527
|
+
routeParams(); // → { id: "99" }
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### Full SSR → hydration flow
|
|
531
|
+
|
|
532
|
+
```
|
|
533
|
+
server client
|
|
534
|
+
────────────────────────────── ──────────────────────────────────────────────
|
|
535
|
+
renderHydratable(url, registry) pageRouter.prime() ← sync signals first
|
|
536
|
+
→ data-ilha="…" markers mount(registry, { root }) ← hydrate islands
|
|
537
|
+
→ data-ilha-state snapshot pageRouter.mount(target, ← setup navigation
|
|
538
|
+
{ hydrate: true, registry })
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Or use the one-liner: `pageRouter.hydrate(registry)`.
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## License
|
|
546
|
+
|
|
547
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { HydratableOptions, Island } from "ilha";
|
|
2
|
+
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
interface RouteRecord {
|
|
5
|
+
pattern: string;
|
|
6
|
+
island: Island<any, any>;
|
|
7
|
+
}
|
|
8
|
+
interface RouteSnapshot {
|
|
9
|
+
path: string;
|
|
10
|
+
params: Record<string, string>;
|
|
11
|
+
search: string;
|
|
12
|
+
hash: string;
|
|
13
|
+
}
|
|
14
|
+
interface AppError {
|
|
15
|
+
message: string;
|
|
16
|
+
status?: number;
|
|
17
|
+
stack?: string;
|
|
18
|
+
}
|
|
19
|
+
type LayoutHandler = (children: Island<any, any>) => Island<any, any>;
|
|
20
|
+
type ErrorHandler = (error: AppError, route: RouteSnapshot) => Island<any, any>;
|
|
21
|
+
declare function wrapLayout(layout: LayoutHandler, page: Island<any, any>): Island<any, any>;
|
|
22
|
+
declare function wrapError(handler: ErrorHandler, page: Island<any, any>): Island<any, any>;
|
|
23
|
+
interface NavigateOptions {
|
|
24
|
+
replace?: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface HydratableRenderOptions extends Partial<Omit<HydratableOptions, "name">> {}
|
|
27
|
+
interface HydrateOptions {
|
|
28
|
+
root?: Element;
|
|
29
|
+
target?: string | Element;
|
|
30
|
+
}
|
|
31
|
+
interface MountOptions {
|
|
32
|
+
hydrate?: boolean;
|
|
33
|
+
registry?: Record<string, Island<any, any>>;
|
|
34
|
+
}
|
|
35
|
+
interface RouterBuilder {
|
|
36
|
+
route(pattern: string, island: Island<any, any>): RouterBuilder;
|
|
37
|
+
prime(): void;
|
|
38
|
+
mount(target: string | Element, options?: MountOptions): () => void;
|
|
39
|
+
render(url: string | URL): string;
|
|
40
|
+
renderHydratable(url: string | URL, registry: Record<string, Island<any, any>>, options?: HydratableRenderOptions): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Hydrate the application - combines prime(), mount(), and router.mount() into one call.
|
|
43
|
+
* @param registry - The island registry from ilha:registry
|
|
44
|
+
* @param options - Optional root element (defaults to document.body) and router target (defaults to root)
|
|
45
|
+
* @returns Cleanup function
|
|
46
|
+
*/
|
|
47
|
+
hydrate(registry: Record<string, Island<any, any>>, options?: HydrateOptions): () => void;
|
|
48
|
+
}
|
|
49
|
+
declare const routePath: {
|
|
50
|
+
(): string;
|
|
51
|
+
(value: string): void;
|
|
52
|
+
};
|
|
53
|
+
declare const routeParams: {
|
|
54
|
+
(): Record<string, string>;
|
|
55
|
+
(value: Record<string, string>): void;
|
|
56
|
+
};
|
|
57
|
+
declare const routeSearch: {
|
|
58
|
+
(): string;
|
|
59
|
+
(value: string): void;
|
|
60
|
+
};
|
|
61
|
+
declare const routeHash: {
|
|
62
|
+
(): string;
|
|
63
|
+
(value: string): void;
|
|
64
|
+
};
|
|
65
|
+
declare function useRoute(): {
|
|
66
|
+
path: {
|
|
67
|
+
(): string;
|
|
68
|
+
(value: string): void;
|
|
69
|
+
};
|
|
70
|
+
params: {
|
|
71
|
+
(): Record<string, string>;
|
|
72
|
+
(value: Record<string, string>): void;
|
|
73
|
+
};
|
|
74
|
+
search: {
|
|
75
|
+
(): string;
|
|
76
|
+
(value: string): void;
|
|
77
|
+
};
|
|
78
|
+
hash: {
|
|
79
|
+
(): string;
|
|
80
|
+
(value: string): void;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Prime route context signals from the current `location` so that islands
|
|
85
|
+
* hydrated by `ilha.mount()` see the correct route values on their first
|
|
86
|
+
* render — preventing a mismatch morph that would destroy hydrated bindings.
|
|
87
|
+
*
|
|
88
|
+
* Call this **before** `ilha.mount()` and **after** all routes have been
|
|
89
|
+
* registered (i.e. after the `router().route(…).route(…)` chain).
|
|
90
|
+
*
|
|
91
|
+
* ```ts
|
|
92
|
+
* import { mount } from "ilha";
|
|
93
|
+
* import { pageRouter } from "ilha:pages";
|
|
94
|
+
* import { registry } from "ilha:registry";
|
|
95
|
+
*
|
|
96
|
+
* pageRouter.prime(); // ← sync signals first
|
|
97
|
+
* mount(registry, { root: … }); // ← then hydrate islands
|
|
98
|
+
* pageRouter.mount("#app", { hydrate: true });
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
declare function prime(): void;
|
|
102
|
+
declare function navigate(to: string, opts?: NavigateOptions): void;
|
|
103
|
+
declare function enableLinkInterception(root?: Element | Document): () => void;
|
|
104
|
+
declare const RouterView: Island<Record<string, unknown>, Record<string, never>>;
|
|
105
|
+
declare const RouterLink: Island<Record<string, unknown>, {
|
|
106
|
+
[x: string]: never;
|
|
107
|
+
href: string;
|
|
108
|
+
} & Record<"label", string>>;
|
|
109
|
+
declare function isActive(pattern: string): boolean;
|
|
110
|
+
declare function router(): RouterBuilder;
|
|
111
|
+
declare const _default: {
|
|
112
|
+
router: typeof router;
|
|
113
|
+
navigate: typeof navigate;
|
|
114
|
+
useRoute: typeof useRoute;
|
|
115
|
+
isActive: typeof isActive;
|
|
116
|
+
enableLinkInterception: typeof enableLinkInterception;
|
|
117
|
+
prime: typeof prime;
|
|
118
|
+
RouterView: Island<Record<string, unknown>, Record<string, never>>;
|
|
119
|
+
RouterLink: Island<Record<string, unknown>, {
|
|
120
|
+
[x: string]: never;
|
|
121
|
+
href: string;
|
|
122
|
+
} & Record<"label", string>>;
|
|
123
|
+
};
|
|
124
|
+
//#endregion
|
|
125
|
+
export { AppError, ErrorHandler, HydratableRenderOptions, HydrateOptions, LayoutHandler, MountOptions, NavigateOptions, RouteRecord, RouteSnapshot, RouterBuilder, RouterLink, RouterView, _default as default, enableLinkInterception, isActive, navigate, prime, routeHash, routeParams, routePath, routeSearch, router, useRoute, wrapError, wrapLayout };
|