lit-ui-router-ssr 0.8.0 → 0.9.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 (86) hide show
  1. package/README.md +122 -15
  2. package/dist/adopt-context.d.ts +7 -1
  3. package/dist/adopt-context.d.ts.map +1 -1
  4. package/dist/adopt-context.js.map +1 -1
  5. package/dist/boot-pending.d.ts +22 -0
  6. package/dist/boot-pending.d.ts.map +1 -0
  7. package/dist/boot-pending.js +2 -0
  8. package/dist/boot-pending.js.map +1 -0
  9. package/dist/client.d.ts +82 -7
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +94 -21
  12. package/dist/client.js.map +1 -1
  13. package/dist/dev-warn.js +0 -1
  14. package/dist/dev-warn.js.map +1 -1
  15. package/dist/development/adopt-context.js.map +1 -1
  16. package/dist/development/boot-pending.js +2 -0
  17. package/dist/development/boot-pending.js.map +1 -0
  18. package/dist/development/client.js +107 -20
  19. package/dist/development/client.js.map +1 -1
  20. package/dist/development/index.js +2 -1
  21. package/dist/development/index.js.map +1 -1
  22. package/dist/development/light-element-renderer.js.map +1 -1
  23. package/dist/development/prerender.js +22 -57
  24. package/dist/development/prerender.js.map +1 -1
  25. package/dist/development/render.js +91 -0
  26. package/dist/development/render.js.map +1 -0
  27. package/dist/development/respond.js.map +1 -1
  28. package/dist/development/served-view.js +12 -3
  29. package/dist/development/served-view.js.map +1 -1
  30. package/dist/development/shadow-fallback-renderer.js +23 -0
  31. package/dist/development/shadow-fallback-renderer.js.map +1 -0
  32. package/dist/development/signature.js +1 -1
  33. package/dist/development/signature.js.map +1 -1
  34. package/dist/development/ui-view-renderer.js +12 -0
  35. package/dist/development/ui-view-renderer.js.map +1 -1
  36. package/dist/development/vite.js.map +1 -1
  37. package/dist/index.d.ts +5 -3
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +2 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/light-element-renderer.d.ts +11 -2
  42. package/dist/light-element-renderer.d.ts.map +1 -1
  43. package/dist/light-element-renderer.js.map +1 -1
  44. package/dist/prerender.d.ts +30 -50
  45. package/dist/prerender.d.ts.map +1 -1
  46. package/dist/prerender.js +23 -52
  47. package/dist/prerender.js.map +1 -1
  48. package/dist/render.d.ts +169 -0
  49. package/dist/render.d.ts.map +1 -0
  50. package/dist/render.js +86 -0
  51. package/dist/render.js.map +1 -0
  52. package/dist/respond.d.ts +12 -0
  53. package/dist/respond.d.ts.map +1 -1
  54. package/dist/respond.js.map +1 -1
  55. package/dist/served-view.d.ts.map +1 -1
  56. package/dist/served-view.js +0 -1
  57. package/dist/served-view.js.map +1 -1
  58. package/dist/shadow-fallback-renderer.d.ts +24 -0
  59. package/dist/shadow-fallback-renderer.d.ts.map +1 -0
  60. package/dist/shadow-fallback-renderer.js +14 -0
  61. package/dist/shadow-fallback-renderer.js.map +1 -0
  62. package/dist/signature.d.ts +6 -0
  63. package/dist/signature.d.ts.map +1 -1
  64. package/dist/signature.js +1 -1
  65. package/dist/signature.js.map +1 -1
  66. package/dist/ui-view-renderer.d.ts +9 -0
  67. package/dist/ui-view-renderer.d.ts.map +1 -1
  68. package/dist/ui-view-renderer.js +12 -0
  69. package/dist/ui-view-renderer.js.map +1 -1
  70. package/dist/vite.d.ts +31 -9
  71. package/dist/vite.d.ts.map +1 -1
  72. package/dist/vite.js.map +1 -1
  73. package/package.json +2 -2
  74. package/src/adopt-context.ts +7 -1
  75. package/src/boot-pending.ts +30 -0
  76. package/src/client.ts +293 -42
  77. package/src/index.ts +12 -2
  78. package/src/light-element-renderer.ts +11 -2
  79. package/src/prerender.ts +81 -148
  80. package/src/render.ts +337 -0
  81. package/src/respond.ts +12 -0
  82. package/src/served-view.ts +13 -5
  83. package/src/shadow-fallback-renderer.ts +73 -0
  84. package/src/signature.ts +6 -0
  85. package/src/ui-view-renderer.ts +25 -0
  86. package/src/vite.ts +33 -13
package/README.md CHANGED
@@ -24,8 +24,9 @@ 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, that state's url parameter values, and the resolved values
28
- `dehydrate` or the `payload` hook draws. A page `renderShell` returns as a string is written as-is, without one.
27
+ state the router stood on, that state's url parameter values, the fully qualified name of each
28
+ `<ui-view>` it drew a routed component into, and the resolved values `dehydrate` or the
29
+ `payload` hook draws. A page `renderShell` returns as a string is written as-is, without one.
29
30
  - **The lit render inside the emit loop.** The loop itself — verdict to file name, redirect to
