lit-ui-router-ssr 0.5.0 → 0.7.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.
Files changed (97) hide show
  1. package/README.md +230 -38
  2. package/dist/client.d.ts +167 -10
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +172 -25
  5. package/dist/client.js.map +1 -1
  6. package/dist/dev-warn.d.ts +10 -0
  7. package/dist/dev-warn.d.ts.map +1 -1
  8. package/dist/dev-warn.js +1 -0
  9. package/dist/dev-warn.js.map +1 -1
  10. package/dist/development/client.js +185 -30
  11. package/dist/development/client.js.map +1 -1
  12. package/dist/development/dev-warn.js +4 -0
  13. package/dist/development/dev-warn.js.map +1 -1
  14. package/dist/development/index.js +2 -0
  15. package/dist/development/index.js.map +1 -1
  16. package/dist/development/light-dom-brand.js +7 -0
  17. package/dist/development/light-dom-brand.js.map +1 -0
  18. package/dist/development/light-dom.js +55 -0
  19. package/dist/development/light-dom.js.map +1 -0
  20. package/dist/development/light-element-renderer.js +16 -0
  21. package/dist/development/light-element-renderer.js.map +1 -0
  22. package/dist/development/prerender.js +43 -141
  23. package/dist/development/prerender.js.map +1 -1
  24. package/dist/development/respond.js +26 -0
  25. package/dist/development/respond.js.map +1 -0
  26. package/dist/development/served-interior.js +9 -0
  27. package/dist/development/served-interior.js.map +1 -0
  28. package/dist/development/served-markers.js +1 -1
  29. package/dist/development/served-view.js +31 -16
  30. package/dist/development/served-view.js.map +1 -1
  31. package/dist/development/signature.js +4 -1
  32. package/dist/development/signature.js.map +1 -1
  33. package/dist/development/testing.js +35 -0
  34. package/dist/development/testing.js.map +1 -0
  35. package/dist/development/ui-view-renderer.js +45 -30
  36. package/dist/development/ui-view-renderer.js.map +1 -1
  37. package/dist/index.d.ts +4 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +2 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/light-dom-brand.d.ts +20 -0
  42. package/dist/light-dom-brand.d.ts.map +1 -0
  43. package/dist/light-dom-brand.js +7 -0
  44. package/dist/light-dom-brand.js.map +1 -0
  45. package/dist/light-dom.d.ts +61 -0
  46. package/dist/light-dom.d.ts.map +1 -0
  47. package/dist/light-dom.js +55 -0
  48. package/dist/light-dom.js.map +1 -0
  49. package/dist/light-element-renderer.d.ts +45 -0
  50. package/dist/light-element-renderer.d.ts.map +1 -0
  51. package/dist/light-element-renderer.js +16 -0
  52. package/dist/light-element-renderer.js.map +1 -0
  53. package/dist/prerender.d.ts +80 -112
  54. package/dist/prerender.d.ts.map +1 -1
  55. package/dist/prerender.js +43 -124
  56. package/dist/prerender.js.map +1 -1
  57. package/dist/respond.d.ts +43 -0
  58. package/dist/respond.d.ts.map +1 -0
  59. package/dist/respond.js +26 -0
  60. package/dist/respond.js.map +1 -0
  61. package/dist/served-interior.d.ts +10 -0
  62. package/dist/served-interior.d.ts.map +1 -0
  63. package/dist/served-interior.js +9 -0
  64. package/dist/served-interior.js.map +1 -0
  65. package/dist/served-markers.d.ts +4 -4
  66. package/dist/served-markers.js +1 -1
  67. package/dist/served-view.d.ts +56 -2
  68. package/dist/served-view.d.ts.map +1 -1
  69. package/dist/served-view.js +29 -12
  70. package/dist/served-view.js.map +1 -1
  71. package/dist/signature.d.ts +22 -0
  72. package/dist/signature.d.ts.map +1 -1
  73. package/dist/signature.js +4 -1
  74. package/dist/signature.js.map +1 -1
  75. package/dist/testing.d.ts +44 -0
  76. package/dist/testing.d.ts.map +1 -0
  77. package/dist/testing.js +35 -0
  78. package/dist/testing.js.map +1 -0
  79. package/dist/ui-view-renderer.d.ts +11 -2
  80. package/dist/ui-view-renderer.d.ts.map +1 -1
  81. package/dist/ui-view-renderer.js +46 -27
  82. package/dist/ui-view-renderer.js.map +1 -1
  83. package/package.json +25 -3
  84. package/src/client.ts +553 -64
  85. package/src/dev-warn.ts +19 -1
  86. package/src/index.ts +15 -1
  87. package/src/light-dom-brand.ts +34 -0
  88. package/src/light-dom.ts +139 -0
  89. package/src/light-element-renderer.ts +66 -0
  90. package/src/prerender.ts +187 -386
  91. package/src/respond.ts +105 -0
  92. package/src/served-interior.ts +26 -0
  93. package/src/served-markers.ts +4 -4
  94. package/src/served-view.ts +119 -34
  95. package/src/signature.ts +43 -0
  96. package/src/testing.ts +88 -0
  97. package/src/ui-view-renderer.ts +128 -73
package/README.md CHANGED
@@ -24,12 +24,17 @@ imports the pre-1.0 renderer or re-derives the incantation.
24
24
  on the page carries `defer-hydration` and renders nothing until the client's walk reaches it.
25
25
  - **The hydration signature.** A page rendered from a template opens on a JSON data block,
26
26
  `<script type="application/json" data-lit-ui-router-ssr>`, naming this package's version, the
