@microsoft/webui-framework 0.0.18 → 0.0.20

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 (46) hide show
  1. package/README.md +130 -70
  2. package/dist/decorators.d.ts +1 -0
  3. package/dist/decorators.js +3 -0
  4. package/dist/element/diff.d.ts +3 -1
  5. package/dist/element/diff.js +197 -90
  6. package/dist/element/markers.js +13 -5
  7. package/dist/element/types.d.ts +18 -10
  8. package/dist/element/types.js +3 -0
  9. package/dist/element.d.ts +0 -4
  10. package/dist/element.js +11 -69
  11. package/dist/hydration-mismatch.js +3 -2
  12. package/dist/index.js +1 -1
  13. package/dist/lifecycle.d.ts +17 -0
  14. package/dist/lifecycle.js +66 -3
  15. package/dist/static-host.js +20 -10
  16. package/dist/streaming-activation.d.ts +2 -0
  17. package/dist/streaming-activation.js +13 -0
  18. package/dist/streaming-bootstrap.d.ts +2 -0
  19. package/dist/streaming-bootstrap.js +48 -0
  20. package/dist/streaming-cleanup.d.ts +4 -0
  21. package/dist/streaming-cleanup.js +66 -0
  22. package/dist/streaming-coordinator.d.ts +7 -0
  23. package/dist/streaming-coordinator.js +367 -0
  24. package/dist/streaming-deferred.d.ts +22 -0
  25. package/dist/streaming-deferred.js +273 -0
  26. package/dist/streaming-dom.d.ts +32 -0
  27. package/dist/streaming-dom.js +148 -0
  28. package/dist/streaming-entry.d.ts +1 -0
  29. package/dist/streaming-entry.js +2 -0
  30. package/dist/streaming-install.d.ts +2 -0
  31. package/dist/streaming-install.js +25 -0
  32. package/dist/streaming-mode.d.ts +5 -0
  33. package/dist/streaming-mode.js +14 -0
  34. package/dist/streaming-protocol.d.ts +32 -0
  35. package/dist/streaming-protocol.js +27 -0
  36. package/dist/streaming.d.ts +5 -0
  37. package/dist/streaming.js +9 -0
  38. package/dist/template-element.d.ts +28 -9
  39. package/dist/template-element.js +448 -238
  40. package/dist/template-events.js +1 -1
  41. package/dist/template-roots.d.ts +0 -1
  42. package/dist/template-roots.js +0 -11
  43. package/dist/template-types.d.ts +7 -1
  44. package/dist/template.d.ts +2 -0
  45. package/dist/template.js +46 -19
  46. package/package.json +9 -3
package/README.md CHANGED
@@ -87,16 +87,21 @@ Build with `--dom=shadow` (default) to wrap in a declarative shadow root, or `--
87
87
  <counter-card label="Taps"></counter-card>
88
88
  ```
89
89
 
90
- ### HTML-only components
90
+ ### HTML-only dormant components
91
91
 
92
92
  If a component has no event handlers, custom lifecycle code, or client-only
93
93
  methods, it can ship only `component.html` and optional `component.css`.
94
94
 
95
- When HTML-only components receive server or route state, import
96
- `@microsoft/webui-framework` somewhere in the browser entry. The framework root
97
- installs the static host runtime, which only claims compiler-owned HTML-only
98
- components whose templates need hidden state or observed host attributes. Fully
99
- static HTML-only components stay as plain SSR DOM.
95
+ The sibling `.ts` or `.js` file is the authored behavior boundary. With
96
+ manifest-enabled projection, only `@observable` and `@attr` fields opt into
97
+ initial state hydration; template-only roots stay in the trusted SSR DOM.
98
+ Without a module, template bindings render on the server and the component
99
+ contributes no projected keys. Without projection metadata, the server
100
+ preserves full state. The compiler still emits template metadata for scriptless
101
+ components. When the framework is loaded, it can activate that template when
102
+ browser state or client-side creation needs it.
103
+ If that first write omits a repeat collection, the host preserves the existing
104
+ SSR items until the collection is explicitly supplied.
100
105
 
