@microsoft/webui-framework 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -21
- package/dist/decorators.js +2 -2
- package/dist/element.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/template-types.d.ts +10 -10
- package/dist/template.d.ts +7 -7
- package/dist/template.js +92 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ Outside the workspace:
|
|
|
32
32
|
pnpm add @microsoft/webui-framework
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
TypeScript must
|
|
35
|
+
TypeScript must enable decorator emit:
|
|
36
36
|
|
|
37
37
|
```json
|
|
38
38
|
{
|
|
@@ -93,7 +93,7 @@ Build with `--dom=shadow` (default) to wrap in a declarative shadow root, or `--
|
|
|
93
93
|
cargo run -p microsoft-webui-cli -- build ./src --out ./dist --plugin=webui
|
|
94
94
|
```
|
|
95
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`.
|
|
96
|
+
The compiler/plugin generates the template metadata and condition closure arrays consumed by the runtime. In normal app code, you should not need to hand-author `window.__webui.templates` or `window.__webui.templateFns`.
|
|
97
97
|
|
|
98
98
|
### Property binding lifecycle
|
|
99
99
|
|
|
@@ -311,7 +311,7 @@ When contributing to the runtime, avoid these patterns:
|
|
|
311
311
|
│ │ │ (Rust/Go/C#/…) │ │ │
|
|
312
312
|
│ HTML template │ │ │ │ SSR HTML (light or │
|
|
313
313
|
│ + expressions │────▶│ TemplateMeta (JSON) │────▶│ shadow DOM) + │
|
|
314
|
-
│ + @if / @for │ │ + state data │ │
|
|
314
|
+
│ + @if / @for │ │ + state data │ │ webui-data JSON │
|
|
315
315
|
│ │ │ │ │ │
|
|
316
316
|
│ Outputs: │ │ Renders: │ │ Hydrates: │
|
|
317
317
|
│ • TemplateMeta │ │ • Full HTML page │ │ • Path-based DOM │
|
|
@@ -340,7 +340,7 @@ flowchart LR
|
|
|
340
340
|
subgraph Serve ["Server (Any Language)"]
|
|
341
341
|
M --> R[Route Handler]
|
|
342
342
|
S[State Data] --> R
|
|
343
|
-
R --> HTML["Full SSR HTML<br/>(shadow or light DOM)<br/>+
|
|
343
|
+
R --> HTML["Full SSR HTML<br/>(shadow or light DOM)<br/>+ inert #webui-data"]
|
|
344
344
|
end
|
|
345
345
|
|
|
346
346
|
subgraph Browser ["Browser"]
|
|
@@ -366,7 +366,7 @@ graph TD
|
|
|
366
366
|
|
|
367
367
|
TYPES["element/types.ts<br/><i>Shared Types</i><br/>TemplateInstance, TextBinding,<br/>AttrBinding, CondBinding,<br/>RepeatBinding, ScopeFrame,<br/>RepeatHost"]
|
|
368
368
|
|
|
369
|
-
TMPL["template.ts<br/><i>Metadata Types + Registry</i><br/>TemplateMeta, getTemplate"]
|
|
369
|
+
TMPL["template.ts<br/><i>Metadata Types + Registry</i><br/>TemplateMeta, getTemplate,<br/>registerTemplateData"]
|
|
370
370
|
|
|
371
371
|
DEC["decorators.ts<br/><i>Reactive Properties</i><br/>@observable, @attr, @volatile"]
|
|
372
372
|
|
|
@@ -388,7 +388,7 @@ graph TD
|
|
|
388
388
|
### SSR Hydration Path
|
|
389
389
|
|
|
390
390
|
When the server renders a component, it emits HTML content (as a declarative
|
|
391
|
-
shadow root or as light DOM children) along with
|
|
391
|
+
shadow root or as light DOM children) along with an inert `#webui-data`
|
|
392
392
|
JSON payload. The browser parses this DOM before any JavaScript runs.
|
|
393
393
|
When the component's JS loads and `connectedCallback` fires, the framework
|
|
394
394
|
uses compiled template paths to resolve SSR DOM nodes without any marker
|
|
@@ -401,7 +401,7 @@ sequenceDiagram
|
|
|
401
401
|
participant CE as Custom Element
|
|
402
402
|
participant FW as Framework
|
|
403
403
|
|
|
404
|
-
Server->>Browser: HTML (shadow or light DOM)<br/>+
|
|
404
|
+
Server->>Browser: HTML (shadow or light DOM)<br/>+ inert #webui-data JSON
|
|
405
405
|
Browser->>Browser: Parse HTML → DOM exists
|
|
406
406
|
Browser->>CE: Custom element upgrade
|
|
407
407
|
CE->>CE: attributeChangedCallback (pre-existing attrs)
|
|
@@ -486,19 +486,16 @@ Compiled metadata:
|
|
|
486
486
|
}
|
|
487
487
|
```
|
|
488
488
|
|
|
489
|
-
### Condition
|
|
489
|
+
### Condition references
|
|
490
490
|
|
|
491
|
-
Conditions are emitted as
|
|
491
|
+
Conditions are emitted as `[functionIndex, paths]` references. The index points
|
|
492
|
+
to a component-local closure in `window.__webui.templateFns[tagName]`, while
|
|
493
|
+
`paths` lets the runtime build targeted reactive indexes without parsing
|
|
494
|
+
function source.
|
|
492
495
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
| `[1, left, op, right]` | Comparison predicate | `@if(count > 0)` |
|
|
497
|
-
| `[2, inner]` | Logical NOT | `@if(!visible)` |
|
|
498
|
-
| `[3, left, op, right]` | Compound AND/OR | `@if(a && b)` |
|
|
499
|
-
|
|
500
|
-
The runtime evaluates these iteratively (stack-based, no recursion) to avoid
|
|
501
|
-
call-stack depth in hot update paths.
|
|
496
|
+
The runtime normalizes each condition reference into `[fn, paths]` once before
|
|
497
|
+
hydration or client-created wiring, so hot update paths call the closure
|
|
498
|
+
directly.
|
|
502
499
|
|
|
503
500
|
---
|
|
504
501
|
|
|
@@ -561,15 +558,15 @@ browser sees `42` in the DOM but the JavaScript property `this.count` is still
|
|
|
561
558
|
`0` (the class default). Without seeding, the first `$update()` would
|
|
562
559
|
overwrite the SSR content with the wrong value.
|
|
563
560
|
|
|
564
|
-
State seeding uses `window.__webui.state` — a JSON object
|
|
565
|
-
server
|
|
561
|
+
State seeding uses `window.__webui.state` — a JSON object loaded from the
|
|
562
|
+
server-emitted `#webui-data` block. Like Preact's props, this delivers the
|
|
566
563
|
same data used for SSR rendering to the client. During `$mount()`,
|
|
567
564
|
`$applySSRState()` writes matching keys directly to observable backing fields
|
|
568
565
|
before any bindings are wired:
|
|
569
566
|
|
|
570
567
|
```mermaid
|
|
571
568
|
flowchart LR
|
|
572
|
-
SCRIPT["<script><br/>
|
|
569
|
+
SCRIPT["<script type='application/json' id='webui-data'><br/>{ state: { count: 42, title: 'Hello' } }"] --> APPLY["$applySSRState()"]
|
|
573
570
|
APPLY --> SEED["Write to backing fields:<br/>this._count = 42<br/>this._title = 'Hello'"]
|
|
574
571
|
SEED --> HYDRATE["$hydrate() — bindings match<br/>server-rendered DOM"]
|
|
575
572
|
```
|
package/dist/decorators.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Reactive decorators for WebUIElement properties.
|
|
5
5
|
*
|
|
6
|
-
* Uses
|
|
7
|
-
*
|
|
6
|
+
* Uses TypeScript's `experimentalDecorators` emit, matching the FAST ecosystem
|
|
7
|
+
* conventions.
|
|
8
8
|
*/
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
// Internal helpers
|
package/dist/element.js
CHANGED
|
@@ -540,7 +540,7 @@ export class WebUIElement extends HTMLElement {
|
|
|
540
540
|
const parent = parentPath.length > 0 ? this.$resolve(root, parentPath) : root;
|
|
541
541
|
if (!parent || (parent.nodeType !== 1 && parent.nodeType !== 11))
|
|
542
542
|
continue;
|
|
543
|
-
condRefs.push({ parent, ref: parent.childNodes[beforeIndex] || null, condition, blockIndex });
|
|
543
|
+
condRefs.push({ parent, ref: parent.childNodes[beforeIndex] || null, condition: condition, blockIndex });
|
|
544
544
|
}
|
|
545
545
|
}
|
|
546
546
|
const repRefs = [];
|
|
@@ -719,7 +719,7 @@ export class WebUIElement extends HTMLElement {
|
|
|
719
719
|
}
|
|
720
720
|
}
|
|
721
721
|
instance.conds.push({
|
|
722
|
-
condition, blockIndex,
|
|
722
|
+
condition: condition, blockIndex,
|
|
723
723
|
anchor: condAnchor,
|
|
724
724
|
scope, instance: condInstance,
|
|
725
725
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,6 @@
|
|
|
19
19
|
*/
|
|
20
20
|
export { WebUIElement } from './element.js';
|
|
21
21
|
export { observable, attr } from './decorators.js';
|
|
22
|
-
export { getTemplate } from './template.js';
|
|
22
|
+
export { getTemplate, registerTemplateData } from './template.js';
|
|
23
23
|
export type { TemplateMeta } from './template.js';
|
|
24
24
|
export { hydrationStart, hydrationEnd } from './lifecycle.js';
|
package/dist/index.js
CHANGED
|
@@ -21,5 +21,5 @@
|
|
|
21
21
|
*/
|
|
22
22
|
export { WebUIElement } from './element.js';
|
|
23
23
|
export { observable, attr } from './decorators.js';
|
|
24
|
-
export { getTemplate } from './template.js';
|
|
24
|
+
export { getTemplate, registerTemplateData } from './template.js';
|
|
25
25
|
export { hydrationStart, hydrationEnd } from './lifecycle.js';
|
package/dist/template-types.d.ts
CHANGED
|
@@ -16,19 +16,19 @@ export type CompiledAttrGroupMeta = [
|
|
|
16
16
|
count: number
|
|
17
17
|
];
|
|
18
18
|
/**
|
|
19
|
-
* Compiled condition —
|
|
20
|
-
* The Rust compiler emits the function
|
|
21
|
-
*
|
|
19
|
+
* Compiled condition — JSON metadata carries a function index plus the paths it
|
|
20
|
+
* references. The Rust compiler emits the actual function bodies in a separate
|
|
21
|
+
* closure array, and the runtime normalizes indexes to functions once.
|
|
22
22
|
*
|
|
23
|
-
* - `[0]` — evaluator function
|
|
23
|
+
* - `[0]` — evaluator function or component-local function index
|
|
24
24
|
* - `[1]` — referenced paths for the reactive path index
|
|
25
25
|
*/
|
|
26
|
-
export type
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
export type CompiledConditionalMeta = [condition:
|
|
31
|
-
export type CompiledAttrMeta = [name: string, kind: 0, value: string] | [name: string, kind: 1, value: string] | [name: string, kind: 2, condition:
|
|
26
|
+
export type CompiledConditionFn = (v: (path: string, s?: unknown) => unknown, s?: unknown) => boolean;
|
|
27
|
+
export type CompiledCondition = [fn: CompiledConditionFn, paths: string[]];
|
|
28
|
+
export type SerializedCompiledCondition = [fnIndex: number, paths: string[]];
|
|
29
|
+
export type TemplateCondition = CompiledCondition | SerializedCompiledCondition;
|
|
30
|
+
export type CompiledConditionalMeta = [condition: TemplateCondition, blockIndex: number, slot: TemplateSlotPath];
|
|
31
|
+
export type CompiledAttrMeta = [name: string, kind: 0, value: string] | [name: string, kind: 1, value: string] | [name: string, kind: 2, condition: TemplateCondition] | [name: string, kind: 3, parts: CompiledAttrPart[]];
|
|
32
32
|
export type CompiledRepeatMeta = [collection: string, itemVar: string, blockIndex: number, slot: TemplateSlotPath];
|
|
33
33
|
export type CompiledEventArg = ['e'] | ['p', string] | ['s', string] | ['n', number] | ['b', number] | ['z'];
|
|
34
34
|
export type CompiledEventArgs = CompiledEventArg[];
|
package/dist/template.d.ts
CHANGED
|
@@ -6,26 +6,26 @@
|
|
|
6
6
|
* - `tx` — text runs `[slot, parts]` for text binding positions
|
|
7
7
|
* - `a` — attribute binding metadata
|
|
8
8
|
* - `ag` — attribute target groups `[path, startIndex, count]`
|
|
9
|
-
* - `c` — conditional blocks `[
|
|
10
|
-
* - `
|
|
11
|
-
* - `r` — repeat/for blocks `[collection, itemVar, blockIndex]`
|
|
12
|
-
* - `rl` — repeat anchor slots
|
|
9
|
+
* - `c` — conditional blocks `[conditionRef, blockIndex, slot]`
|
|
10
|
+
* - `r` — repeat/for blocks `[collection, itemVar, blockIndex, slot]`
|
|
13
11
|
* - `e` — element events `[eventName, handlerName, argSpecs, targetPath]`
|
|
14
12
|
* - `b` — nested compiled block metadata
|
|
15
13
|
* - `sa` — adopted stylesheet specifier for CSS module strategy
|
|
16
14
|
* - `sd` — shadow DOM flag for client-created components
|
|
17
15
|
* - `re` — root events on the host element
|
|
18
16
|
*/
|
|
19
|
-
export type { CompiledAttrGroupMeta, CompiledAttrMeta, CompiledAttrPart, CompiledCondition, CompiledConditionalMeta, CompiledEventArg, CompiledEventArgs, CompiledTextRunMeta, TemplateBlockMeta, TemplateMeta, TemplateNodePath, TemplateSlotPath, } from './template-types.js';
|
|
20
|
-
import type { TemplateMeta } from './template-types.js';
|
|
17
|
+
export type { CompiledAttrGroupMeta, CompiledAttrMeta, CompiledAttrPart, CompiledCondition, CompiledConditionFn, CompiledConditionalMeta, CompiledEventArg, CompiledEventArgs, SerializedCompiledCondition, TemplateCondition, CompiledTextRunMeta, TemplateBlockMeta, TemplateMeta, TemplateNodePath, TemplateSlotPath, } from './template-types.js';
|
|
18
|
+
import type { CompiledConditionFn, TemplateMeta } from './template-types.js';
|
|
21
19
|
declare global {
|
|
22
20
|
interface Window {
|
|
23
|
-
/** Consolidated SSR
|
|
21
|
+
/** Consolidated SSR metadata loaded from `#webui-data` or partial responses. */
|
|
24
22
|
__webui?: {
|
|
25
23
|
state?: Record<string, unknown>;
|
|
26
24
|
templates?: Record<string, TemplateMeta>;
|
|
25
|
+
templateFns?: Record<string, CompiledConditionFn[]>;
|
|
27
26
|
[key: string]: unknown;
|
|
28
27
|
};
|
|
29
28
|
}
|
|
30
29
|
}
|
|
31
30
|
export declare function getTemplate(name: string): TemplateMeta | undefined;
|
|
31
|
+
export declare function registerTemplateData(templates: Record<string, TemplateMeta>, templateFns?: Record<string, CompiledConditionFn[]>): void;
|
package/dist/template.js
CHANGED
|
@@ -1,5 +1,96 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT license.
|
|
3
|
+
const WEBUI_DATA_ID = 'webui-data';
|
|
4
|
+
const normalizedTemplates = new WeakSet();
|
|
5
|
+
let webuiDataLoaded = false;
|
|
3
6
|
export function getTemplate(name) {
|
|
4
|
-
|
|
7
|
+
let meta = window.__webui?.templates?.[name];
|
|
8
|
+
if (!meta) {
|
|
9
|
+
loadWebUIDataBlock();
|
|
10
|
+
meta = window.__webui?.templates?.[name];
|
|
11
|
+
}
|
|
12
|
+
if (meta)
|
|
13
|
+
normalizeTemplate(name, meta);
|
|
14
|
+
return meta;
|
|
15
|
+
}
|
|
16
|
+
export function registerTemplateData(templates, templateFns) {
|
|
17
|
+
const w = window;
|
|
18
|
+
if (!w.__webui)
|
|
19
|
+
w.__webui = {};
|
|
20
|
+
if (!w.__webui.templates)
|
|
21
|
+
w.__webui.templates = {};
|
|
22
|
+
if (templateFns) {
|
|
23
|
+
if (!w.__webui.templateFns)
|
|
24
|
+
w.__webui.templateFns = {};
|
|
25
|
+
const fnNames = Object.keys(templateFns);
|
|
26
|
+
for (let i = 0; i < fnNames.length; i++) {
|
|
27
|
+
const tag = fnNames[i];
|
|
28
|
+
w.__webui.templateFns[tag] = templateFns[tag];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const names = Object.keys(templates);
|
|
32
|
+
for (let i = 0; i < names.length; i++) {
|
|
33
|
+
const tag = names[i];
|
|
34
|
+
const meta = templates[tag];
|
|
35
|
+
w.__webui.templates[tag] = meta;
|
|
36
|
+
normalizeTemplate(tag, meta);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function loadWebUIDataBlock() {
|
|
40
|
+
if (webuiDataLoaded || window.__webui?.state !== undefined || typeof document === 'undefined')
|
|
41
|
+
return;
|
|
42
|
+
const el = document.getElementById(WEBUI_DATA_ID);
|
|
43
|
+
if (!el) {
|
|
44
|
+
webuiDataLoaded = true;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const text = el.textContent;
|
|
48
|
+
if (text) {
|
|
49
|
+
const templateFns = window.__webui?.templateFns;
|
|
50
|
+
const parsed = JSON.parse(text);
|
|
51
|
+
if (templateFns)
|
|
52
|
+
parsed.templateFns = templateFns;
|
|
53
|
+
window.__webui = parsed;
|
|
54
|
+
}
|
|
55
|
+
el.remove();
|
|
56
|
+
webuiDataLoaded = true;
|
|
57
|
+
}
|
|
58
|
+
function normalizeTemplate(name, meta) {
|
|
59
|
+
if (normalizedTemplates.has(meta))
|
|
60
|
+
return;
|
|
61
|
+
const fns = window.__webui?.templateFns?.[name] ?? [];
|
|
62
|
+
const stack = [meta];
|
|
63
|
+
while (stack.length > 0) {
|
|
64
|
+
const block = stack.pop();
|
|
65
|
+
if (!block)
|
|
66
|
+
continue;
|
|
67
|
+
if (block.a) {
|
|
68
|
+
for (let i = 0; i < block.a.length; i++) {
|
|
69
|
+
const attr = block.a[i];
|
|
70
|
+
if (attr[1] === 2)
|
|
71
|
+
normalizeCondition(name, attr[2], fns);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (block.c) {
|
|
75
|
+
for (let i = 0; i < block.c.length; i++) {
|
|
76
|
+
normalizeCondition(name, block.c[i][0], fns);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const children = block.b;
|
|
80
|
+
if (children) {
|
|
81
|
+
for (let i = 0; i < children.length; i++)
|
|
82
|
+
stack.push(children[i]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
normalizedTemplates.add(meta);
|
|
86
|
+
}
|
|
87
|
+
function normalizeCondition(tagName, condition, fns) {
|
|
88
|
+
const first = condition[0];
|
|
89
|
+
if (typeof first === 'function')
|
|
90
|
+
return;
|
|
91
|
+
const fn = fns[first];
|
|
92
|
+
if (typeof fn !== 'function') {
|
|
93
|
+
throw new Error(`[WebUI] Missing condition closure ${first} for <${tagName}>.`);
|
|
94
|
+
}
|
|
95
|
+
condition[0] = fn;
|
|
5
96
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microsoft/webui-framework",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "WebUI Framework Next — Preact-inspired lightweight Web Component runtime with SSR hydration. 15KB minified, compiled-template path mapping, no hydration markers.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"@playwright/test": "^1.58.2",
|
|
20
20
|
"@types/node": "^25.3.5",
|
|
21
21
|
"typescript": "^5.9.3",
|
|
22
|
-
"@microsoft/webui-test-support": "0.0.
|
|
22
|
+
"@microsoft/webui-test-support": "0.0.17"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsc",
|