@microsoft/webui-framework 0.0.4

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 (3) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +722 -0
  3. package/package.json +37 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Microsoft Corporation.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,722 @@
1
+ # `@microsoft/webui-framework`
2
+
3
+ Lightweight Web Component runtime for WebUI apps.
4
+
5
+ This package is the browser-side runtime used by `webui build --plugin=webui`. It provides:
6
+
7
+ - `WebUIElement` for SSR hydration and client-created elements
8
+ - `@observable`, `@attr`, and `@volatile` decorators
9
+ - compiled template path mapping for direct DOM binding resolution
10
+ - light DOM or shadow DOM rendering (`--dom=light|shadow` flag)
11
+ - SSR state seeding from `window.__webui_state` (like Preact's props)
12
+
13
+ If you are building WebUI apps in this repo, this is the component model used by examples like `examples/app/todo-webui`, `examples/app/commerce`, and `examples/app/contact-book-manager`.
14
+
15
+ > 📖 **Full documentation at [microsoft.github.io/webui](https://microsoft.github.io/webui)** — see the [Interactivity Guide](https://microsoft.github.io/webui/guide/concepts/interactivity) for component authoring patterns.
16
+
17
+ ## Install
18
+
19
+ In this workspace:
20
+
21
+ ```json
22
+ {
23
+ "dependencies": {
24
+ "@microsoft/webui-framework": "workspace:*"
25
+ }
26
+ }
27
+ ```
28
+
29
+ Outside the workspace:
30
+
31
+ ```bash
32
+ pnpm add @microsoft/webui-framework
33
+ ```
34
+
35
+ TypeScript must use legacy decorators:
36
+
37
+ ```json
38
+ {
39
+ "compilerOptions": {
40
+ "experimentalDecorators": true,
41
+ "useDefineForClassFields": false
42
+ }
43
+ }
44
+ ```
45
+
46
+ ## Quick Example
47
+
48
+ 1. Author a component class in TypeScript
49
+ 2. Author a WebUI template in HTML
50
+ 3. Run `webui build --plugin=webui`
51
+ 4. The runtime hydrates SSR output or creates client-side components using compiled path mapping
52
+
53
+ ### `counter-card.ts`
54
+
55
+ ```ts
56
+ import { WebUIElement, attr, observable, volatile } from '@microsoft/webui-framework';
57
+
58
+ export class CounterCard extends WebUIElement {
59
+ @attr label = 'Clicks';
60
+ @observable count = 0;
61
+
62
+ @volatile
63
+ get doubled(): number {
64
+ return this.count * 2;
65
+ }
66
+
67
+ increment(): void {
68
+ this.count += 1;
69
+ }
70
+ }
71
+
72
+ CounterCard.define('counter-card');
73
+ ```
74
+
75
+ ### `counter-card.html`
76
+
77
+ ```html
78
+ <p>{{label}}: {{count}} ({{doubled}})</p>
79
+ <button @click="{increment()}">Increment</button>
80
+ ```
81
+
82
+ Build with `--dom=shadow` (default) to wrap in a declarative shadow root, or `--dom=light` for light DOM rendering.
83
+
84
+ ### Use it from your page
85
+
86
+ ```html
87
+ <counter-card label="Taps"></counter-card>
88
+ ```
89
+
90
+ ### Build with the WebUI plugin
91
+
92
+ ```bash
93
+ cargo run -p microsoft-webui-cli -- build ./src --out ./dist --plugin=webui
94
+ ```
95
+
96
+ The compiler/plugin generates the template metadata consumed by the runtime. In normal app code, you should not need to hand-author `window.__webui_templates`.
97
+
98
+ ### DOM strategy (`--dom`)
99
+
100
+ The `--dom` flag controls how the server renders component content:
101
+
102
+ | Flag | Behavior |
103
+ |------|----------|
104
+ | `--dom=shadow` (default) | Wraps component HTML in `<template shadowrootmode="open">` |
105
+ | `--dom=light` | Renders component content as direct children of the host element |
106
+
107
+ The runtime auto-detects which mode was used at hydration time:
108
+ - If a `shadowRoot` already exists → shadow DOM SSR path
109
+ - If `childNodes` exist but no shadow root → light DOM SSR path
110
+ - If neither → client-created path (uses `meta.sd` or `window.__webui_shadow` to decide)
111
+
112
+ Light DOM is useful for simpler styling (CSS inheritance works naturally) and
113
+ better search-engine indexing. Shadow DOM provides style encapsulation.
114
+
115
+ ---
116
+
117
+ ## API Reference
118
+
119
+ ### `WebUIElement`
120
+
121
+ Base class for framework components.
122
+
123
+ | Member | Purpose |
124
+ |--------|---------|
125
+ | `static define(tagName)` | Register the class as a custom element |
126
+ | `$emit(name, detail?)` | Dispatch a bubbling, composed `CustomEvent` |
127
+ | `$update()` | Force a reactive update (normally called automatically) |
128
+ | `setInitialState(state, params?)` | Populate `@observable` properties from router state |
129
+ | `disconnectedCallback()` | Override for cleanup (global listeners, etc.) |
130
+
131
+ In most components you do not call `$update()` directly. Property changes through `@observable` and `@attr` trigger updates for you.
132
+
133
+ ### `@observable`
134
+
135
+ Marks a property as reactive. When the value changes, the framework
136
+ re-evaluates the compiled bindings that reference it.
137
+
138
+ ```ts
139
+ class SearchPanel extends WebUIElement {
140
+ @observable open = false;
141
+
142
+ toggle(): void {
143
+ this.open = !this.open;
144
+ }
145
+ }
146
+ ```
147
+
148
+ ### `@attr`
149
+
150
+ Like `@observable` but also reflects to/from an HTML attribute (kebab-case).
151
+
152
+ ```ts
153
+ class ProductPrice extends WebUIElement {
154
+ @attr currency = 'USD';
155
+ @attr({ attribute: 'amount-cents' }) amountCents = '0';
156
+ }
157
+ ```
158
+
159
+ Notes:
160
+
161
+ - default attribute names use kebab-case
162
+ - attribute values arrive as strings
163
+ - use `@observable` for richer client-only state
164
+
165
+ ### `@volatile`
166
+
167
+ Marks a computed getter that should be re-read whenever bindings access it.
168
+
169
+ ```ts
170
+ class CartSummary extends WebUIElement {
171
+ @observable items: Array<{ count: number }> = [];
172
+
173
+ @volatile
174
+ get totalCount(): number {
175
+ return this.items.reduce((sum, item) => sum + item.count, 0);
176
+ }
177
+ }
178
+ ```
179
+
180
+ ## Template Features
181
+
182
+ The WebUI plugin compiles these template features into runtime metadata:
183
+
184
+ - text bindings: `{{title}}`
185
+ - attribute bindings: `href="{{item.href}}"`
186
+ - event handlers: `@click="{onClick()}"`
187
+ - refs: `w-ref="addInput"`
188
+ - conditionals: `<if condition="...">`
189
+ - repeats: `<for each="item in items">`
190
+
191
+ Example from `examples/app/todo-webui`:
192
+
193
+ ```html
194
+ <h1>{{title}}</h1>
195
+
196
+ <input
197
+ class="add-input"
198
+ w-ref="addInput"
199
+ @keydown="{onAddKeydown(e)}"
200
+ />
201
+
202
+ <for each="item in items">
203
+ <todo-item
204
+ id="{{item.id}}"
205
+ title="{{item.title}}"
206
+ state="{{item.state}}"
207
+ ></todo-item>
208
+ </for>
209
+ ```
210
+
211
+ Root-level events (e.g. `@toggle-item="{onToggleItem(e)}"`) can be declared on the component's host element and are wired via `meta.re`.
212
+
213
+ ## Recommended Patterns
214
+
215
+ - Treat decorated properties as the source of truth.
216
+ - Update state with property assignments such as `this.open = !this.open`.
217
+ - Use `$emit()` for child-to-parent communication.
218
+ - Use `w-ref` for true DOM-only concerns like focus or reading input values.
219
+ - Prefer `@observable someValue!: T;` when a value is expected to be seeded externally after construction.
220
+
221
+ Avoid imperative DOM mutation for application state that can be represented by reactive properties.
222
+
223
+ ---
224
+
225
+ ## Performance Philosophy
226
+
227
+ This framework is designed for **minimal memory, minimal work, zero waste**.
228
+ Every design decision optimizes for real-world interactive performance on
229
+ resource-constrained devices.
230
+
231
+ ### Design principles
232
+
233
+ 1. **No work on the hot path that doesn't change the DOM.**
234
+ `$update(path)` only visits bindings that reference the changed property.
235
+ Everything else is skipped via a per-path index built once at hydration time.
236
+
237
+ 2. **Zero allocations during updates.**
238
+ Targeted updates are a single `Map.get()` → direct array iteration.
239
+ No intermediate arrays, no object creation, no spread operators on the
240
+ update path.
241
+
242
+ 3. **Parse once, clone forever.**
243
+ Compiled template HTML is parsed via `innerHTML` once per component tag
244
+ and cached as a `DocumentFragment`. Every subsequent instance uses
245
+ `cloneNode(true)` — DOM cloning is significantly faster than HTML parsing.
246
+
247
+ 4. **Delegate events, don't multiply listeners.**
248
+ Event bindings use delegation: one listener per event type on the
249
+ component root, with handler names resolved from compiled paths. 200
250
+ items × 5 events = 1 delegated listener, not 1000 closures.
251
+
252
+ 5. **Single-pass hydration via path mapping.**
253
+ SSR DOM is matched to compiled template bindings through
254
+ template-parallel traversal (`$resolveSSR`). No marker comments, no
255
+ data attributes — just path-based node resolution. The hydration walk
256
+ touches each DOM node exactly once.
257
+
258
+ 6. **Keep the framework out of the GC's way.**
259
+ Fewer JS objects = fewer GC pauses. Binding arrays are pre-built at
260
+ hydration time and reused across updates. No per-update temporaries.
261
+
262
+ ### Benchmark fixtures
263
+
264
+ The `tests/fixtures/bench/` directory contains Playwright-driven benchmarks
265
+ that validate these properties:
266
+
267
+ - **Update throughput**: 50k single-prop mutations with 65 bindings
268
+ - **Repeat instantiation**: 200 items created from compiled templates
269
+ - **Event memory**: 1000 event bindings measured via heap snapshots
270
+
271
+ Run benchmarks with:
272
+
273
+ ```bash
274
+ cd packages/webui-framework
275
+ npx playwright test tests/fixtures/bench/
276
+ ```
277
+
278
+ ### What NOT to do
279
+
280
+ When contributing to the runtime, avoid these patterns:
281
+
282
+ - **Don't allocate on the update path.** No `[...spread]`, no `new Map()`,
283
+ no object literals inside `$updateBindings` or `$updateInstance`.
284
+ - **Don't add `querySelector` calls during updates.** All DOM references are
285
+ pre-resolved at hydration time via compiled path mapping.
286
+ - **Don't use recursion in hot paths.** Condition evaluation and DOM walks
287
+ use iterative stacks.
288
+ - **Don't create closures per binding.** Use delegation or shared handlers.
289
+ - **Don't re-parse template HTML.** Always clone from the cached fragment.
290
+
291
+ ---
292
+
293
+ ## Architecture
294
+
295
+ ### How It Fits Together
296
+
297
+ ```
298
+ ┌──────────────────────┐ ┌───────────────────────┐ ┌──────────────────────┐
299
+ │ Rust Compiler │ │ Any Server │ │ Browser │
300
+ │ │ │ (Rust/Go/C#/…) │ │ │
301
+ │ HTML template │ │ │ │ SSR HTML (light or │
302
+ │ + expressions │────▶│ TemplateMeta (JSON) │────▶│ shadow DOM) + │
303
+ │ + @if / @for │ │ + state data │ │ __webui_state JSON │
304
+ │ │ │ │ │ │
305
+ │ Outputs: │ │ Renders: │ │ Hydrates: │
306
+ │ • TemplateMeta │ │ • Full HTML page │ │ • Path-based DOM │
307
+ │ • Static HTML │ │ • Shadow or light │ │ resolution │
308
+ │ • Binding metadata │ │ • State as JSON │ │ • O(1) updates │
309
+ └──────────────────────┘ └───────────────────────┘ └──────────────────────┘
310
+ ```
311
+
312
+ **Key differentiator: language-agnostic SSR.** React, Solid, Svelte, and
313
+ Angular all require a JavaScript runtime on the server. This framework's SSR
314
+ is driven by data (template metadata + state values), not code. Any language
315
+ that can read the compiled metadata and produce HTML can serve as the SSR
316
+ backend. No comment markers or data attributes are needed — the runtime
317
+ resolves SSR DOM nodes via template-parallel path traversal.
318
+
319
+ ### Build → Serve → Hydrate → Update
320
+
321
+ ```mermaid
322
+ flowchart LR
323
+ subgraph Build ["Build Time (Rust)"]
324
+ T[HTML Template] --> P[Parser Plugin]
325
+ P --> M[TemplateMeta JSON]
326
+ P --> H[Static HTML]
327
+ end
328
+
329
+ subgraph Serve ["Server (Any Language)"]
330
+ M --> R[Route Handler]
331
+ S[State Data] --> R
332
+ R --> HTML["Full SSR HTML<br/>(shadow or light DOM)<br/>+ TemplateMeta &lt;script&gt;<br/>+ __webui_state &lt;script&gt;"]
333
+ end
334
+
335
+ subgraph Browser ["Browser"]
336
+ HTML --> CE[Custom Element Upgrade]
337
+ CE --> MT{$mount}
338
+ MT -- SSR DOM exists --> SSR["$applySSRState<br/>$hydrate (path-based)"]
339
+ MT -- No SSR DOM --> CL["$wire (from template)"]
340
+ SSR --> BIND[Binding Arrays]
341
+ CL --> BIND
342
+ BIND --> UPD["$update() — O(1) patches"]
343
+ end
344
+ ```
345
+
346
+ ### Module Structure
347
+
348
+ ```mermaid
349
+ graph TD
350
+ EL["element.ts (~850 lines)<br/><i>Orchestrator</i><br/>$mount, $wire, $hydrate,<br/>$resolveSSR, $applySSRState,<br/>$update, events, cleanup"]
351
+
352
+ DIFF["element/diff.ts (~130 lines)<br/><i>List Reconciliation</i><br/>keyed/sequential diffing<br/>for @for repeat blocks"]
353
+
354
+ COND["element/conditions.ts<br/><i>Condition Evaluation</i><br/>evaluateCondition (iterative),<br/>conditionUsesPath"]
355
+
356
+ TYPES["element/types.ts<br/><i>Shared Types</i><br/>TemplateInstance, TextBinding,<br/>AttrBinding, CondBinding,<br/>RepeatBinding, ScopeFrame,<br/>RepeatHost"]
357
+
358
+ TMPL["template.ts<br/><i>Metadata Types + Registry</i><br/>TemplateMeta, getTemplate"]
359
+
360
+ DEC["decorators.ts<br/><i>Reactive Properties</i><br/>@observable, @attr, @volatile"]
361
+
362
+ LIFE["lifecycle.ts<br/><i>Hydration Timing</i><br/>Performance marks,<br/>hydration-complete event"]
363
+
364
+ EL --> DIFF
365
+ EL --> COND
366
+ EL --> TMPL
367
+ EL --> DEC
368
+ EL --> LIFE
369
+ DIFF --> TYPES
370
+ EL --> TYPES
371
+ ```
372
+
373
+ ---
374
+
375
+ ## Lifecycle Detail
376
+
377
+ ### SSR Hydration Path
378
+
379
+ When the server renders a component, it emits HTML content (as a declarative
380
+ shadow root or as light DOM children) along with a `window.__webui_state`
381
+ JSON payload. The browser parses this DOM before any JavaScript runs.
382
+ When the component's JS loads and `connectedCallback` fires, the framework
383
+ uses compiled template paths to resolve SSR DOM nodes without any marker
384
+ comments or data attributes:
385
+
386
+ ```mermaid
387
+ sequenceDiagram
388
+ participant Server
389
+ participant Browser
390
+ participant CE as Custom Element
391
+ participant FW as Framework
392
+
393
+ Server->>Browser: HTML (shadow or light DOM)<br/>+ __webui_state JSON
394
+ Browser->>Browser: Parse HTML → DOM exists
395
+ Browser->>CE: Custom element upgrade
396
+ CE->>CE: attributeChangedCallback (pre-existing attrs)
397
+ CE->>FW: connectedCallback() → $mount()
398
+ FW->>FW: SSR DOM detected (shadow root or children exist)
399
+ FW->>FW: $applySSRState() — seed observables from __webui_state
400
+ FW->>FW: $hydrate() — template-parallel path resolution
401
+ FW->>FW: $resolveSSR() — match SSR nodes via ordinal traversal
402
+ FW->>FW: $wireEvents() + $wireRefs()
403
+ FW->>FW: $buildPathIndex(), $ready = true
404
+ Note over FW: DOM is already correct from SSR.<br/>No $update() call needed.
405
+ ```
406
+
407
+ ### Client-Created Path
408
+
409
+ When a component is created dynamically (e.g. inside a `@for` loop or via
410
+ `document.createElement`), there's no SSR DOM:
411
+
412
+ ```mermaid
413
+ sequenceDiagram
414
+ participant App
415
+ participant CE as Custom Element
416
+ participant FW as Framework
417
+
418
+ App->>CE: document.createElement('my-comp')
419
+ App->>CE: Append to DOM
420
+ CE->>FW: connectedCallback() → $mount()
421
+ FW->>FW: No SSR DOM → client path
422
+ FW->>FW: Parse + clone template from meta.h
423
+ FW->>FW: Attach to shadow root or light DOM
424
+ FW->>FW: $wire(root, meta) — resolve via childNode paths
425
+ FW->>FW: $wireEvents() + $wireRefs()
426
+ FW->>FW: $buildPathIndex(), $ready = true
427
+ FW->>FW: $update() — flush initial property values
428
+ ```
429
+
430
+ ---
431
+
432
+ ## Compiled Template Metadata
433
+
434
+ The Rust compiler transforms HTML templates into a `TemplateMeta` JSON object
435
+ that describes every dynamic binding without any template syntax. This object
436
+ is delivered to the browser as a `<script>` tag.
437
+
438
+ ### Metadata Shape
439
+
440
+ ```typescript
441
+ interface TemplateMeta {
442
+ h: string; // Static HTML (no markers)
443
+ tx?: [slot, parts][]; // Text run locators
444
+ a?: CompiledAttrMeta[]; // Attribute bindings
445
+ ag?: [path, start, count][]; // Attribute target groups
446
+ c?: [conditionAST, blockIndex][]; // Conditional blocks
447
+ cl?: SlotPath[]; // Conditional anchor slots
448
+ r?: [collection, itemVar, blockIdx][];// Repeat blocks
449
+ rl?: SlotPath[]; // Repeat anchor slots
450
+ e?: [event, handler, needsEvent][]; // Events
451
+ el?: NodePath[]; // Event target paths
452
+ b?: TemplateBlockMeta[]; // Nested block metadata
453
+ sa?: string; // Adopted stylesheet specifier
454
+ sd?: boolean; // Shadow DOM flag for client-created
455
+ re?: [event, handler, needsEvent][]; // Root-level events
456
+ }
457
+ ```
458
+
459
+ ### Example
460
+
461
+ Template:
462
+ ```html
463
+ <h1>{{title}}</h1>
464
+ <button @click="increment">Count: {{count}}</button>
465
+ ```
466
+
467
+ Compiled metadata:
468
+ ```javascript
469
+ {
470
+ h: '<h1></h1><button>Count: </button>',
471
+ tx: [
472
+ [[[0], 0], [["title"]]], // slot in <h1>, dynamic "title"
473
+ [[[1], 1], ["Count: ", ["count"]]] // slot in <button>, static + dynamic
474
+ ],
475
+ e: [["click", "increment", 0]], // click → increment, no event arg
476
+ el: [[1]] // event target is child[1] (button)
477
+ }
478
+ ```
479
+
480
+ ### Condition AST
481
+
482
+ Conditions are emitted as compact tuples:
483
+
484
+ | Tuple | Meaning | Example |
485
+ |-------|---------|---------|
486
+ | `[0, path]` | Identifier (truthy check) | `@if(visible)` |
487
+ | `[1, left, op, right]` | Comparison predicate | `@if(count > 0)` |
488
+ | `[2, inner]` | Logical NOT | `@if(!visible)` |
489
+ | `[3, left, op, right]` | Compound AND/OR | `@if(a && b)` |
490
+
491
+ The runtime evaluates these iteratively (stack-based, no recursion) to avoid
492
+ call-stack depth in hot update paths.
493
+
494
+ ---
495
+
496
+ ## Reactive Update Model
497
+
498
+ ### How `@observable` Triggers Updates
499
+
500
+ ```mermaid
501
+ sequenceDiagram
502
+ participant App as Application Code
503
+ participant Dec as @observable setter
504
+ participant FW as $update('count')
505
+ participant IDX as Path Index
506
+ participant DOM
507
+
508
+ App->>Dec: this.count = 5
509
+ Dec->>Dec: Store in _count backing field
510
+ Dec->>Dec: Call countChanged(old, new) if defined
511
+ Dec->>FW: $update('count') (if element.isConnected)
512
+ FW->>IDX: Look up 'count' bindings + '*' wildcards
513
+ IDX-->>FW: 2 text bindings + 1 volatile binding
514
+ FW->>DOM: Patch only affected nodes
515
+ ```
516
+
517
+ ### Why Updates Are O(affected)
518
+
519
+ After hydration, every dynamic value in the template is connected to a direct
520
+ DOM node reference stored in a binding array. A per-path index maps each
521
+ `@observable` property name to the subset of bindings that reference it.
522
+
523
+ When `this.count = 5` fires, the `@observable` setter calls `$update('count')`,
524
+ which looks up `'count'` in the index and only patches the bindings that
525
+ actually depend on `count` — not every binding in the component.
526
+
527
+ Computed/volatile getters (paths not in the `@observable` set) are stored
528
+ under a wildcard key and always included in targeted updates.
529
+
530
+ ```typescript
531
+ // Targeted update (simplified):
532
+ const entry = this.$pathIndex.get(path); // O(1) map lookup
533
+ const wild = this.$pathIndex.get('*'); // volatile/computed bindings
534
+ // Only walk affected bindings, not all 65+
535
+ for (const binding of [...entry.texts, ...wild.texts]) {
536
+ if (binding.node.textContent !== str) {
537
+ binding.node.textContent = str; // Direct Text node reference
538
+ }
539
+ }
540
+ ```
541
+
542
+ No virtual DOM diffing. No selector queries. No tree walking. Each binding
543
+ is a pre-resolved pointer to the exact DOM node that needs updating, and the
544
+ path index ensures only affected pointers are visited.
545
+
546
+ ---
547
+
548
+ ## SSR State Seeding
549
+
550
+ When the server renders `<span>42</span>` for `@observable count = 0`, the
551
+ browser sees `42` in the DOM but the JavaScript property `this.count` is still
552
+ `0` (the class default). Without seeding, the first `$update()` would
553
+ overwrite the SSR content with the wrong value.
554
+
555
+ State seeding uses `window.__webui_state` — a JSON object emitted by the
556
+ server handler as a `<script>` tag. Like Preact's props, this delivers the
557
+ same data used for SSR rendering to the client. During `$mount()`,
558
+ `$applySSRState()` writes matching keys directly to observable backing fields
559
+ before any bindings are wired:
560
+
561
+ ```mermaid
562
+ flowchart LR
563
+ SCRIPT["&lt;script&gt;<br/>window.__webui_state = {<br/> count: 42,<br/> title: 'Hello'<br/>}"] --> APPLY["$applySSRState()"]
564
+ APPLY --> SEED["Write to backing fields:<br/>this._count = 42<br/>this._title = 'Hello'"]
565
+ SEED --> HYDRATE["$hydrate() — bindings match<br/>server-rendered DOM"]
566
+ ```
567
+
568
+ `$applySSRState()` only sets properties that exist in the component's
569
+ `@observable` set — unknown keys are ignored. Writes go to the backing
570
+ field (`_prop`) directly, avoiding reactive updates before bindings are wired.
571
+
572
+ ---
573
+
574
+ ## Repeat Reconciliation
575
+
576
+ `@for(item of items)` blocks support two reconciliation strategies,
577
+ implemented in `element/diff.ts` (~130 lines):
578
+
579
+ ### Keyed Reconciliation
580
+
581
+ When the repeat block's root element has attribute bindings (e.g.
582
+ `<todo-item id="{{item.id}}">`), the framework uses the first attribute as a
583
+ key. This preserves DOM nodes across reorders:
584
+
585
+ ```mermaid
586
+ flowchart TD
587
+ subgraph Before ["Before: items = [A, B, C]"]
588
+ A1["&lt;todo-item&gt; key=A"]
589
+ B1["&lt;todo-item&gt; key=B"]
590
+ C1["&lt;todo-item&gt; key=C"]
591
+ end
592
+
593
+ subgraph After ["After: items = [C, A]"]
594
+ C2["&lt;todo-item&gt; key=C ← reused"]
595
+ A2["&lt;todo-item&gt; key=A ← reused"]
596
+ B2["key=B ← removed"]
597
+ end
598
+
599
+ A1 -.->|"moved"| A2
600
+ C1 -.->|"moved"| C2
601
+ B1 -.->|"destroyed"| B2
602
+ ```
603
+
604
+ ### Sequential Reconciliation
605
+
606
+ When no keying attributes exist, items are matched by position. Excess items
607
+ are removed; new items are appended.
608
+
609
+ ### SSR State Reading
610
+
611
+ On initial hydration, the repeat system walks existing SSR children and
612
+ reconstructs collection instances by matching them against the compiled
613
+ template via `$resolveSSR` path traversal. State is already seeded from
614
+ `window.__webui_state`, so repeat items reflect the server-rendered list
615
+ without parsing marker comments.
616
+
617
+ ---
618
+
619
+ ## CSS Strategies
620
+
621
+ The framework supports three CSS delivery strategies:
622
+
623
+ | Strategy | How it works |
624
+ |----------|-------------|
625
+ | **Link** | `<link>` tag baked into `meta.h` — loaded by the browser naturally |
626
+ | **Inline** | `<style>` tag baked into `meta.h` — no external request |
627
+ | **Module** | `<style type="module" specifier="tag-name">` in the HTML payload, parsed into a `CSSStyleSheet` and applied via `adoptedStyleSheets` for shadow DOM isolation |
628
+
629
+ CSS module stylesheets are cached so each component instance adopts the same
630
+ parsed sheet without re-parsing CSS. The `meta.sa` field specifies the
631
+ stylesheet specifier for a component.
632
+
633
+ ---
634
+
635
+ ## Path-Based Binding Resolution
636
+
637
+ Unlike frameworks that use comment markers or data attributes to locate
638
+ dynamic content, this framework uses **compiled template paths** — arrays of
639
+ child-node indices that describe exactly where each binding lives in the DOM
640
+ tree.
641
+
642
+ ### Client-created resolution (`$resolve`)
643
+
644
+ For client-created components, the DOM matches `meta.h` exactly (it was cloned
645
+ from the parsed template fragment). Resolution is a simple child-node index
646
+ walk:
647
+
648
+ ```typescript
649
+ // path = [1, 0] → root.childNodes[1].childNodes[0]
650
+ let cur: Node = root;
651
+ for (const idx of path) {
652
+ cur = cur.childNodes[idx];
653
+ }
654
+ ```
655
+
656
+ ### SSR resolution (`$resolveSSR`)
657
+
658
+ SSR DOM may differ from the compiled template — the browser's HTML parser can
659
+ strip whitespace-only text nodes. `$resolveSSR` walks the SSR DOM and the
660
+ compiled template DOM **in parallel**, translating each child-node index into
661
+ an element-ordinal or text-ordinal lookup:
662
+
663
+ ```typescript
664
+ // For element nodes: count element siblings up to idx in template,
665
+ // then find the element at that ordinal in SSR DOM.
666
+ // For text nodes: same approach with text node ordinals.
667
+ ```
668
+
669
+ This template-parallel traversal eliminates the need for any marker comments,
670
+ `data-*` attributes, or DOM annotations. The SSR server emits clean HTML.
671
+
672
+ ---
673
+
674
+ ## Performance Characteristics
675
+
676
+ | Operation | Cost | Why |
677
+ |-----------|------|-----|
678
+ | Initial hydration | O(bindings) | Single pass over compiled path mappings |
679
+ | Reactive update | O(affected) | Per-path index skips unrelated bindings |
680
+ | Conditional toggle | O(block size) | Create/destroy a block instance |
681
+ | Repeat reconciliation | O(items) | Keyed map lookup or sequential scan |
682
+ | Event wiring | O(events) | One-time during hydration |
683
+
684
+ ### What the framework does NOT do
685
+
686
+ - **No virtual DOM** — no tree copy, no diff algorithm
687
+ - **No runtime template parsing** — the Rust compiler handles all syntax
688
+ - **No `innerHTML` on updates** — only `textContent` and `setAttribute`
689
+ - **No `querySelector` on updates** — all nodes are pre-resolved references
690
+ - **No recursion in hot paths** — conditions use iterative stack evaluation
691
+
692
+ ---
693
+
694
+ ## Debugging Hydration
695
+
696
+ The runtime exposes hydration timing via the Performance API:
697
+
698
+ - Per component: `webui:hydrate:<tag>:start` / `webui:hydrate:<tag>:end`
699
+ - Global: `webui:hydrate:total:start` / `webui:hydrate:total:end`
700
+ - Window event: `webui:hydration-complete`
701
+
702
+ ```ts
703
+ window.addEventListener('webui:hydration-complete', () => {
704
+ console.log('All initial framework components are hydrated.');
705
+ });
706
+ ```
707
+
708
+ ---
709
+
710
+ ## Where to Look Next
711
+
712
+ - `examples/app/todo-webui`
713
+ - `examples/app/contact-book-manager`
714
+ - `examples/app/commerce`
715
+
716
+ ## Package Development
717
+
718
+ ```bash
719
+ pnpm --dir packages/webui-framework build
720
+ pnpm --dir packages/webui-framework typecheck
721
+ pnpm --dir packages/webui-framework test
722
+ ```
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@microsoft/webui-framework",
3
+ "version": "0.0.4",
4
+ "type": "module",
5
+ "description": "WebUI Framework Next — Preact-inspired lightweight Web Component runtime with SSR hydration. 15KB minified, compiled-template path mapping, no hydration markers.",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": {
9
+ "import": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ }
14
+ },
15
+ "files": [
16
+ "dist/"
17
+ ],
18
+ "devDependencies": {
19
+ "@playwright/test": "^1.58.2",
20
+ "@types/node": "^25.3.5",
21
+ "esbuild": "^0.27.3",
22
+ "typescript": "^5.9.3",
23
+ "@microsoft/webui-test-support": "0.0.4"
24
+ },
25
+ "scripts": {
26
+ "build": "find src -name '*.ts' ! -name '*.test.ts' | xargs esbuild --outdir=dist --outbase=src --format=esm --platform=browser && tsc --emitDeclarationOnly",
27
+ "typecheck": "tsc --noEmit",
28
+ "typecheck:e2e": "tsc -p tsconfig.test.json --noEmit",
29
+ "build:e2e:scripts": "esbuild ./tests/server.ts --bundle --external:esbuild --outdir=tests/dist --format=esm --platform=node --target=es2022",
30
+ "build:e2e": "pnpm build:e2e:scripts",
31
+ "test": "pnpm test:unit && pnpm test:e2e",
32
+ "test:unit": "esbuild \"src/**/*.test.ts\" --bundle --outdir=dist/tests --outbase=src --format=esm --platform=node --target=es2022 && node --test \"dist/tests/**/*.test.js\"",
33
+ "test:e2e": "pnpm typecheck:e2e && pnpm build:e2e && playwright test",
34
+ "test:update-snapshots": "pnpm test:e2e:update-snapshots",
35
+ "test:e2e:update-snapshots": "pnpm typecheck:e2e && pnpm build:e2e && playwright test --update-snapshots"
36
+ }
37
+ }