101
106
  Create a custom element only for an Interactive Island: event handlers, custom
102
107
  lifecycle code, imperative methods, or state that TypeScript code reads or
@@ -111,9 +116,45 @@ cargo run -p microsoft-webui-cli -- build ./src --out ./dist --plugin=webui
111
116
 
112
117
  The WebUI plugin prepares component templates for the browser. Bundle your
113
118
  source browser entry directly. Import `@microsoft/webui-framework` from authored
114
- component modules, or once from the browser entry when the app has no authored
115
- components but still uses HTML-only components that receive server or route
116
- state.
119
+ component modules. An app that stays static after SSR needs no framework
120
+ browser import. Import the framework once when HTML-only components must accept
121
+ browser state or participate in soft navigation.
122
+
123
+ The plugin alone preserves full server state. To emit exact `@observable` and
124
+ `@attr` state surfaces, run the application's bundler first with
125
+ `@microsoft/webui/projection.js`, then pass its manifest to `webui build` with
126
+ `--projection-manifest`. The manifest tooling is build-only; this runtime
127
+ package does not depend on esbuild or TypeScript.
128
+
129
+ ### Progressive streaming hydration
130
+
131
+ Streaming applications opt into a separate side-effect entry:
132
+
133
+ ```ts
134
+ import '@microsoft/webui-framework/streaming.js';
135
+ import './counter-card.js';
136
+ ```
137
+
138
+ Import it before component registration modules and load the application entry
139
+ early with `<script type="module" async>` in `<head>`. The server must render
140
+ authored `<boundary>` directives through
141
+ `WebUIHandler::render_streaming`. The default
142
+ `@microsoft/webui-framework` entry has no dependency on the coordinator, so
143
+ normal applications pay no streaming bundle or initialization cost.
144
+
145
+ Each committed boundary receives its own ephemeral state object directly during
146
+ activation. The coordinator does not publish that state to
147
+ `window.__webui.state`, and it removes generated checkpoint scaffolding after
148
+ commit. Every commit also emits a `performance.mark()` — `webui:boundary:<id>`,
149
+ `webui:boundary:<id>:update`, or `webui:streaming:terminal` — which needs no
150
+ flag and no listener, so tooling that loads after hydration can still read it.
151
+ Set `window.__WEBUI_STREAMING_DEBUG__ = true` only when tooling needs the live
152
+ `webui:boundary-hydrated` event as well.
153
+
154
+ Set `window.__WEBUI_STREAMING_SLICE_MS__` to a positive millisecond budget to
155
+ make the coordinator yield between boundaries instead of draining its queue in
156
+ one pass. That is for pages where an intermediary coalesces the response into a
157
+ single chunk; it costs total hydration time, so leave it unset otherwise.
117
158
 
118
159
  ### Property binding lifecycle
119
160
 
@@ -134,12 +175,27 @@ re-render it. An `@observable` written before hydration finishes — in a field
134
175
  initializer, the `constructor`, or before `super.connectedCallback()` — cannot
135
176
  update that DOM, so the write is dropped and the runtime logs a
136
177
  `[WebUI] Hydration mismatch` warning naming the properties. Seed such values in
137
- the SSR state, or assign them after `super.connectedCallback()`. The warning is
178
+ the SSR state, or assign them from `hydratedCallback()`. The warning is
138
179
  development-only and is dead-code-eliminated from production bundles via the
139
180
  `__WEBUI_DEV__` compile-time flag (on by default; `webui-press build` sets it to
140
181
  `false`). See the