30
31
  rules line, the `_redirects` file, delegating states skipped, tally, warnings for paths that
31
32
  matched nothing — is
@@ -33,6 +34,8 @@ imports the pre-1.0 renderer or re-derives the incantation.
33
34
  supplies the lit one and takes the same options, plus `router`, `renderShell`, `document`,
34
35
  `root`, `elementRenderers`, `dehydrate`, `payload`, `responses` and `origin`. No SPA catch-all is ever written: one
35
36
  turns every 404 into a 200.
37
+ - **One page for a host that owns the request.** `renderToString` is that render alone, for a
38
+ framework route or a worker that settles the router itself.
36
39
  - **Response states after settle.** `responder` runs the handler bound to a response state's name
37
40
  with the resolves its transition landed with.
38
41
  - **`<ui-view>` on both sides.** `UiViewRenderer` fills the element's light DOM on the server;
@@ -57,6 +60,9 @@ install it when you import `lit-ui-router-ssr/client`. `lit-ui-router-ssr/vite`
57
60
  The `lit-ui-router` peer is `^1.19.0`: the server renders a view through the helper `<ui-view>`
58
61
  renders through, and the client hands a served class element to the view it wakes.
59
62
 
63
+ The `ui-router-server` peer is `^0.9.0`: `prerender` asks its `redirectedTo` whether a page's
64
+ `redirectTo` fired, and reads the status from the redirecting state's `redirectStatusCode`.
65
+
60
66
  ## Quick Start
61
67
 
62
68
  ```typescript
@@ -95,7 +101,8 @@ virtual fs instead.
95
101
  `ui-router-server/vite`'s, with the DOM shim and `lit-ui-router-ssr/register` loaded before the
96
102
  prerender script, and its `definePrerender` calls this package's `prerender` with the options the
97
103
  script returns; [Prerender with Vite](https://lit-ui-router.dev/guides/prerender-with-vite) shows
98
- both files in full.
104
+ both files in full. [Prerender with Eleventy](https://lit-ui-router.dev/guides/prerender-with-eleventy)
105
+ calls `prerender` from an Eleventy data file instead, and lets Eleventy write the pages.
99
106
 
100
107
  ## Static hosts add a trailing slash
101
108
 
@@ -124,7 +131,7 @@ A server render reads the router only once its transition has landed, resolves i
124
131
  `settle(router, path)`, the `ui-router-server/ssr` step this package re-exports, sets the url, syncs
125
132
  the router to it, and resolves with the transition that landed, so the synchronous render after it
126
133
  reads every resolve. A `redirectTo` chain settles on its final state, and a path the router already
127
- stands on resolves at once.
134
+ stands on resolves at once. `redirectedTo` reports where a redirected transition landed.
128
135
 
129
136
  A page fails to land in four ways, and each rejects rather than hangs:
130
137
 
@@ -140,6 +147,85 @@ A page fails to land in four ways, and each rejects rather than hangs:
140
147
  `settle()` never calls `router.start()`, which runs once per router: it drives `urlService`
141
148
  directly, so a page loop calls it once per path on the same router, started or not.
142
149
 
150
+ ## Page, response state or redirect: which decides when
151
+
152
+ A url's answer is decided at one of two points:
153
+
154
+ - **The verdict**, from the url alone, before any of the app's code runs. A pattern rule or a data
155
+ `redirectTo` answers a redirect, a state marked `response: true` a response state, a `delegate` a
156
+ host handler, and any other state a page.
157
+ - **Settle**, once the page's transition runs its resolves, hooks and `redirectTo` functions. They
158
+ can change only a page's answer: a `redirectTo` function can move the transition elsewhere, and a
159
+ resolve that rejects fails `settle`, which the host maps to an error page.
160
+
161
+ A response state suits a url whose pattern alone says it is not a page. When urls of one pattern
162
+ are sometimes a page and sometimes a redirect, such as a compare page whose reversed pair redirects
163
+ to the forward one, only a `redirectTo` function tells them apart.
164
+
165
+ The state whose `redirectTo` fires, data or function, declares the status in `redirectStatusCode`:
166
+ `301`, `302`, `303`, `307` or `308`, `302` by default. A redirect function often depends on data,
167
+ and browsers cache a permanent redirect with no expiry, so a redirect that never changes opts into
168
+ `308`. A pattern rule's redirect answers `302`.
169
+
170
+ After `settle`, `redirectedTo(router, transition)` returns `{ url, status }` for a transition a
171
+ redirect moved, a change of params alone included: the url it landed on, relative to the mount,
172
+ query included, and the redirecting state's `redirectStatusCode`. A live host answers with it:
173
+
174
+ ```ts
175
+ import { redirectedTo, settle } from 'lit-ui-router-ssr';
176
+
177
+ // A shell verdict under /releases, on a router built for this request.
178
+ const redirect = redirectedTo(router, await settle(router, subpath));
179
+
180
+ if (redirect)
181
+ return new Response(null, {
182
+ status: redirect.status,
183
+ headers: { Location: `/releases${redirect.url}` },
184
+ });
185
+ ```
186
+
187
+ `prerender` does the same after each `renderShell`: when the transition the hook landed was
188
+ redirected, the path gets a rules line in place of a page, counted in `tally.redirect`. A url rule
189
+ that rewrites the url, `when('/old', '/new')`, starts a new transition rather than redirecting one,
190
+ so `redirectedTo` reports nothing for it; declare it in the mount's `redirects`. See
191
+ [page, response state or redirect](https://lit-ui-router.dev/packages/ssr#page-response-state-or-redirect-which-decides-when).
192
+
193
+ ## Rendering one page per request
194
+
195
+ A host that owns the request, such as a framework route or a worker, renders one page with
196
+ `renderToString(template, { router })`. It is the render `prerender()` runs for each page, hydration
197
+ signature and `deferHydration` included, so its markup is byte for byte what `prerender()` hands
198
+ `document` for the same template and router. The host settles the router, decides the response and
199
+ writes the document around the markup:
200
+
201
+ ```typescript
202
+ import { redirectedTo, renderToString, settle } from 'lit-ui-router-ssr';
203
+
204
+ // `router` serves the mount at /releases; `subpath` is the request's path within it.
205
+ const redirect = redirectedTo(router, await settle(router, subpath));
206
+
207
+ if (redirect)
208
+ return new Response(null, {
209
+ status: redirect.status,
210
+ headers: { Location: `/releases${redirect.url}` },
211
+ });
212
+
213
+ const { html } = renderToString(page(router), { router, dehydrate: {} });
214
+
215
+ return new Response(shell(html), {
216
+ status: router.globals.current.name === 'notFound' ? 404 : 200,
217
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
218
+ });
219
+ ```
220
+
221
+ The router must stand on the page before the call. It is provided on `root`, a fresh `EventTarget`
222
+ unless one is passed, for the render alone. `elementRenderers` and `dehydrate` act as `prerender()`'s
223
+ options of the same name, and `payload` takes a payload drawn by hand rather than a hook. The result's
224
+ `payload` reports the bytes the resolve payload adds to the signature and, under `dehydrate`, the
225
+ resolves left to re-run on the client.
226
+
227
+ The markup is collected into one string, synchronously. A streamed render would be a separate export.
228
+
143
229
  ## Response states
144
230
 
145
231
  A [response state](https://lit-ui-router.dev/packages/server#response-states) answers with a
@@ -244,7 +330,9 @@ Post-processing of prerendered HTML keys on the [hydration signature](#the-clien
244
330
  The use site opts in with `uiViewSlot()`, which is what reaches the renderer's light-DOM render and
245
331
  commits nothing on the client:
246
332
 
247
- ```typescript
333
+ ```typescript check
334
+ import { html } from 'lit';
335
+ import type { UIRouterLit } from 'lit-ui-router';
248
336
  import { uiViewSlot } from 'lit-ui-router-ssr/client';
249
337
 
250
338
  const page = (router: UIRouterLit) => html`