27
- state the router stood on, and that state's url parameter values. A page `renderShell` returns
28
- as a string is written as-is, without one.
29
- - **The emit loop.** Verdict to file name, redirect to rules line, tally, warnings for paths that
30
- matched nothing.
31
- - **The host rules file.** `_redirects` by default, every generated line paired with and without a
32
- trailing slash. No SPA catch-all is ever written: one turns every 404 into a 200.
27
+ state the router stood on, that state's url parameter values, and the resolved values the
28
+ `payload` hook supplies. A page `renderShell` returns as a string is written as-is, without one.
29
+ - **The lit render inside the emit loop.** The loop itself — verdict to file name, redirect to
30
+ rules line, the `_redirects` file, delegating states skipped, tally, warnings for paths that
31
+ matched nothing — is
32
+ `prerender` from `ui-router-server/prerender`, which takes any render. This package's `prerender`
33
+ supplies the lit one and takes the same options, plus `router`, `renderShell`, `document`,
34
+ `root`, `elementRenderers`, `payload`, `responses` and `origin`. No SPA catch-all is ever written: one
35
+ turns every 404 into a 200.
36
+ - **Response states after settle.** `responder` runs the handler bound to a response state's name
37
+ with the resolves its transition landed with.
33
38
  - **`<ui-view>` on both sides.** `UiViewRenderer` fills the element's light DOM on the server;
34
39
  `lit-ui-router-ssr/client` adopts what it drew.
35
40
 
@@ -48,6 +53,9 @@ npm install lit-ui-router-ssr
48
53
  the client half, so a bundle takes one or the other, never both. `@lit-labs/ssr-client` is optional:
49
54
  install it when you import `lit-ui-router-ssr/client`.
50
55
 
56
+ The `lit-ui-router` peer is `^1.19.0`: the server renders a view through the helper `<ui-view>`
57
+ renders through, and the client hands a served class element to the view it wakes.
58
+
51
59
  ## Quick Start
52
60
 
53
61
  ```typescript
@@ -66,9 +74,13 @@ const result = await prerender({
66
74
  document: (body, { path }) => fillShell(titles.get(path), body),
67
75
  });
68
76
 
69
- console.log(result.tally); // { shell: 2, redirect: 1, notFound: 0, document: 1 }
77
+ console.log(result.tally); // { shell: 2, redirect: 1, notFound: 0, delegate: 0, document: 1 }
70
78
  ```
71
79
 
80
+ `result.delegated` lists the paths that matched a delegating state. They get no page and no rules
81
+ line, so route them to your server; on Cloudflare, list them in the Worker's
82
+ `assets.run_worker_first` so the asset layer doesn't answer them first.
83
+
72
84
  `result.pages` lists every artefact in enumeration order, `result.rules` every line the host file
73
85
  carries, and `result.warnings` the paths that matched no route and had no projection to fall back
74
86
  on. `dryRun: true` plans all of it and writes nothing.
@@ -119,10 +131,59 @@ A page fails to land in four ways, and each rejects rather than hangs:
119
131
  `settle()` never calls `router.start()`, which runs once per router: it drives `urlService`
120
132
  directly, so a page loop calls it once per path on the same router, started or not.
121
133
 