141
182
  [Interactivity Guide](https://microsoft.github.io/webui/guide/concepts/interactivity#setting-observable-state-during-setup).
142
183
 
184
+ Override the protected `hydratedCallback()` hook for work that requires the
185
+ component's bindings, events, and `w-ref` references to be ready. It runs
186
+ synchronously exactly once after the first successful ordinary SSR hydration,
187
+ client-created mount, deferred streamed activation, or dormant static-host wake.
188
+ Its once-latch is set before author code runs, so a thrown callback is not
189
+ retried on reconnect.
190
+
191
+ `connectedCallback()` remains a native per-connection lifecycle. On ordinary
192
+ SSR and client-created mounts, `super.connectedCallback()` hydrates
193
+ synchronously, but a streamed `data-ws` root returns while still deferred and
194
+ hydrates only when its boundary commits. Therefore `connectedCallback()` cannot
195
+ be used as a universal post-hydration signal. Descendants must not structurally
196
+ mutate a containing component's SSR subtree before it hydrates, because
197
+ hydration relies on stable compiled paths.
198
+
143
199
  ### DOM strategy (`--dom`)
144
200
 
145
201
  The `--dom` flag controls how the server renders component content:
@@ -168,6 +224,7 @@ Base class for framework components.
168
224
  | Member | Purpose |
169
225
  |--------|---------|
170
226
  | `static define(tagName)` | Register the class as a custom element |
227
+ | `protected hydratedCallback()` | Run once after the first successful hydration or client mount |
171
228
  | `$emit(name, detail?)` | Dispatch a bubbling, composed `CustomEvent` |
172
229
  | `$update()` | Force a reactive update (normally called automatically) |
173
230
  | `disconnectedCallback()` | Override for cleanup (global listeners, etc.) |
@@ -240,6 +297,7 @@ Notes:
240
297
 
241
298
  - default attribute names use kebab-case
242
299
  - attribute values arrive as strings
300
+ - during SSR hydration, an existing host attribute wins over projected state
243
301
  - use `@observable` for state that client code reads or mutates
244
302
 
245
303
  ### `@volatile`
@@ -269,7 +327,8 @@ The WebUI plugin supports these template features:
269
327
  - repeats: `<for each="item in items">`
270
328
 
271
329
  Components that use `@event` must have authored `.ts` or `.js` code that
272
- defines a `WebUIElement` for the tag; HTML-only components are declarative only.
330
+ defines a `WebUIElement` for the tag. HTML-only components do not provide
331
+ application event handlers.
273
332
 
274
333
  Example from `examples/app/todo-webui`:
275
334
 
@@ -302,8 +361,10 @@ Root-level events (e.g. `@toggle-item="{onToggleItem(e)}"`) can be declared on t
302
361
  - Use `w-ref` for true DOM-only concerns like focus or reading input values.
303
362
  - Omit `@observable` for values that are only read by the template and seeded
304
363
  externally after construction.
305
- - Omit the TypeScript class for HTML-only components that only need template
306
- bindings and router/server state.
364
+ - Omit the TypeScript class when compiled template behavior is sufficient,
365
+ including browser-applied state, route updates, and client-created instances.
366
+ Add a same-named module only for authored events, lifecycle, decorators, or
367
+ imperative APIs.
307
368
 
308
369
  Avoid imperative DOM mutation for application state that can be represented by reactive properties.
309
370
 
@@ -338,9 +399,9 @@ resource-constrained devices.
338
399
 
339
400
  5. **Single-pass hydration via path mapping.**
340
401
  SSR DOM is matched to compiled template bindings through
341
- template-parallel traversal (`$resolveSSR`). No marker comments, no
342
- data attributes — just path-based node resolution. The hydration walk
343
- touches each DOM node exactly once.
402
+ template-parallel traversal (`$resolveSSR`). Ordinary buffered hydration
403
+ needs no marker comments or data attributes for binding resolution. The
404
+ hydration walk touches each DOM node exactly once.
344
405
 
345
406
  6. **Keep the framework out of the GC's way.**
346
407
  Fewer JS objects = fewer GC pauses. Binding arrays are pre-built at
@@ -402,7 +463,10 @@ Angular all require a JavaScript runtime on the server. This framework's SSR
402
463
  is driven by data (template metadata + state values), not code. Any language
403
464
  that can read the compiled metadata and produce HTML can serve as the SSR
404
465
  backend. No comment markers or data attributes are needed — the runtime
405
- resolves SSR DOM nodes via template-parallel path traversal.
466
+ resolves ordinary buffered SSR nodes via template-parallel path traversal.
467
+ Progressive streaming uses temporary checkpoint scaffolding only to delay
468
+ activation until a complete region arrives; it removes that scaffolding after
469
+ commit.
406
470
 
407
471
  ### Build → Serve → Hydrate → Update
408
472
 
@@ -437,7 +501,7 @@ flowchart LR
437
501
  graph TD
438
502
  EL["element.ts (~850 lines)<br/><i>Orchestrator</i><br/>$mount, $wire, $hydrate,<br/>$resolveSSR, $applySSRState,<br/>$update, events, cleanup"]
439
503
 
440
- DIFF["element/diff.ts (~130 lines)<br/><i>List Reconciliation</i><br/>keyed/sequential diffing<br/>for @for repeat blocks"]
504
+ DIFF["element/diff.ts<br/><i>List Reconciliation</i><br/>positional + explicit-key diffing<br/>for &lt;for&gt; repeat blocks"]
441
505
 
442
506
  COND["element/conditions.ts<br/><i>Condition Evaluation</i><br/>evaluateCondition (iterative),<br/>conditionUsesPath"]
443
507
 
@@ -468,8 +532,8 @@ When the server renders a component, it emits HTML content (as a declarative
468
532
  shadow root or as light DOM children) along with an inert `#webui-data`
469
533
  JSON payload. The browser parses this DOM before any JavaScript runs.
470
534
  When the component's JS loads and `connectedCallback` fires, the framework
471
- uses compiled template paths to resolve SSR DOM nodes without any marker
472
- comments or data attributes:
535
+ uses compiled template paths to resolve ordinary buffered SSR DOM nodes without
536
+ binding markers:
473
537
 
474
538
  ```mermaid
475
539
  sequenceDiagram
@@ -484,11 +548,12 @@ sequenceDiagram
484
548
  CE->>CE: attributeChangedCallback (pre-existing attrs)
485
549
  CE->>FW: connectedCallback() → $mount()
486
550
  FW->>FW: SSR DOM detected (shadow root or children exist)
487
- FW->>FW: $applySSRState() — seed decorated + template state
551
+ FW->>FW: $applySSRState() — seed decorated state
488
552
  FW->>FW: $hydrate() — template-parallel path resolution
489
553
  FW->>FW: $resolveSSR() — match SSR nodes via ordinal traversal
490
554
  FW->>FW: $wireEvents() + $wireRefs()
491
555
  FW->>FW: $buildPathIndex(), $ready = true
556
+ FW->>CE: hydratedCallback() (once)
492
557
  Note over FW: DOM is already correct from SSR.<br/>No $update() call needed.
493
558
  ```
494
559
 
@@ -513,6 +578,7 @@ sequenceDiagram
513
578
  FW->>FW: $wireEvents() + $wireRefs()
514
579
  FW->>FW: $buildPathIndex(), $ready = true
515
580
  FW->>FW: $update() — flush initial property values
581
+ FW->>CE: hydratedCallback() (once)
516
582
  ```
517
583
 
518
584
  ---
@@ -538,7 +604,8 @@ interface TemplateMeta {
538
604
  sa?: string; // Adopted stylesheet specifier
539
605
  sd?: boolean; // Shadow DOM flag for client-created
540
606
  re?: [event, handler, argSpecs][]; // Root-level events
541
- th?: 1; // Compiler-owned static host
607
+ tr?: string[]; // Template state roots
608
+ ta?: string[]; // Host attributes aligned with tr
542
609
  }
543
610
  ```
544
611
 
@@ -635,61 +702,53 @@ sees `42` in the DOM before the component's JavaScript state exists. Without
635
702
  seeding, the first `$update()` would overwrite the SSR content with the wrong
636
703
  value.
637
704
 
638
- State seeding uses `window.__webui.state` — a JSON object loaded from the
639
- server-emitted `#webui-data` block. Like Preact's props, this delivers the
640
- same data used for SSR rendering to the client. During `$mount()`,
641
- `$applySSRState()` writes matching decorated keys directly to observable backing
642
- fields and stores undecorated template roots in hidden framework state before
643
- any bindings are wired:
705
+ State seeding uses `window.__webui.state` loaded from the server-emitted
706
+ `#webui-data` block. When the protocol contains projection metadata, only
707
+ `@observable` and `@attr` keys from reachable authored components select
708
+ initial state; HTML-only dormant components and authored template-only roots
709
+ contribute no startup keys. Without projection metadata, the server preserves
710
+ full state. During `$mount()`, `$applySSRState()` writes matching decorated keys
711
+ directly to observable backing fields before any bindings are wired:
644
712
 
645
713
  ```mermaid
646
714
  flowchart LR
647
- SCRIPT["&lt;script type='application/json' id='webui-data'&gt;<br/>{ state: { count: 42, title: 'Hello' } }"] --> APPLY["$applySSRState()"]
648
- APPLY --> SEED["Write decorated fields + hidden template state"]
715
+ SCRIPT["&lt;script type='application/json' id='webui-data'&gt;<br/>{ state: { count: 42 } }"] --> APPLY["$applySSRState()"]
716
+ APPLY --> SEED["Write decorated backing fields"]
649
717
  SEED --> HYDRATE["$hydrate() — bindings match<br/>server-rendered DOM"]
650
718
  ```
651
719
 
652
- `$applySSRState()` only accepts keys that are decorated properties or compiled
653
- template roots. Unknown keys are ignored. Decorated writes go to the backing
654
- field (`_prop`) directly, and undecorated template roots stay internal, avoiding
655
- reactive updates before bindings are wired.
720
+ Decorated writes go to the backing field (`_prop`) directly, avoiding reactive
721
+ updates before bindings are wired. For `@attr`, an existing SSR host attribute
722
+ takes precedence and the projected value is skipped. Template-only values
723
+ remain represented by the SSR DOM until browser state explicitly changes them.
724
+ Later `setState()` calls, including router partials, accept both decorated
725
+ properties and compiled template roots; undecorated roots are stored in hidden
726
+ framework state. The first write to a dormant HTML-only host replays only the
727
+ roots present in that write, preserving omitted SSR text, attributes,
728
+ conditions, and repeats.
656
729
 
657
730
  ---
658
731
 
659
732
  ## Repeat Reconciliation
660
733
 
661
- `@for(item of items)` blocks support two reconciliation strategies,
662
- implemented in `element/diff.ts` (~130 lines):
663
-
664
- ### Keyed Reconciliation
665
-
666
- When the repeat block's root element has attribute bindings (e.g.
667
- `<todo-item id="{{item.id}}">`), the framework uses the first attribute as a
668
- key. This preserves DOM nodes across reorders:
669
-
670
- ```mermaid
671
- flowchart TD
672
- subgraph Before ["Before: items = [A, B, C]"]
673
- A1["&lt;todo-item&gt; key=A"]
674
- B1["&lt;todo-item&gt; key=B"]
675
- C1["&lt;todo-item&gt; key=C"]
676
- end
677
-
678
- subgraph After ["After: items = [C, A]"]
679
- C2["&lt;todo-item&gt; key=C ← reused"]
680
- A2["&lt;todo-item&gt; key=A ← reused"]
681
- B2["key=B ← removed"]
682
- end
683
-
684
- A1 -.->|"moved"| A2
685
- C1 -.->|"moved"| C2
686
- B1 -.->|"destroyed"| B2
687
- ```
688
-
689
- ### Sequential Reconciliation
690
-
691
- When no keying attributes exist, items are matched by position. Excess items
692
- are removed; new items are appended.
734
+ `<for>` blocks reconcile by array position by default. The existing block at
735
+ index `i` receives the current item at index `i`; only a new or removed tail
736
+ creates or removes blocks.
737
+
738
+ Duplicate values and attributes are safe because dynamic attributes never act
739
+ as hidden keys. Reordering rebinds existing blocks in place, so local
740
+ browser-owned or component state remains associated with positions rather than
741
+ logical items.
742
+
743
+ For reorderable or stateful lists, author `key="{{item.id}}"` on the first
744
+ child inside `<for>` to move existing blocks with their logical items.
745
+ `key="{{item}}"` supports arrays of unique string or finite-number primitives.
746
+ `key` is compiler-only metadata and is removed from SSR and client HTML;
747
+ `data-key` remains an ordinary attribute with no identity semantics. Key paths
748
+ are compiler-validated and stored only for explicitly keyed repeats, so
749
+ unkeyed bindings carry no key state or map allocation. Duplicate or invalid
750
+ runtime keys warn once, clear identity, and use positional reconciliation until
751
+ valid identity is re-established.
693
752
 