@@ -259,7 +347,7 @@ same strings with the hole empty and each `<ui-view>` fills itself.
259
347
 
260
348
  Take a card that receives a post by property and draws it in its own `render()`:
261
349
 
262
- ```typescript
350
+ ```typescript check
263
351
  import { html, LitElement } from 'lit';
264
352
 
265
353
  type Post = { title: string; summary: string };
@@ -369,6 +457,13 @@ parses, and hydration adopts them there. `LitElementRenderer` does the same for
369
457
  on the page, whether or not it has anything to show on the server, which is the cost the default
370
458
  avoids.
371
459
 
460
+ A shell element needs that list too: a shadow-DOM component that holds `<ui-view>` in its own
461
+ template, such as an `<app-root>`. Under the default it renders as its bare tag, empty, and the
462
+ views inside it are missing with it. With `LitElementRenderer` after `LightElementRenderer`, the
463
+ `<ui-view>` in its shadow root renders, hydrates and adopts. In development, `prerender()` and
464
+ `renderToString()` warn once per tag when a shadow-DOM `LitElement` reaches no renderer that draws
465
+ it; `<ui-router>`, whose shadow root is a bare slot, is left out.
466
+
372
467
  ## Registering the elements
373
468
 
374
469
  A prerendered page needs the served `<ui-view>`, and `lit-ui-router-ssr/register` is the one import
@@ -442,7 +537,7 @@ registry holds no `<ui-view>`.
442
537
  It imports `lit-ui-router/pure`, which registers nothing, and defines the served class under a tag
443
538
  of its own:
444
539
 
445
- ```typescript
540
+ ```typescript check
446
541
  import { UiView } from 'lit-ui-router/pure';
447
542
  import { withServedRender } from 'lit-ui-router-ssr/client';
448
543
 
@@ -470,12 +565,23 @@ const { outcome } = await bootstrap(router, root, page(router));
470
565
  signature, starts the router, waits for the boot transition to land — following redirects to the
471
566
  one that succeeds — then runs `hydrateRoot()`. The walk commits `.uiRouter` onto `<ui-router>` and
472
567
  each `<ui-view>` re-seeks the router before its own first render, so every view finds the settled