134
+ ## Response states
135
+
136
+ A [response state](https://lit-ui-router.dev/packages/server#response-states) answers with a
137
+ `Response` built from its resolves instead of a page. `responder(router, responses)` settles
138
+ `router` on the verdict's subpath, query included, runs the handler bound to the landed state's
139
+ name with that transition's resolves (inherited ones included) and the request, and returns the
140
+ `Response` as built; a HEAD gets it without its body. A resolve that throws rejects before any
141
+ handler runs. It fits the fetch adapter's `serveResponse`, and `prerender` takes the same handlers
142
+ as `responses`, writing a 2xx body at the path its url spells or a 3xx as a rules line. See
143
+ [response states](https://lit-ui-router.dev/packages/ssr#response-states).
144
+
145
+ ## Adopting the resolves on boot
146
+
147
+ The `payload` option hands each rendered page's landed transition to a hook, which returns the
148
+ resolve payload to write. `dehydrate` from `ui-router-server/ssr` draws one, and the hook decides what to do with the resolves it leaves out:
149
+
150
+ ```typescript
151
+ import { prerender } from 'lit-ui-router-ssr';
152
+ import { dehydrate } from 'ui-router-server/ssr';
153
+
154
+ await prerender({
155
+ // mounts, router, paths, renderShell, …
156
+ payload: (transition, { path }) => {
157
+ const { payload, unserializable } = dehydrate(transition);
158
+
159
+ for (const { state, token } of unserializable)
160
+ console.warn(`${path}: ${state}.${token} re-runs on the client`);
161
+
162
+ return payload;
163
+ },
164
+ });
165
+ ```
166
+
167
+ The payload's `resolves` go into the page's hydration signature, beside `version`, `state` and
168
+ `params`, so the one block is the payload. `bootstrap()` seeds the boot transition from it before
169
+ the router starts: each resolve the page was drawn with adopts its value in place of running its
170
+ function. Every other resolve runs as it would without one: a value `dehydrate` left out, a
171
+ `NOWAIT` resolve, every resolve of a boot that lands on another state or other parameters, and
172
+ every resolve of a page whose block carries no `resolves`. Without the hook no page carries
173
+ resolves. What `dehydrate` draws, and which values it leaves out, is in
174
+ [its documentation](https://lit-ui-router.dev/packages/server#handing-resolves-to-the-client).
175
+
176
+ A value plain JSON can't carry, such as a `Date`, travels through a transporter. Pass the same
177
+ `transport` to `dehydrate` in the hook and to `bootstrap()`: the signature keeps the payload's
178
+ `transported` marks, and `bootstrap()` hands `transport` to `seed`. See
179
+ [values beyond plain JSON](https://lit-ui-router.dev/packages/server#values-beyond-plain-json).
180
+
122
181
  ## The routed view, drawn on the server
123
182
 
124
- `elementRenderers` defaults to `[UiViewRenderer]` — not `@lit-labs/ssr`'s `[LitElementRenderer]`,
125
- which wraps every custom element in a declarative shadow root it never asked for. `UiViewRenderer`
183
+ `elementRenderers` defaults to `[UiViewRenderer, LightElementRenderer]` — not `@lit-labs/ssr`'s
184
+ `[LitElementRenderer]`, which wraps every custom element in a declarative shadow root it never asked
185
+ for. `LightElementRenderer` answers only for an element that opted in (see
186
+ [Light-DOM elements](#light-dom-elements)). `UiViewRenderer`
126
187
  answers for `ui-view`: it registers the view at the address its `name` attribute and enclosing
127
188
  `<ui-view>`s spell, takes the `ViewConfig` the registration syncs back, and writes the routed
128
189
  component into the element's light DOM between the part markers the element's own `render()`
@@ -130,6 +191,20 @@ hydrates against. An address no state routes gets empty markers. That pair stays
130
191
  hydrating the view's surroundings reads it as the `uiViewSlot()` part and stops there; every marker
131
192
  between it carries a prefix, so the same walk reads past the view's interior.
132
193
 
194
+ The template comes from `lit-ui-router`'s own render helper, the one `<ui-view>`'s `render()` calls,
195
+ so the served markup and the client's first render share one source. A routed `LitElement` class,
196
+ plain or `sticky`, prerenders too: `lit-ui-router` builds the element and assigns its props, and
197
+ `LitElementRenderer` writes it, shadow root included, into the slot `<ui-view>` places a class in.
198
+ At the wake the served view hands that element to `lit-ui-router`, which adopts it instead of
199
+ building its own, so the element the server wrote is the element the client keeps. A class that
200
+ renders into its own light DOM prerenders into it when it uses
201
+ [`LightDomHydrating`](#light-dom-elements), and hydrates those nodes in place. Without it, the class
202
+ is written as its bare element, with no shadow root to hide its render, and renders on the client. A
203
+ class must be on the `customElements` registry to prerender.
204
+
205
+ The served `<ui-view>` markup is internal to this package and `lit-ui-router` and changes with them.
206
+ Post-processing of prerendered HTML keys on the [hydration signature](#the-client-half) block.
207
+
133
208
  The use site opts in with `uiViewSlot()`, which is what reaches the renderer's light-DOM render and
134
209
  commits nothing on the client:
135
210
 
@@ -172,8 +247,8 @@ card empty:
172
247
  ```
173
248
 
174
249
  `@lit-labs/ssr` writes a `.prop=${…}` binding as its part marker and nothing else — no attribute, no
175
- child, no value. Setting the property is an element renderer's job, and `elementRenderers` defaults
176
- to `[UiViewRenderer]`, which answers for `ui-view` alone. The card has no renderer, so nothing on
250
+ child, no value. Setting the property is an element renderer's job, and the default `elementRenderers`
251
+ answer for `ui-view` and for opted-in light-DOM elements alone. The card has no renderer, so nothing on
177
252
  the server sets `post` or runs its `render()`: the card first draws on the client, once hydration
178
253
  sets the property, and the served page shows an empty tag where it stands.
179
254
 
@@ -233,11 +308,16 @@ shadow root:
233
308
 
234
309
  ```typescript
235
310
  import { LitElementRenderer } from '@lit-labs/ssr';
236
- import { prerender, UiViewRenderer } from 'lit-ui-router-ssr';
311
+ import {
312
+ LightElementRenderer,
313
+ prerender,
314
+ UiViewRenderer,
315
+ } from 'lit-ui-router-ssr';
237
316
 
238
317
  await prerender({
239
318
  // …
240
- elementRenderers: [UiViewRenderer, LitElementRenderer],
319
+ // The list replaces the default; LightElementRenderer stays ahead of the shadow-root renderer.
320
+ elementRenderers: [UiViewRenderer, LightElementRenderer, LitElementRenderer],
241
321
  });
242
322
  ```
243
323
 
@@ -270,16 +350,14 @@ registers `<ui-view>`. That entry defines `<ui-router>` from core and `<ui-view>
270
350
  `withServedRender(UiView)`. Every value the app takes from the router — `UIRouterLit`, `srefHref`,
271
351
  `srefActiveClass`, `srefAriaCurrent` — comes from `lit-ui-router/pure`, which registers nothing;
272
352
  the root entry registers the plain `<ui-view>` as a side effect, and imported after this one it
273
- warns that the tag is already defined. Then the router boots and one call adopts the page:
353
+ warns that the tag is already defined. Then one call boots the router and adopts the page:
274
354
 
275
355
  ```typescript
276
356
  import 'lit-ui-router-ssr/register';
277
357
  import { UIRouterLit, srefHref } from 'lit-ui-router/pure';
278
- import { hydrateRoot } from 'lit-ui-router-ssr/client';
358
+ import { bootstrap } from 'lit-ui-router-ssr/client';
279
359
 
280
- router.start();
281
- await booted; // the first successful transition
282
- const release = hydrateRoot(root, page(router));
360
+ await bootstrap(router, root, page(router));
283
361
  ```
284
362
 
285
363
  A `<ui-view>` another class already defined throws, naming both ways out.
@@ -325,29 +403,45 @@ The result still extends core's `UiView`, so an enclosing view adopts it as a pa
325
403
 
326
404
  ## The client half
327
405
 
328
- `lit-ui-router-ssr/client` is the adopt side — no import side effects. The router boots
329
- first, and one call adopts the page:
406
+ `lit-ui-router-ssr/client` is the adopt side — no import side effects. One call boots the router
407
+ and adopts the page, or renders it cold, and resolves once the page is live:
330
408
 
331
409
  ```typescript
332
- import { hydrateRoot, uiViewSlot } from 'lit-ui-router-ssr/client';
333
-
334
- const booted = new Promise<void>((resolve) => {
335
- const off = router.transitionService.onSuccess({}, () => {
336
- off();
337
- resolve();
338
- });
339
- });
340
- router.start();
341
- await booted;
410
+ import { bootstrap } from 'lit-ui-router-ssr/client';
342
411
 
343
- const release = hydrateRoot(root, page(router));
344
- if (!release) render(page(router), root);
412
+ const { outcome } = await bootstrap(router, root, page(router));
345
413
  ```
346
414
 
347
- - **The sequence is the contract.** `router.start()`, await its first successful transition, then
348
- `hydrateRoot()`. The walk commits `.uiRouter` onto `<ui-router>` and each `<ui-view>` re-seeks the
349
- router before its own first render, so every view finds the settled router rather than the
350
- placeholder it registered against. Nothing constrains when `lit-ui-router-ssr/register` is imported.
415
+ - **`bootstrap(router, container, value, options?)` owns the boot.** It reads the container's
416
+ signature, starts the router, waits for the boot transition to land — following redirects to the
417
+ one that succeeds — then runs `hydrateRoot()`. The walk commits `.uiRouter` onto `<ui-router>` and
418
+ each `<ui-view>` re-seeks the router before its own first render, so every view finds the settled
419
+ router rather than the placeholder it registered against. The call waits for every awake
420
+ `<ui-view>`, nested ones included, to finish updating, then releases the adopter. It resolves with
421
+ `outcome` — `adopted` or `cold` — and `signature` (`BootstrapSignature`): `matched` when the boot
422
+ landed on the state and url parameters the signature names, `elsewhere` when the document carried
423
+ a signature and the boot landed somewhere else or landed no transition, and `absent` when it
424
+ carried none. Nothing constrains when `lit-ui-router-ssr/register` is imported.
425
+ - **The cold path is the same call.** A container with nothing to adopt is emptied and `value`
426
+ rendered into it before the router starts. A boot transition that fails, a url that starts none,
427
+ and a root `hydrate()` that throws over a mutated document all empty the container and render
428
+ `value` cold too, and the call resolves with the cause in `error` rather than rejecting.
429
+ - **The boot decides mismatches once.** A boot that lands where the document was drawn adopts every
430
+ view, and a view that still mismatches warns in development: the two halves render that template
431
+ differently. A boot that lands elsewhere — one static document served for a family of urls, or a
432
+ redirect — adopts the views that match and renders the rest cold without a warning each. A view
433
+ whose state, or whose url parameters (its own and inherited), differ from the drawn ones renders
434
+ cold. Once the page is live, a boot into another state warns once in development, naming both
435
+ states and how many views rendered cold; a boot into other url parameters of the drawn state is
436
+ silent.
437
+ - **The boot transition is seeded.** The payload the signature carries is handed to `seed()` from
438
+ `ui-router-server/ssr/client` before the router starts, so the boot transition adopts the
439
+ resolved values the prerender drew in place of running their resolve functions.
440
+ `options.payload` takes its place when given.
441
+ - **`hydrateRoot()` is the step underneath.** An app that owns its own boot calls
442
+ `seed(router, readResolvePayload(root))` and `router.start()`, awaits the first successful
443
+ transition, then `hydrateRoot(root, page(router))`, and renders `page(router)` itself when that
444
+ returns `false`.
351
445
  - **The signature decides first.** `hydrateRoot()` reads the container's hydration signature before
352
446
  it touches the document, and returns `false` over an emptied container when there is none — a
353
447
  cold client render, a dev server, a page from an earlier `prerender()`, a string `renderShell` —
@@ -368,7 +462,9 @@ if (!release) render(page(router), root);
368
462
  before that update stays asleep, holding its nodes, until it is attached again. The walk pins the
369
463
  adopter to every served view it passes, so that view is adopted on its return even once the root
370
464
  provider is released. A view the walk never reached drops those nodes, renders cold, and warns in
371
- development.
465
+ development. A deferred element no walk reaches — one the render wrote no marker for — wakes
466
+ with the adoption of the view that holds it, after that view's hydrate, or with the root walk when
467
+ no view holds it.
372
468
  - **The prefix is the protocol, and both halves are here.** `UiViewRenderer` writes a plain outer
373
469
  part pair around each view's routed markup and prefixes every marker between them; `hydrate()`
374
470
  reads past a prefixed comment, so the walk hydrating a view's surroundings stops at that pair. The
@@ -392,11 +488,67 @@ if (!release) render(page(router), root);
392
488
  (`uiViewAdoptEventName`), bubbling and composed, with `detail.outcome` one of `adopted` — its
393
489
  served nodes are the live ones — `fell-back` — a mismatch or a lost pair, with `detail.error`
394
490
  carrying the cause — or `none`, nothing to adopt. It fires once per view, parent first, in
395
- production as in development. `hydrateRoot()`'s `onAdopt(view, outcome, error)` option receives
491
+ production as in development. The `onAdopt(view, outcome, error)` option of `bootstrap()` and `hydrateRoot()` receives
396
492
  the same reports for every view its walk reaches, a view the pin adopts after the release included.
397
493
  - **A mutated document throws.** `hydrateRoot()` rethrows what `hydrate()` threw, over a container it
398
494
  first empties. The caller renders into it.
399
495
 
496
+ ### Light-DOM elements
497
+
498
+ A consumer's own `LitElement` that renders into its light DOM opts in to the same sleep-then-adopt
499
+ cycle with `LightDomHydrating(Base)` from `lit-ui-router-ssr/client`. `LightElementRenderer` from
500
+ `lit-ui-router-ssr`, one of `prerender()`'s default renderers, draws it on the server, and
501
+ `lightSlot()` marks the hole at the use site:
502
+
503
+ ```typescript
504
+ import { html, LitElement } from 'lit';
505
+ import { property } from 'lit/decorators.js';
506
+ import { LightDomHydrating, lightSlot } from 'lit-ui-router-ssr/client';
507
+
508
+ export class AppCard extends LightDomHydrating(LitElement) {
509
+ @property() label = '';
510
+
511
+ render() {
512
+ return html`<p>${this.label}</p>`;
513
+ }
514
+ }
515
+
516
+ export const card = (label: string) =>
517
+ html`<app-card .label=${label}>${lightSlot()}</app-card>`;
518
+ ```
519
+
520
+ - **Each class opts in.** The mixin stamps a `Symbol.for` brand on the class it returns, and the
521
+ renderer matches that brand, so a subclass stays opted in, the base class does not, and a second
522
+ copy of this package recognises it. An element that did not opt in is served empty and renders
523
+ cold.
524
+ - **The server writes `render()` into the light DOM.** `LightElementRenderer` extends
525
+ `@lit-labs/ssr`'s `LitElementRenderer`: it builds the element, sets its attributes and properties,
526
+ runs `willUpdate`, and writes the render where a shadow root would go, prefixing its markers as
527
+ `UiViewRenderer` prefixes a view's. Custom elements inside it, `<ui-view>` included, render too.
528
+ An explicit `elementRenderers` replaces the default, so keep it in that list, ahead of any
529
+ `LitElementRenderer`: the first renderer that matches a class wins. A render that awaits is not
530
+ served.
531
+ - **`lightSlot()` is the hole.** It is `uiViewSlot()` under a name that does not read as a view: the
532
+ server reaches the renderer through it, and on the client it wakes the element when the
533
+ enclosing walk reaches it, parent first, and pins the adopter to it.
534
+ - **The element wakes through the adopter.** It sleeps under `defer-hydration`, which `prerender()`
535
+ writes on every custom element, and its first update requests `adoptUiViewContext`. The adopter
536
+ hydrates `render()` against the served nodes in place, then wakes the elements inside it. A view's
537
+ reveal stops at a light-DOM element and the element's stops at a view or another light-DOM
538
+ element, so each interior waits for its own wake. With no adopter, the element drops the served
539
+ nodes and renders cold, warning in development.
540
+ - **It reports through its own event.** Each opted-in element dispatches `light-dom:adopt`
541
+ (`lightDomAdoptEventName`) with the same `detail` as `ui-view:adopt`. `onAdopt`, and the count of
542
+ views `bootstrap()` rendered cold, report views only.
543
+ - **A mismatch falls back.** A hydrate that throws drops the element's served nodes, and it renders
544
+ cold, warning in development; its surroundings keep theirs.
545
+ - **An author child with bindings of its own falls back every time.** A child the use site writes
546
+ beside `lightSlot()` keeps its markers ahead of the element's pair, and the element's hydrate
547
+ meets them first. Static author children are kept, ahead of the render, as they are in a
548
+ `<ui-view>`.
549
+ - **`static styles` do not apply.** A light-DOM element has no shadow root to adopt them into; it
550
+ takes the document's styles.
551
+
400
552
  ### How this compares
401
553
 
402
554
  Two axes separate the hydration models in circulation: where the code that wakes the markup comes
@@ -419,6 +571,46 @@ the grid entirely.
419
571
 
420
572
  The [guide](https://lit-ui-router.dev/packages/ssr#how-this-compares) carries the longer discussion.
421
573
 
574
+ ## Measuring the round trip
575
+
576
+ `lit-ui-router-ssr/testing` counts how many of the server's elements survive the boot. It imports
577
+ nothing — no lit, no router — and runs anywhere there is a DOM. `stampServed(root)` takes every
578
+ element under `root`, open shadow roots included, before the client boots; `countRetained(stamp)`
579
+ returns `{ served, retained, removed, added }` after it. A hydrated page keeps `removed` at 0. A view
580
+ the client rendered over counts its served elements as removed and its new ones as added; an element
581
+ a component draws in `firstUpdated`, or the shadow `<slot>` each `<ui-router>` renders, counts only
582
+ as added.
583
+
584
+ ```ts
585
+ import { countRetained, stampServed } from 'lit-ui-router-ssr/testing';
586
+
587
+ const stamp = stampServed(container); // after the prerender is parsed in, before bootstrap()
588
+ await bootstrap(router, container, page(router));
589
+ expect(countRetained(stamp)).toMatchObject({ removed: 0 });
590
+ ```
591
+
592
+ In a browser the stamp and the count must run in the page. Each helper is self-contained, so its
593
+ source text is valid page script. In Playwright, an init script stamps the body when parsing ends,
594
+ before module scripts run:
595
+
596
+ ```ts
597
+ import { countRetained, stampServed } from 'lit-ui-router-ssr/testing';
598
+
599
+ await page.addInitScript(`
600
+ const stampServed = ${stampServed};
601
+ window.countRetained = ${countRetained};
602
+ document.addEventListener('readystatechange', () => {
603
+ if (document.readyState === 'interactive') window.servedStamp = stampServed(document.body);
604
+ });
605
+ `);
606
+ await page.goto('/sheet/7B/');
607
+ await page.waitForFunction(() => !document.querySelector('[defer-hydration]'));
608
+
609
+ expect(await page.evaluate('countRetained(servedStamp)')).toMatchObject({
610
+ removed: 0,
611
+ });
612
+ ```
613
+
422
614
  ## Documentation
423
615
 
424
616
  - [Guide](https://lit-ui-router.dev/packages/ssr)
package/dist/client.d.ts CHANGED
@@ -1,22 +1,26 @@
1
1
  import { renderLight } from "@lit-labs/ssr-client/directives/render-light.js";
2
2
  import type { RenderOptions } from "lit";
3
+ import type { UIRouterLit } from "lit-ui-router/pure";
4
+ import type { ResolvePayload, SeedOptions } from "ui-router-server/ssr/client";
3
5
  import type { HydrationSignature } from "./signature.js";
4
6
  export { adoptUiViewContext, type AdoptableView, type AdoptUiViewContext, type AdoptUiViewContextKey } from "./adopt-context.js";
5
7
  export { isServedViewClass, type ServedUiView, type ServedUiViewConstructor, servedViewBrand, withServedRender } from "./served-view.js";
8
+ export { LightDomHydrating, type LightDomHydratingConstructor, type LightDomHydratingElement, type LitElementConstructor } from "./light-dom.js";
6
9
  export type { HydrationSignature } from "./signature.js";
7
10
  /**
8
11
  * What a served `<ui-view>`'s wake came to.
9
12
  *
10
13
  * - `adopted`: the element hydrated its served nodes, and they are the live ones.
11
14
  * - `fell-back`: the served pair did not match the render the client booted
12
- * into, or was lost; the view dropped the served interior and rendered cold.
15
+ * into, was drawn for another state or other url parameters, or was lost;
16
+ * the view dropped the served interior and rendered cold.
13
17
  * - `none`: nothing to adopt — an empty served pair, the address no state
14
18
  * routed, or a view the document served no markers for.
15
19
  */
16
20
  export type AdoptOutcome = "adopted" | "fell-back" | "none";
17
21
  /**
18
22
  * The name of the event a served `<ui-view>` dispatches once its wake's
19
- * outcome is known, in the `ui-view:` house style of the served markers.
23
+ * outcome is known.
20
24
  */
21
25
  export declare const uiViewAdoptEventName: "ui-view:adopt";
22
26
  /**
@@ -25,7 +29,7 @@ export declare const uiViewAdoptEventName: "ui-view:adopt";
25
29
  export interface UiViewAdoptDetail {
26
30
  /** What the wake came to. */
27
31
  readonly outcome: AdoptOutcome;
28
- /** What `hydrate()` threw, or why the pair was lost; present only on `fell-back`. */
32
+ /** What `hydrate()` threw, or why the view did not adopt; present only on `fell-back`. */
29
33
  readonly error?: unknown;
30
34
  }
31
35
  /**
@@ -34,6 +38,14 @@ export interface UiViewAdoptDetail {
34
38
  */
35
39
  export type UiViewAdoptEvent = CustomEvent<UiViewAdoptDetail>;
36
40
  /**
41
+ * The event a light-DOM hydrating element dispatches from itself at its wake,
42
+ * once its outcome is known. It carries the same detail as {@link
43
+ * uiViewAdoptEventName}, and it does not reach `onAdopt`, which reports views.
44
+ */
45
+ export declare const lightDomAdoptEventName: "light-dom:adopt";
46
+ /** A light-DOM hydrating element's hydration outcome. */
47
+ export type LightDomAdoptEvent = CustomEvent<UiViewAdoptDetail>;
48
+ /**
37
49
  * Receives each served view's outcome, as {@link HydrateRootOptions.onAdopt}.
38
50
  */
39
51
  export type AdoptReporter = (view: Element, outcome: AdoptOutcome, error?: unknown) => void;
@@ -53,6 +65,8 @@ declare global {
53
65
  interface HTMLElementEventMap {
54
66
  /** A served `<ui-view>`'s hydration outcome; see {@link UiViewAdoptEvent}. */
55
67
  "ui-view:adopt": UiViewAdoptEvent;
68
+ /** A light-DOM hydrating element's hydration outcome; see {@link LightDomAdoptEvent}. */
69
+ "light-dom:adopt": LightDomAdoptEvent;
56
70
  }
57
71
  }
58
72
  /**
@@ -77,6 +91,21 @@ declare global {
77
91
  */
78
92
  export declare const uiViewSlot: typeof renderLight;
79
93
  /**
94
+ * The hole a `LightDomHydrating` element fills, written at the use site: the
95
+ * {@link uiViewSlot} directive under a name that does not read as a view.
96
+ *
97
+ * On the server it reaches `LightElementRenderer`'s `renderLight()`, so the
98
+ * element's own render goes where the template puts it. On the client it wakes
99
+ * the element when the enclosing walk reaches it, parent first, and pins the
100
+ * adopter to it.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * html`<app-card .title=${title}>${lightSlot()}</app-card>`;
105
+ * ```
106
+ */
107
+ export declare const lightSlot: typeof renderLight;
108
+ /**
80
109
  * Reads the hydration signature `prerender()` wrote ahead of the render a
81
110
  * container holds: the version of this package that drew the document, and
82
111
  * the state and parameter values it was drawn for.
@@ -97,7 +126,7 @@ export declare const uiViewSlot: typeof renderLight;
97
126
  *
98
127
  * await booted;
99
128
  * const { state } = readHydrationSignature(root) ?? {};
100
- * const drawnForBoot = state === router.globals.current.name;
129
+ * const bootedWhereDrawn = state === router.globals.current.name;
101
130
  * const release = hydrateRoot(root, page(router));
102
131
  * ```
103
132
  *
@@ -107,6 +136,30 @@ export declare const uiViewSlot: typeof renderLight;
107
136
  */
108
137
  export declare function readHydrationSignature(container: ParentNode): HydrationSignature | null;
109
138
  /**
139
+ * Reads the resolve payload `prerender()` wrote into the container's
140
+ * {@link readHydrationSignature | hydration signature}: the state and url
141
+ * parameters the document was drawn for, and the values its resolves settled
142
+ * on, keyed by state name, then by resolve token.
143
+ *
144
+ * {@link bootstrap} reads it on its own. A boot that calls {@link hydrateRoot}
145
+ * directly hands it to `seed` from `ui-router-server/ssr/client` before the
146
+ * router starts.
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * import { readResolvePayload } from 'lit-ui-router-ssr/client';
151
+ * import { seed } from 'ui-router-server/ssr/client';
152
+ *
153
+ * seed(router, readResolvePayload(root));
154
+ * router.start();
155
+ * ```
156
+ *
157
+ * @param container - the element the server's markup was written into
158
+ * @returns the payload, or `undefined` when the container holds no signature,
159
+ * one from another release line of this package, or one without `resolves`
160
+ */
161
+ export declare function readResolvePayload(container: ParentNode): ResolvePayload | undefined;
162
+ /**
110
163
  * Adopts a server-rendered container, and the `<ui-view>`s that wake under it.
111
164
  *
112
165
  * The container's {@link readHydrationSignature | hydration signature} is read
@@ -132,7 +185,9 @@ export declare function readHydrationSignature(container: ParentNode): Hydration
132
185
  * outside `container`, or one whose attribute the app cleared itself after the
133
186
  * release — is answered by nobody: core drops its held nodes and warns in
134
187
  * development. An element the render deferred and wrote no marker for is woken
135
- * once the walk has finished, since nothing in the walk reaches it.
188
+ * once the walk has finished, since nothing in the walk reaches it — except
189
+ * one inside a `<ui-view>`, which wakes once that view has hydrated its own
190
+ * render, so a routed element never wakes ahead of the view that routes it.
136
191
  *
137
192
  * The provider answers synchronously and stops immediate propagation, so an
138
193
  * outer provider never answers the same request twice. It outlives this call,
@@ -150,11 +205,12 @@ export declare function readHydrationSignature(container: ParentNode): Hydration
150
205
  * A `hydrate()` that throws is rethrown, over a container emptied the same
151
206
  * way. The caller renders into it.
152
207
  *
153
- * The boot is the router first: `router.start()`, await its first successful
154
- * transition, then this call. The walk commits `.uiRouter` onto `<ui-router>`
155
- * and each woken `<ui-view>` re-seeks the router before its own first render,
156
- * so the views find the settled router rather than the placeholder they
157
- * registered against. Nothing constrains when `lit-ui-router-ssr/register` is
208
+ * {@link bootstrap} runs the whole boot around this call. Called directly, the
209
+ * boot is the router first: `seed` the router with {@link readResolvePayload},
210
+ * `router.start()`, await its first successful transition, then this call.
211
+ * The walk commits `.uiRouter` onto `<ui-router>` and each woken `<ui-view>`
212
+ * re-seeks the router before its own first render, so the views find the
213
+ * settled router rather than the placeholder they registered against. Nothing constrains when `lit-ui-router-ssr/register` is
158
214
  * imported.
159
215
  *
160
216
  * @example
@@ -172,4 +228,105 @@ export declare function readHydrationSignature(container: ParentNode): Hydration
172
228
  * @returns the function that releases the provider, or `false`, over an emptied container, when there is nothing to adopt — no signature, as on a cold client render or a dev server, one from another release line, or one no render marker follows
173
229
  */
174
230
  export declare function hydrateRoot(container: HTMLElement, value: unknown, options?: HydrateRootOptions): false | (() => void);
231
+ /** What {@link bootstrap} came to, once the page is live. */
232
+ export type BootstrapOutcome = "adopted" | "cold";
233
+ /**
234
+ * How a boot compares with the {@link readHydrationSignature | hydration
235
+ * signature} the document carried.
236
+ *
237
+ * - `matched`: the boot transition landed on the state and url parameters the
238
+ * signature names.
239
+ * - `elsewhere`: the document carried a signature, and the boot landed on
240
+ * another state or other parameters — a redirect, or a static host serving
241
+ * one document for a family of urls. A signed document whose boot landed no
242
+ * transition reports this too, beside `outcome: 'cold'` and its `error`.
243
+ * - `absent`: the document carried no signature.
244
+ *
245
+ * A signature from another release line, or one no render marker follows, is
246
+ * still compared, though the container renders cold.
247
+ */
248
+ export type BootstrapSignature = "matched" | "elsewhere" | "absent";
249
+ /** What {@link bootstrap} resolves with. */
250
+ export interface BootstrapResult {
251
+ /**
252
+ * `adopted`: the served document was hydrated, and each served view reported
253
+ * its own {@link AdoptOutcome}. `cold`: the container was emptied and the
254
+ * template rendered into it.
255
+ */
256
+ readonly outcome: BootstrapOutcome;
257
+ /** How the boot compares with the document's hydration signature; see {@link BootstrapSignature}. */
258
+ readonly signature: BootstrapSignature;
259
+ /**
260
+ * The boot transition's `Rejection`, or what the root `hydrate()` threw;
261
+ * present only on a cold boot that one of them caused.
262
+ */
263
+ readonly error?: unknown;
264
+ }
265
+ /**
266
+ * {@link bootstrap}'s options: {@link HydrateRootOptions}, plus the boot
267
+ * transition's seed and the `transport` handed to `seed` with it.
268
+ */
269
+ export interface BootstrapOptions extends HydrateRootOptions, SeedOptions {
270
+ /**
271
+ * Resolved values the document was drawn with, handed to `seed` from
272
+ * `ui-router-server/ssr/client` before the router starts, so the boot
273
+ * transition adopts them in place of running their resolve functions. It
274
+ * takes the place of the payload the container's signature carries.
275
+ */
276
+ readonly payload?: ResolvePayload;
277
+ }
278
+ /**
279
+ * Boots `router` into the page `container` holds, and resolves once that page
280
+ * is live.
281
+ *
282
+ * It owns the whole client boot. The boot transition is seeded first, with
283
+ * `options.payload` when given, and otherwise with the resolves the
284
+ * container's signature carries ({@link readResolvePayload}): each resolve the
285
+ * document was drawn with adopts its value in place of running its function,
286
+ * and every other resolve runs as it would without a payload. The
287
+ * container's {@link readHydrationSignature | signature} is read before
288
+ * anything else touches the document; a container {@link
289
+ * hydrateRoot} would not adopt — no signature, another release line, stripped
290
+ * markers — is emptied and `value` rendered into it at once, and the router
291
+ * then starts. Otherwise the router starts, the call waits for the boot
292
+ * transition to land, following redirects to the one that succeeds, and then
293
+ * hydrates the container with `value`.
294
+ *
295
+ * The boot decides once what the page does:
296
+ *
297
+ * - The boot lands where the signature says the document was drawn: every
298
+ * served view adopts, and a view that still mismatches warns in development,
299
+ * since that is a template the two halves render differently.
300
+ * - The boot lands elsewhere — a static host serving one document for a
301
+ * family of urls, or a redirect: the views that match adopt, and the views
302
+ * that differ render cold without a warning of their own. A view whose
303
+ * state, or whose url parameters, its own and inherited, differ from the
304
+ * drawn ones renders cold. Once the page is live, a boot into another state
305
+ * warns once in development, naming both states and how many views rendered
306
+ * cold; a boot into other url parameters of the drawn state is silent.
307
+ * - The boot fails, or the url starts no transition: the container is emptied
308
+ * and `value` rendered cold. The root `hydrate()` throwing, as over a
309
+ * mutated document, does the same. The call resolves rather than rejects.
310
+ *
311
+ * Every served view still reports its own {@link AdoptOutcome}, as a {@link
312
+ * UiViewAdoptEvent} and to `options.onAdopt`. The call waits for each awake
313
+ * `<ui-view>` to finish updating, nested views included, then releases the
314
+ * adopter `hydrateRoot()` provided; a view the app detached keeps its pin.
315
+ *
316
+ * @example
317
+ * ```ts
318
+ * import { bootstrap } from 'lit-ui-router-ssr/client';
319
+ *
320
+ * const { outcome } = await bootstrap(router, root, page(router));
321
+ * ```
322
+ *
323
+ * @param router - the client router, its states registered and not yet started
324
+ * @param container - the element the server's markup was written into, or an empty one
325
+ * @param value - the same template the server rendered, with every `<ui-view>` hole empty
326
+ * @param options - lit render options, `onAdopt`, and the boot transition's `payload` and `transport`
327
+ * @returns what the boot came to, once every awake view has updated
328
+ * @throws what `router.start()` throws, as on a router already started
329
+ * @throws the `TypeError` `seed` throws on a payload with no `resolves`, before the router starts
330
+ */
331
+ export declare function bootstrap(router: UIRouterLit, container: HTMLElement, value: unknown, options?: BootstrapOptions): Promise<BootstrapResult>;
175
332
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","mappings":"AAOA,SAAS,mBAAmB;AAI5B,cAAyB,qBAAqB;AAW9C,cAAc,0BAA0B;AAExC,SACE,yBACK,oBACA,yBACA,6BACA;AAEP,SACE,wBACK,mBACA,yBACL,iBACA,wBACK;AAEP,cAAc,0BAA0B;;;;;;;;;;AAcxC,YAAY,eAAe,YAAY,cAAc;;;;;AAMrD,OAAO,cAAM,sBAAuB;;;;AAKpC,iBAAiB,kBAAkB;;UAExB,SAAS;;UAET;AACX;;;;;AAMA,YAAY,mBAAmB,YAAY;;;;AAK3C,YAAY,iBACV,MAAM,SACN,SAAS,cAET;;;;;AAOF,iBAAiB,2BAA2B,cAAc;;;;;;CAMxD,UAAU;AACZ;eAEe;WACH,oBAAoB;;EAE5B,iBAAiB;CACnB;AACF;;;;;;;;;;;;;;;;;;;;;AAgEA,OAAO,cAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmVhC,OAAO,iBAAS,uBACd,WAAW,aACV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4IH,OAAO,iBAAS,YACd,WAAW,aAEX,gBACA,UAAS,qBACR","names":[],"ignoreList":[],"sources":["../src/client.ts"]}
1
+ {"version":3,"file":"client.d.ts","mappings":"AAQA,SAAS,mBAAmB;AAK5B,cAAyB,qBAAqB;AAM9C,cAAc,mBAAmB;AAEjC,cAAc,gBAAgB,mBAAmB;AAYjD,cAAc,0BAA0B;AAExC,SACE,yBACK,oBACA,yBACA,6BACA;AAEP,SACE,wBACK,mBACA,yBACL,iBACA,wBACK;AAEP,SACE,wBACK,mCACA,+BACA,6BACA;AAEP,cAAc,0BAA0B;;;;;;;;;;;AAexC,YAAY,eAAe,YAAY,cAAc;;;;;AAMrD,OAAO,cAAM,sBAAuB;;;;AAKpC,iBAAiB,kBAAkB;;UAExB,SAAS;;UAET;AACX;;;;;AAMA,YAAY,mBAAmB,YAAY;;;;;;AAO3C,OAAO,cAAM,wBAAyB;;AAGtC,YAAY,qBAAqB,YAAY;;;;AAK7C,YAAY,iBACV,MAAM,SACN,SAAS,cAET;;;;;AAOF,iBAAiB,2BAA2B,cAAc;;;;;;CAMxD,UAAU;AACZ;eAEe;WACH,oBAAoB;;EAE5B,iBAAiB;;EAEjB,mBAAmB;CACrB;AACF;;;;;;;;;;;;;;;;;;;;;AAgEA,OAAO,cAAM,mBAAmB;;;;;;;;;;;;;;;AAgBhC,OAAO,cAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgZ/B,OAAO,iBAAS,uBACd,WAAW,aACV;;;;;;;;;;;;;;;;;;;;;;;;AA6BH,OAAO,iBAAS,mBACd,WAAW,aACV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2JH,OAAO,iBAAS,YACd,WAAW,aAEX,gBACA,UAAS,qBACR;;AAsCH,YAAY,mBAAmB,YAAY;;;;;;;;;;;;;;;;AAiB3C,YAAY,qBAAqB,YAAY,cAAc;;AAG3D,iBAAiB,gBAAgB;;;;;;UAMtB,SAAS;;UAET,WAAW;;;;;UAKX;AACX;;;;;AAMA,iBAAiB,yBAAyB,oBAAoB,YAAY;;;;;;;UAO/D,UAAU;AACrB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6LA,OAAO,iBAAe,UACpB,QAAQ,aACR,WAAW,aAEX,gBACA,UAAS,mBACR,QAAQ","names":[],"ignoreList":[],"sources":["../src/client.ts"]}