694
753
  ### SSR State Reading
695
754
 
@@ -719,10 +778,11 @@ stylesheet specifier for a component.
719
778
 
720
779
  ## Path-Based Binding Resolution
721
780
 
722
- Unlike frameworks that use comment markers or data attributes to locate
723
- dynamic content, this framework uses **compiled template paths** — arrays of
781
+ Unlike frameworks that use comment markers or data attributes to locate each
782
+ dynamic binding, this framework uses **compiled template paths** — arrays of
724
783
  child-node indices that describe exactly where each binding lives in the DOM
725
- tree.
784
+ tree. Progressive streaming's temporary boundary markers locate complete
785
+ activation regions, not individual bindings.
726
786
 
727
787
  ### Client-created resolution (`$resolve`)
728
788
 
@@ -763,7 +823,7 @@ This template-parallel traversal eliminates the need for any marker comments,
763
823
  | Initial hydration | O(bindings) | Single pass over compiled path mappings |
764
824
  | Reactive update | O(affected) | Per-path index skips unrelated bindings |
765
825
  | Conditional toggle | O(block size) | Create/destroy a block instance |
766
- | Repeat reconciliation | O(items) | Keyed map lookup or sequential scan |
826
+ | Repeat reconciliation | O(items) | Positional scan; explicit keys use a reusable map only when order changes |
767
827
  | Event wiring | O(events) | One-time during hydration |