473
- router rather than the placeholder it registered against. The call waits for every awake
474
- `<ui-view>`, nested ones included, to finish updating, then releases the adopter. It resolves with
475
- `outcome` — `adopted` or `cold` — and `signature` (`BootstrapSignature`): `matched` when the boot
476
- landed on the state and url parameters the signature names, `elsewhere` when the document carried
477
- a signature and the boot landed somewhere else or landed no transition, and `absent` when it
478
- carried none. Nothing constrains when `lit-ui-router-ssr/register` is imported.
568
+ router rather than the placeholder it registered against. It resolves with `outcome` — `adopted`
569
+ or `cold`, what the root hydrate came to — `signature` (`BootstrapSignature`): `matched` when the
570
+ boot landed on the state and url parameters the signature names, `elsewhere` when the document
571
+ carried a signature and the boot landed somewhere else or landed no transition, and `absent` when
572
+ it carried none — and `views`, described next. Nothing constrains when
573
+ `lit-ui-router-ssr/register` is imported.
574
+ - **`views` says what each view came to.** A read-only map keyed by a view's fully qualified name
575
+ (`$default`, `$default.$default`, `$default.aside`) to a `BootstrapViewReport`: `outcome`, an
576
+ `AdoptOutcome` or `pending`, with the reporting `view` and, on `fell-back`, its `error`. It lists
577
+ every view the signature says the document was drawn with, then any other view that woke and
578
+ requested the adopter. Views are tracked by what they report to the adopter, whose request is a
579
+ composed event, so a view in a shadow root counts like any other. The call
580
+ resolves once the views that woke have updated, nested ones and those in shadow roots their hosts
581
+ hydrated included, and lists a drawn view that has not woken as `pending`. The map is a snapshot:
582
+ the adopter stays installed until every drawn view has reported, so a view whose host hydrates
583
+ later still adopts and still reaches `onAdopt`. A document that booted where it was drawn and
584
+ adopted none of its drawn views warns once in development.
479
585
  - **The cold path is the same call.** A container with nothing to adopt is emptied and `value`
480
586
  rendered into it before the router starts. A boot transition that fails, a url that starts none,
481
587
  and a root `hydrate()` that throws over a mutated document all empty the container and render
@@ -543,7 +649,8 @@ const { outcome } = await bootstrap(router, root, page(router));
543
649
  served nodes are the live ones — `fell-back` — a mismatch or a lost pair, with `detail.error`
544
650
  carrying the cause — or `none`, nothing to adopt. It fires once per view, parent first, in
545
651
  production as in development. The `onAdopt(view, outcome, error)` option of `bootstrap()` and `hydrateRoot()` receives
546
- the same reports for every view its walk reaches, a view the pin adopts after the release included.
652
+ the same reports for every view its walk reaches, a view the pin adopts after the release included,
653
+ and a view in a shadow root its host hydrates after the walk.
547
654
  - **A mutated document throws.** `hydrateRoot()` rethrows what `hydrate()` threw, over a container it
548
655
  first empties. The caller renders into it.
549
656
 
@@ -554,7 +661,7 @@ cycle with `LightDomHydrating(Base)` from `lit-ui-router-ssr/client`. `LightElem
554
661
  `lit-ui-router-ssr`, one of `prerender()`'s default renderers, draws it on the server, and
555
662
  `lightSlot()` marks the hole at the use site:
556
663
 
557
- ```typescript
664
+ ```typescript check
558
665
  import { html, LitElement } from 'lit';
559
666
  import { property } from 'lit/decorators.js';
560
667
  import { LightDomHydrating, lightSlot } from 'lit-ui-router-ssr/client';
@@ -38,7 +38,13 @@ export type AdoptUiViewContext = Context<AdoptUiViewContextKey, (view: Adoptable
38
38
  * @example
39
39
  * ```ts
40
40
  * import { provideContext } from 'lit-ui-router/context';
41
- * import { adoptUiViewContext } from 'lit-ui-router-ssr/client';
41
+ * import {
42
+ * adoptUiViewContext,
43
+ * type AdoptableView,
44
+ * } from 'lit-ui-router-ssr/client';
45
+ *
46
+ * declare const container: HTMLElement;
47
+ * declare function adopt(view: AdoptableView): void;
42
48
  *