768
828
 
769
829
  ### What the framework does NOT do
@@ -1,6 +1,7 @@
1
1
  export declare function toKebabCase(str: string): string;
2
2
  export declare function getObservableNames(ctor: Function): Set<string>;
3
3
  export declare function isAttributeProperty(ctor: Function, property: string): boolean;
4
+ export declare function attributeNameForProperty(ctor: Function, property: string): string | undefined;
4
5
  export declare function syncAttrProperties(instance: object, ctor: Function): void;
5
6
  export declare function observable(target: object, name: string): void;
6
7
  export interface AttrOptions {
@@ -94,6 +94,9 @@ function attrPropertyMapFor(ctor) {
94
94
  export function isAttributeProperty(ctor, property) {
95
95
  return attrPropertyMapFor(ctor)?.has(property) === true;
96
96
  }
97
+ export function attributeNameForProperty(ctor, property) {
98
+ return attrPropertyMapFor(ctor)?.get(property)?.attribute;
99
+ }
97
100
  function setReflectingAttribute(instance, attrName) {
98
101
  instance[reflectingAttribute] = attrName;
99
102
  }
@@ -1,3 +1,5 @@
1
- import type { RepeatBinding, RepeatHost } from './types.js';
1
+ import type { RepeatBinding, RepeatHost, RepeatKeyState } from './types.js';
2
2
  export declare function dotWalk(cursor: unknown, path: string, from: number): unknown;
3
+ export declare function createRepeatKeyState(path: string): RepeatKeyState;
4
+ export declare function seedHydratedRepeatKeys(rep: RepeatBinding, items: unknown[]): void;
3
5
  export declare function syncRepeat(host: RepeatHost, rep: RepeatBinding): void;