43
49
  * const uninstall = provideContext(container, adoptUiViewContext, (view) =>
44
50
  * adopt(view),
@@ -1 +1 @@
1
- {"version":3,"file":"adopt-context.d.ts","mappings":"AACA,cAAc,qBAAqB;AACnC,cAAc,eAAe;;;;;AAM7B,iBAAiB,sBAAsB;;UAE5B,MAAM;AACjB;;;;;AAMA,iBAAiB,sBAAsB,YAAY;;CAGjD;;UAES,eAAe;AAC1B;;;;;;AAOA,YAAY,qBAAqB,QAC/B,wBACC,MAAM;;;;;;;;;;;;;;;;;;;;;;AAyBT,OAAO,cAAM,oBAAoB","names":[],"ignoreList":[],"sources":["../src/adopt-context.ts"]}
1
+ {"version":3,"file":"adopt-context.d.ts","mappings":"AACA,cAAc,qBAAqB;AACnC,cAAc,eAAe;;;;;AAM7B,iBAAiB,sBAAsB;;UAE5B,MAAM;AACjB;;;;;AAMA,iBAAiB,sBAAsB,YAAY;;CAGjD;;UAES,eAAe;AAC1B;;;;;;AAOA,YAAY,qBAAqB,QAC/B,wBACC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BT,OAAO,cAAM,oBAAoB","names":[],"ignoreList":[],"sources":["../src/adopt-context.ts"]}
@@ -1 +1 @@
1
- {"version":3,"file":"adopt-context.js","mappings":"AAyDA,OAAO,MAAM,qBAAyC,OAAO,OAAO,EAClE,MAAM,0CACR,CAAC","names":[],"ignoreList":[],"sources":["../src/adopt-context.ts"]}
1
+ {"version":3,"file":"adopt-context.js","mappings":"AA+DA,OAAO,MAAM,qBAAyC,OAAO,OAAO,EAClE,MAAM,0CACR,CAAC","names":[],"ignoreList":[],"sources":["../src/adopt-context.ts"]}
@@ -0,0 +1,22 @@
1
+ import type { Context } from "lit-ui-router/context";
2
+ /** The identity of {@link bootPendingContext}. */
3
+ interface BootPendingContextKey {
4
+ /** Names the key for a debugger; identity, not this string, is what matches. */
5
+ readonly name: "lit-ui-router-ssr/context#boot-pending";
6
+ }
7
+ /**
8
+ * The boot-pending key: a pending `bootstrap()` answers with a function that
9
+ * takes the view's check and runs it again once its walk has settled.
10
+ *
11
+ * @internal
12
+ */
13
+ export type BootPendingContext = Context<BootPendingContextKey, (recheck: () => void) => void>;
14
+ /**
15
+ * Answered under a container while a `bootstrap()` over it has yet to settle
16
+ * its walk, in development only.
17
+ *
18
+ * @internal
19
+ */
20
+ export declare const bootPendingContext: BootPendingContext;
21
+ export {};
22
+ //# sourceMappingURL=boot-pending.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boot-pending.d.ts","mappings":"AACA,cAAc,eAAe;;UAGnB,sBAAsB;;UAErB,MAAM;AACjB;;;;;;;AAQA,YAAY,qBAAqB,QAC/B,wBACC;;;;;;;AAUH,OAAO,cAAM,oBAAoB","names":[],"ignoreList":[],"sources":["../src/boot-pending.ts"]}
@@ -0,0 +1,2 @@
1
+ export const bootPendingContext = Object.freeze({ name: "lit-ui-router-ssr/context#boot-pending" });
2
+ //# sourceMappingURL=boot-pending.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boot-pending.js","mappings":"AA2BA,OAAO,MAAM,qBAAyC,OAAO,OAAO,EAClE,MAAM,yCACR,CAAC","names":[],"ignoreList":[],"sources":["../src/boot-pending.ts"]}
package/dist/client.d.ts CHANGED
@@ -86,6 +86,12 @@ declare global {
86
86
  *
87
87
  * @example
88
88
  * ```ts
89
+ * import { html } from 'lit';
90
+ * import type { UIRouterLit } from 'lit-ui-router';
91
+ * import { uiViewSlot } from 'lit-ui-router-ssr/client';
92
+ *
93
+ * declare const router: UIRouterLit;
94
+ *
89
95
  * html`<ui-router .uiRouter=${router}><ui-view>${uiViewSlot()}</ui-view></ui-router>`;
90
96
  * ```
91
97
  */
@@ -101,6 +107,11 @@ export declare const uiViewSlot: typeof renderLight;
101
107
  *
102
108
  * @example
103
109
  * ```ts
110
+ * import { html } from 'lit';
111
+ * import { lightSlot } from 'lit-ui-router-ssr/client';
112
+ *
113
+ * declare const title: string;
114
+ *
104
115
  * html`<app-card .title=${title}>${lightSlot()}</app-card>`;
105
116
  * ```
106
117
  */
@@ -123,6 +134,13 @@ export declare const lightSlot: typeof renderLight;
123
134
  * @example
124
135
  * ```ts
125
136
  * import { hydrateRoot, readHydrationSignature } from 'lit-ui-router-ssr/client';
137
+ * import type { TemplateResult } from 'lit';
138
+ * import type { UIRouterLit } from 'lit-ui-router';
139
+ *
140
+ * declare const booted: Promise<void>;
141
+ * declare const root: HTMLElement;
142
+ * declare const router: UIRouterLit;
143
+ * declare function page(router: UIRouterLit): TemplateResult;
126
144
  *
127
145
  * await booted;
128
146
  * const { state } = readHydrationSignature(root) ?? {};
@@ -149,6 +167,10 @@ export declare function readHydrationSignature(container: ParentNode): Hydration
149
167
  * ```ts
150
168
  * import { readResolvePayload } from 'lit-ui-router-ssr/client';
151
169
  * import { seed } from 'ui-router-server/ssr/client';
170
+ * import type { UIRouterLit } from 'lit-ui-router';
171
+ *
172
+ * declare const root: HTMLElement;
173
+ * declare const router: UIRouterLit;
152
174
  *
153
175
  * seed(router, readResolvePayload(root));
154
176
  * router.start();
@@ -216,6 +238,13 @@ export declare function readResolvePayload(container: ParentNode): ResolvePayloa
216
238
  * @example
217
239
  * ```ts
218
240
  * import { hydrateRoot } from 'lit-ui-router-ssr/client';
241
+ * import { render, type TemplateResult } from 'lit';
242
+ * import type { UIRouterLit } from 'lit-ui-router';
243
+ *
244
+ * declare const booted: Promise<void>;
245
+ * declare const root: HTMLElement;
246
+ * declare const router: UIRouterLit;
247
+ * declare function page(router: UIRouterLit): TemplateResult;
219
248
  *
220
249
  * await booted;
221
250
  * const release = hydrateRoot(root, page(router));
@@ -246,17 +275,45 @@ export type BootstrapOutcome = "adopted" | "cold";
246
275
  * still compared, though the container renders cold.
247
276
  */
248
277
  export type BootstrapSignature = "matched" | "elsewhere" | "absent";
278
+ /**
279
+ * What one view came to, as {@link BootstrapResult.views} reports it: its
280
+ * {@link AdoptOutcome}, or `pending` for a view the document was drawn with
281
+ * that had not woken when {@link bootstrap} resolved.
282
+ */
283
+ export type BootstrapViewOutcome = AdoptOutcome | "pending";
284
+ /** One view's entry in {@link BootstrapResult.views}. */
285
+ export interface BootstrapViewReport {
286
+ /** What the view came to; see {@link BootstrapViewOutcome}. */
287
+ readonly outcome: BootstrapViewOutcome;
288
+ /** The view that reported; absent on a `pending` one. */
289
+ readonly view?: Element;
290
+ /** What `hydrate()` threw, or why the view did not adopt; present only on `fell-back`. */
291
+ readonly error?: unknown;
292
+ }
249
293
  /** What {@link bootstrap} resolves with. */
250
294
  export interface BootstrapResult {
251
295
  /**
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.
296
+ * What the root hydrate came to, not each view. `adopted`: the served
297
+ * document was hydrated, and each served view reported its own {@link
298
+ * AdoptOutcome}, which {@link BootstrapResult.views} collects; a document
299
+ * whose views all rendered cold still reports `adopted` here. `cold`: the
300
+ * container was emptied and the template rendered into it.
255
301
  */
256
302
  readonly outcome: BootstrapOutcome;
257
303
  /** How the boot compares with the document's hydration signature; see {@link BootstrapSignature}. */
258
304
  readonly signature: BootstrapSignature;
259
305
  /**
306
+ * Each served view's outcome, keyed by the view's fully qualified name
307
+ * (`$default`, `$default.$default`, `$default.aside`): every view the
308
+ * signature says the document was drawn with, in the order it lists them,
309
+ * then any other view that woke and requested the adopter, such as one that
310
+ * woke with no served render. A drawn view not yet woken is `pending`; the
311
+ * first view to report under a name holds it. A snapshot taken as the call
312
+ * resolves: a view that adopts later still reports to `onAdopt` and its
313
+ * {@link UiViewAdoptEvent}. Empty on a cold boot.
314
+ */
315
+ readonly views: ReadonlyMap<string, BootstrapViewReport>;
316
+ /**
260
317
  * The boot transition's `Rejection`, or what the root `hydrate()` threw;
261
318
  * present only on a cold boot that one of them caused.
262
319
  */
@@ -309,13 +366,31 @@ export interface BootstrapOptions extends HydrateRootOptions, SeedOptions {
309
366
  * mutated document, does the same. The call resolves rather than rejects.
310
367
  *
311
368
  * 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.
369
+ * UiViewAdoptEvent} and to `options.onAdopt`, and the result collects them in
370
+ * {@link BootstrapResult.views}. Views are tracked by what they report to the
371
+ * adopter, whose request is a composed event, so a view in a shadow root is
372
+ * reported like any other; nothing queries the document for views. The call
373
+ * waits until the views awake under the boot have
374
+ * updated, nested views and views in shadow roots their hosts hydrated
375
+ * included, and resolves then, reporting a view the signature lists that has
376
+ * not woken as `pending`. The adopter stays installed on `container` until
377
+ * every view the signature lists has reported, so a view whose host hydrates
378
+ * later still adopts and still reaches `onAdopt`; a view the app detached
379
+ * keeps its pin. A signed document that booted where it was drawn and adopted
380
+ * none of the views it was drawn with warns once in development.
381
+ *
382
+ * In development a sleeping view's "still asleep" warning stands down while
383
+ * the boot transition runs, and is checked again once the walk has settled.
315
384
  *
316
385
  * @example
317
386
  * ```ts
318
387
  * import { bootstrap } from 'lit-ui-router-ssr/client';
388
+ * import type { TemplateResult } from 'lit';
389
+ * import type { UIRouterLit } from 'lit-ui-router';
390
+ *
391
+ * declare const root: HTMLElement;
392
+ * declare const router: UIRouterLit;
393
+ * declare function page(router: UIRouterLit): TemplateResult;
319
394
  *
320
395
  * const { outcome } = await bootstrap(router, root, page(router));
321
396
  * ```
@@ -324,7 +399,7 @@ export interface BootstrapOptions extends HydrateRootOptions, SeedOptions {
324
399
  * @param container - the element the server's markup was written into, or an empty one
325
400
  * @param value - the same template the server rendered, with every `<ui-view>` hole empty
326
401
  * @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
402
+ * @returns what the boot came to, and what each view came to, once the awake views have updated
328
403
  * @throws what `router.start()` throws, as on a router already started
329
404
  * @throws the `TypeError` `seed` throws on a payload with no `resolves`, before the router starts
330
405
  */
@@ -1 +1 @@
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"]}
1
+ {"version":3,"file":"client.d.ts","mappings":"AAQA,SAAS,mBAAmB;AAK5B,cAAyB,qBAAqB;AAM9C,cAAc,mBAAmB;AAEjC,cAAc,gBAAgB,mBAAmB;AAajD,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;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsEA,OAAO,cAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;AAqBhC,OAAO,cAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAka/B,OAAO,iBAAS,uBACd,WAAW,aACV;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCH,OAAO,iBAAS,mBACd,WAAW,aACV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkKH,OAAO,iBAAS,YACd,WAAW,aAEX,gBACA,UAAS,qBACR;;AAsCH,YAAY,mBAAmB,YAAY;;;;;;;;;;;;;;;;AAiB3C,YAAY,qBAAqB,YAAY,cAAc;;;;;;AAO3D,YAAY,uBAAuB,eAAe;;AAGlD,iBAAiB,oBAAoB;;UAE1B,SAAS;;UAET,OAAO;;UAEP;AACX;;AAGA,iBAAiB,gBAAgB;;;;;;;;UAQtB,SAAS;;UAET,WAAW;;;;;;;;;;;UAWX,OAAO,oBAAoB;;;;;UAK3B;AACX;;;;;AAMA,iBAAiB,yBAAyB,oBAAoB,YAAY;;;;;;;UAO/D,UAAU;AACrB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoUA,OAAO,iBAAe,UACpB,QAAQ,aACR,WAAW,aAEX,gBACA,UAAS,mBACR,QAAQ","names":[],"ignoreList":[],"sources":["../src/client.ts"]}
package/dist/client.js CHANGED
@@ -1,13 +1,12 @@
1
1
  import { hydrate } from "@lit-labs/ssr-client";
2
2
  import { renderLight } from "@lit-labs/ssr-client/directives/render-light.js";
3
- import { isFunction, RejectType } from "@uirouter/core";
3
+ import { isFunction, isString, RejectType } from "@uirouter/core";
4
4
  import { noChange, render } from "lit";
5
5
  import { directive } from "lit/directive.js";
6
6
  import { getDirectiveClass } from "lit/directive-helpers.js";
7
- import { provideContext } from "lit-ui-router/context";
7
+ import { provideContext, requestContext } from "lit-ui-router/context";
8
8
  import { UiView } from "lit-ui-router/pure";
9
9
  import { seed } from "ui-router-server/ssr/client";
10
- import "#dev-gate";
11
10
  import { adoptUiViewContext } from "./adopt-context.js";
12
11
  import { isLightDomElement } from "./light-dom-brand.js";
13
12
  import { servedMarkerPrefix } from "./served-markers.js";
@@ -37,7 +36,7 @@ class UiViewSlotDirective extends RenderLightDirective {
37
36
  }
38
37
  export const uiViewSlot = directive(UiViewSlotDirective);
39
38
  export const lightSlot = uiViewSlot;
40
- let walkPolicy = {};
39
+ let walkPolicy;
41
40
  const walkWith = (policy, walk) => {
42
41
  const outer = walkPolicy;
43
42
  walkPolicy = policy;
@@ -153,7 +152,10 @@ const pinAdopter = (view, policy) => {
153
152
  let release = () => {};
154
153
  release = provideContext(view, adoptUiViewContext, (woken) => {
155
154
  if (woken === view) release();
156
- adopt(woken, policy);
155
+ if (policy) return adopt(woken, policy);
156
+ const above = requestContext(view.parentNode, adoptUiViewContext);
157
+ if (above) return above(woken);
158
+ adopt(woken, {});
157
159
  });
158
160
  };
159
161
  const isHydrationSignature = (value) => typeof value === "object" && value !== null && "version" in value && typeof value.version === "string";
@@ -257,15 +259,67 @@ const drawnView = (signature, router) => (view) => {
257
259
  return Object.entries(urlParamsOf(state, router.globals.params)).every(([id, value]) => JSON.stringify(value) === JSON.stringify(drawnParams[id]));
258
260
  };
259
261
  const signatureMatches = (signature, router) => JSON.stringify([signature.state, signature.params]) === JSON.stringify(routeOf(router.globals));
260
- const settleViews = async (container) => {
261
- const settled = new Set();
262
- for (;;) {
263
- const awake = [...container.querySelectorAll("*")].filter((element) => element instanceof UiView && element.isConnected && !element.hasAttribute(DEFER) && !settled.has(element));
264
- if (awake.length === 0) return;
265
- for (const view of awake) settled.add(view);
266
- await Promise.all(awake.map((view) => view.updateComplete));
262
+ const noneAdopted = (reports) => !reports.some(({ outcome }) => outcome === "adopted");
263
+ const checkSomeAdopted = (tracker) => {
264
+ const reports = tracker.drawnReports();
265
+ if (reports.length === 0 || !noneAdopted(reports)) return;
266
+ if (reports.some(({ outcome }) => outcome !== "pending")) return tracker.snapshot(), void 0;
267
+ tracker.whenDrawnReported(() => {
268
+ if (noneAdopted(tracker.drawnReports())) tracker.snapshot();
269
+ });
270
+ };
271
+ const drawnViewsOf = (signature) => Array.isArray(signature.views) ? signature.views.filter(isString) : [];
272
+ const trackViews = (drawn, onward) => {
273
+ const reports = new Map();
274
+ const waiting = new Set(drawn);
275
+ const awake = [];
276
+ const done = [];
277
+ const settle = () => {
278
+ if (waiting.size > 0) return;
279
+ for (const run of done.splice(0)) run();
280
+ };
281
+ const reporter = (view, outcome, error) => {
282
+ if (view instanceof UiView) awake.push(view);
283
+ const name = view instanceof UiView ? view.fqn : void 0;
284
+ if (name !== void 0 && !reports.has(name)) {
285
+ reports.set(name, outcome === "fell-back" ? {
286
+ outcome,
287
+ view,
288
+ error
289
+ } : {
290
+ outcome,
291
+ view
292
+ });
293
+ waiting.delete(name);
294
+ settle();
295
+ }
296
+ onward(view, outcome, error);
297
+ };
298
+ const drawnEntries = () => drawn.map((name) => [name, reports.get(name) ?? { outcome: "pending" }]);
299
+ return {
300
+ reporter,
301
+ awake: () => awake,
302
+ snapshot: () => new Map([...drawnEntries(), ...reports]),
303
+ drawnReports: () => drawnEntries().map(([, report]) => report),
304
+ whenDrawnReported: (run) => {
305
+ done.push(run);
306
+ settle();
307
+ }
308
+ };
309
+ };
310
+ const nextTask = () => new Promise((resolve) => {
311
+ setTimeout(resolve, 0);
312
+ });
313
+ const settleReports = async (tracker) => {
314
+ for (let seen = -1; seen !== tracker.awake().length;) {
315
+ seen = tracker.awake().length;
316
+ await nextTask();
317
+ await Promise.all(tracker.awake().map((view) => view.updateComplete));
267
318
  }
268
319
  };
320
+ const holdAsleepWarnings = (container) => {
321
+ return () => {};
322
+ };
269
323
  const renderCold = (container, value, renderOptions) => {
270
324
  makeCold(container);
271
325
  render(value, container, renderOptions);
@@ -279,31 +333,46 @@ export async function bootstrap(router, container, value, options = {}) {
279
333
  renderCold(container, value, renderOptions);
280
334
  const landing = await startBoot(router);
281
335
  disarm();
282
- await settleViews(container);
336
+ await nextTask();
283
337
  return {
284
338
  outcome: "cold",
285
339
  signature: compareSignature(served, landing, router),
340
+ views: new Map(),
286
341
  ...pickError(landing)
287
342
  };
288
343
  }
344
+ const rearm = holdAsleepWarnings(container);
345
+ try {
346
+ return await bootServed(router, container, value, signature, {
347
+ onAdopt,
348
+ renderOptions,
349
+ disarm
350
+ });
351
+ } finally {
352
+ rearm();
353
+ }
354
+ }
355
+ const bootServed = async (router, container, value, signature, { onAdopt, renderOptions, disarm }) => {
289
356
  const landing = await startBoot(router);
290
357
  disarm();
291
358
  if (!("transition" in landing)) {
292
359
  renderCold(container, value, renderOptions);
293
- await settleViews(container);
360
+ await nextTask();
294
361
  return {
295
362
  outcome: "cold",
296
363
  signature: "elsewhere",
364
+ views: new Map(),
297
365
  ...pickError(landing)
298
366
  };
299
367
  }
300
368
  const compared = compareSignature(signature, landing, router);
301
369
  const drawn = compared === "matched";
302
370
  let fellBack = 0;
303
- const reporter = (view, outcome, error) => {
371
+ const tracker = trackViews(drawnViewsOf(signature), (view, outcome, error) => {
304
372
  if (outcome === "fell-back") fellBack += 1;
305
373
  onAdopt?.(view, outcome, error);
306
- };
374
+ });
375
+ const { reporter } = tracker;
307
376
  let release;
308
377
  try {
309
378
  release = adoptRoot(container, value, renderOptions, drawn ? {
@@ -316,21 +385,25 @@ export async function bootstrap(router, container, value, options = {}) {
316
385
  });
317
386
  } catch (error) {
318
387
  render(value, container, renderOptions);
319
- await settleViews(container);
388
+ await nextTask();
320
389
  return {
321
390
  outcome: "cold",
322
391
  signature: compared,
392
+ views: new Map(),
323
393
  error
324
394
  };
325
395
  }
326
- await settleViews(container);
327
- release();
396
+ await settleReports(tracker);
397
+ tracker.whenDrawnReported(release);
398
+ const views = tracker.snapshot();
328
399
  if (fellBack > 0 && signature.state !== router.globals.current.name);
400
+ if (drawn) checkSomeAdopted(tracker);
329
401
  return {
330
402
  outcome: "adopted",
331
- signature: compared
403
+ signature: compared,
404
+ views
332
405
  };
333
- }
406
+ };
334
407
  const compareSignature = (signature, landing, router) => {
335
408
  if (!signature) return "absent";
336
409
  return "transition" in landing && signatureMatches(signature, router) ? "matched" : "elsewhere";