Skip to content

Commit 03fe114

Browse files
feat: introduce @microsoft/webui-framework
Introduce @microsoft/webui-framework — a lightweight Web Component runtime that hydrates server-rendered HTML using compiled template metadata and direct DOM binding. No virtual DOM, no runtime template parsing, no JavaScript runtime required on the server. - WebUIElement base class with @observable, @attr, @volatile decorators - Single-pass SSR hydration that seeds component state inline from markers - Per-path targeted updates via a binding index (O(affected) not O(all)) - Template cloning cache for fast repeat instantiation (cloneNode not innerHTML) - Zero-allocation update path with pre-merged wildcard bindings - Event delegation (one listener per event type, not one closure per element) - Keyed and sequential repeat reconciliation for @for loops - Iterative condition AST evaluation for @if blocks (no recursion) - CSS module stylesheet cache via adoptedStyleSheets - Modular architecture: element.ts orchestrator + repeat, paths, conditions, seed, types, and styles modules with file-level documentation - Co-located unit tests (conditions, seed, types, template) and Playwright e2e fixtures covering hydration, reactivity, repeats, conditionals, events, refs, state seeding, CSS strategies, and performance benchmarks - Shared test infrastructure via @microsoft/webui-test-support (private) - HTML entity decoding in compiled text run static parts - Comprehensive README with architecture diagrams, performance philosophy, API reference, and contributor guidelines - Hydration docs, copilot instructions, and webui-dev skill updated
1 parent 568821a commit 03fe114

88 files changed

Lines changed: 8417 additions & 135 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/skills/webui-dev/SKILL.md

Lines changed: 91 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
---
22
name: webui-dev
3-
description: Build interactive WebUI example apps with FAST-HTML hydration, template syntax, and component patterns.
3+
description: Build interactive WebUI example apps with compiled-template hydration, template syntax, and component patterns.
44
---
55

6-
# WebUI App Development with FAST-HTML
6+
# WebUI App Development
77

88
Use this skill when building or modifying example apps under `examples/app/`.
99

10-
WebUI server-renders HTML at request time. FAST-HTML hydrates the pre-rendered DOM on the client, making it interactive without a full re-render.
10+
WebUI server-renders HTML at request time. The `@microsoft/webui-framework` runtime hydrates the pre-rendered DOM on the client using compiled template metadata and direct DOM binding — no virtual DOM, no runtime template parsing.
1111

1212
## Project structure
1313

@@ -17,7 +17,7 @@ Every example app follows this layout:
1717
examples/app/<name>/
1818
├── src/
1919
│ ├── index.html # HTML shell + CSS design tokens
20-
│ ├── index.ts # Hydration bootstrap
20+
│ ├── index.ts # Component registration
2121
│ └── <component-name>/ # One directory per component
2222
│ ├── <component-name>.ts
2323
│ ├── <component-name>.html
@@ -35,24 +35,22 @@ examples/app/<name>/
3535
"type": "module",
3636
"scripts": {
3737
"start:client": "esbuild src/index.ts --bundle --outfile=dist/index.js --format=esm --sourcemap --watch",
38-
"start:server": "cargo run -p microsoft-webui-cli -- start ./src --state ./data/state.json --plugin=fast --servedir ./dist --port 3001 --watch",
38+
"start:server": "cargo run -p microsoft-webui-cli -- start ./src --state ./data/state.json --plugin=webui --servedir ./dist --port 3001 --watch",
3939
"start": "cargo xtask dev <name>"
4040
},
4141
"devDependencies": {
42-
"@microsoft/fast-element": "catalog:",
43-
"@microsoft/fast-html": "catalog:",
42+
"@microsoft/webui-framework": "workspace:*",
4443
"esbuild": "catalog:",
45-
"tslib": "catalog:",
4644
"typescript": "catalog:"
4745
}
4846
}
4947
```
5048

51-
Use `catalog:` for all dependency versions — they resolve from `pnpm-workspace.yaml`.
49+
Use `catalog:` for dependency versions — they resolve from `pnpm-workspace.yaml`.
5250

5351
### tsconfig.json
5452

55-
Required settings for FAST-HTML:
53+
Required settings:
5654

5755
```json
5856
{
@@ -63,7 +61,7 @@ Required settings for FAST-HTML:
6361
}
6462
```
6563

66-
`useDefineForClassFields: false` is **mandatory**FAST decorators rely on legacy class field behavior.
64+
`useDefineForClassFields: false` is **mandatory** — decorators rely on legacy class field behavior.
6765

6866
## Template syntax (HTML)
6967

@@ -148,123 +146,82 @@ For structured data that passes through as-is:
148146
<my-component :config={{settings}}></my-component>
149147
```
150148

151-
## FAST-HTML component patterns
149+
## Component patterns
152150

153151
### Component class
154152

155-
Every component extends `RenderableFASTElement(FASTElement)`:
153+
Every component extends `WebUIElement`:
156154

157155
```typescript
158-
import { FASTElement, attr, observable } from '@microsoft/fast-element';
159-
import { RenderableFASTElement } from '@microsoft/fast-html';
156+
import { WebUIElement, attr, observable, volatile } from '@microsoft/webui-framework';
160157

161-
export class MyComponent extends RenderableFASTElement(FASTElement) {
162-
@attr name!: string; // HTML attribute (string, reflected)
163-
@observable items!: ItemData[]; // Reactive property (triggers re-render)
158+
export class MyComponent extends WebUIElement {
159+
@attr label = 'Default'; // HTML attribute (string, reflected)
160+
@observable count = 0; // Reactive property
161+
@observable items: Item[] = []; // Collection for @for loops
164162

165-
inputRef!: HTMLInputElement; // Template ref target (via f-ref)
166-
167-
async prepare(): Promise<void> {
168-
// Hydration hook — read state from pre-rendered DOM
163+
@volatile get doubled(): number { // Computed, re-evaluated on access
164+
return this.count * 2;
169165
}
170166

171-
connectedCallback(): void {
172-
super.connectedCallback();
173-
// Post-hydration setup (event listeners, focus, etc.)
167+
increment(): void {
168+
this.count += 1;
174169
}
175170
}
176171

177-
MyComponent.defineAsync({
178-
name: 'my-component',
179-
templateOptions: 'defer-and-hydrate',
180-
});
172+
MyComponent.define('my-component');
181173
```
182174

183175
### `@attr` — HTML attributes
184176

185177
- Reflected to/from the HTML attribute on the element.
186-
- **Use `!:` (no initializer)** for fields set in `prepare()`. Class field initializers run AFTER `super()`, overwriting values set during hydration.
178+
- **Use `!:` (no initializer)** for fields seeded from SSR. Class field initializers run AFTER `super()`, overwriting values set during hydration.
187179
- Hyphenated HTML attributes map via `@attr({ attribute: 'display-value' })`.
180+
- Attribute values arrive as strings — use `@observable` for non-string state.
188181

189182
### `@observable` — Reactive properties
190183

191-
- Triggers FAST template re-rendering when changed.
192-
- Use for data that drives `<for>` loops or conditional display.
193-
- **Use `!:` (no initializer)** for fields set in `prepare()`.
184+
- Triggers per-path targeted update when changed — only bindings referencing this property are visited.
185+
- Use for data that drives `<for>` loops, `<if>` conditionals, or text/attribute bindings.
186+
- **Use `!:` (no initializer)** for fields seeded from SSR.
187+
188+
### `@volatile` — Computed getters
194189

195-
### `prepare()` — Hydration hook
190+
- Re-evaluated on every access (no caching).
191+
- Automatically included in targeted updates via a wildcard binding.
196192

197-
Called during hydration to initialize component state from the pre-rendered DOM. This is **the** place to read server-rendered data:
193+
### `$emit` — Custom events
198194

199195
```typescript
200-
async prepare(): Promise<void> {
201-
// Read @attr values from HTML attributes (initializers haven't run yet)
202-
this.mode = this.getAttribute('mode') || 'default';
203-
204-
// Read child elements rendered by SSR
205-
const items: ItemData[] = [];
206-
for (const el of this.shadowRoot!.querySelectorAll('my-item')) {
207-
items.push({
208-
id: el.getAttribute('id') || '',
209-
title: el.getAttribute('title') || '',
210-
});
211-
}
212-
this.items = items;
213-
}
196+
this.$emit('toggle-item', { id: this.id });
214197
```
215198

216-
Rules:
217-
218-
- Always read `@attr` values from `this.getAttribute()` — decorators initialize AFTER `prepare()`.
219-
- Read child element data from `this.shadowRoot.querySelectorAll()`.
220-
- Guard with `if (!this.shadowRoot) return;` when appropriate.
221-
- Avoid `<for>` loops over `@observable` string arrays — use object arrays or static HTML.
199+
Events bubble through the shadow DOM boundary via `composed: true`.
222200

223-
### Component template (HTML)
201+
### `w-ref` — Template refs
224202

225-
The `<template shadowrootmode="open">` wrapper is **optional** — the framework adds it automatically when absent. Only include it when you need to attach event listeners or other directives to the template root:
203+
Bind a DOM element to a component property for direct access:
226204

227205
```html
228-
<!-- Minimal — no root-level bindings needed -->
229-
<div>{{title}}</div>
230-
<for each="item in items">
231-
<child-component name="{{item.name}}"></child-component>
232-
</for>
233-
234-
<!-- With root-level event listeners — wrapper required -->
235-
<template shadowrootmode="open"
236-
@custom-event="{onCustomEvent(e)}"
237-
>
238-
<div>{{title}}</div>
239-
</template>
206+
<input w-ref="myInput" @keydown="{onKeydown(e)}" />
240207
```
241208

242-
### Component styles (CSS)
243-
244-
Scoped to the component via shadow DOM:
245-
246-
```css
247-
:host {
248-
display: block;
249-
}
250-
251-
:host([state="active"]) .indicator {
252-
background: var(--color-accent);
253-
}
209+
```typescript
210+
myInput!: HTMLInputElement; // Populated during hydration
254211

255-
.content {
256-
padding: var(--spacing-m);
212+
onSubmit(): void {
213+
const value = this.myInput.value;
214+
this.myInput.value = '';
215+
this.myInput.focus();
257216
}
258217
```
259218

260-
Use `:host([attr="value"])` selectors to style based on attribute state.
219+
Use refs only for DOM-only concerns (focus, measurement, selection). Application state belongs in `@observable` / `@attr`.
261220

262221
## Event handling
263222

264223
### Template event binding — `@event`
265224

266-
Bind DOM and custom events to component methods in the template:
267-
268225
```html
269226
<!-- Standard DOM events -->
270227
<button @click="{onClick()}">Click</button>
@@ -279,66 +236,69 @@ Bind DOM and custom events to component methods in the template:
279236

280237
Pass `e` to receive the event object. Omit it when not needed.
281238

282-
### Emitting custom events`$emit`
239+
Events use delegation internallyone listener per event type on the shadow root, not one closure per element.
283240

284-
Child components emit events to parent components:
241+
## Component template (HTML)
285242

286-
```typescript
287-
// Emit a custom event with detail data
288-
this.$emit('toggle-item', { id: this.id });
243+
The `<template shadowrootmode="open">` wrapper is **optional** — the framework adds it automatically when absent. Only include it when you need root-level event listeners:
289244

290-
// Parent catches it via @toggle-item="{onToggleItem(e)}" on its <template>
291-
```
245+
```html
246+
<!-- Minimal — no root-level bindings needed -->
247+
<div>{{title}}</div>
248+
<for each="item in items">
249+
<child-component name="{{item.name}}"></child-component>
250+
</for>
292251

293-
Events bubble through the shadow DOM boundary via `composed: true`.
252+
<!-- With root-level event listeners — wrapper required -->
253+
<template shadowrootmode="open"
254+
@custom-event="{onCustomEvent(e)}"
255+
>
256+
<div>{{title}}</div>
257+
</template>
258+
```
294259

295-
### Template refs — `f-ref`
260+
## Component styles (CSS)
296261

297-
Bind a DOM element to a component property for direct access:
262+
Scoped to the component via shadow DOM:
298263

299-
```html
300-
<input f-ref="{myInput}" @keydown="{onKeydown(e)}" />
301-
```
264+
```css
265+
:host {
266+
display: block;
267+
}
302268

303-
```typescript
304-
myInput!: HTMLInputElement; // Populated after hydration
269+
:host([state="active"]) .indicator {
270+
background: var(--color-accent);
271+
}
305272

306-
onSubmit(): void {
307-
const value = this.myInput.value;
308-
this.myInput.value = '';
309-
this.myInput.focus();
273+
.content {
274+
padding: var(--spacing-m);
310275
}
311276
```
312277

313-
## Hydration bootstrap (index.ts)
278+
Use `:host([attr="value"])` selectors to style based on attribute state.
314279

315-
The entry point registers components and activates hydration:
280+
## SSR hydration
316281

317-
```typescript
318-
performance.mark('app-hydration-started');
282+
The framework handles two paths automatically:
319283

320-
import { TemplateElement } from '@microsoft/fast-html';
284+
**SSR path** — the server renders a Declarative Shadow Root with hydration markers. On custom element upgrade, the framework:
285+
1. Walks SSR markers once to connect bindings
286+
2. Seeds `@observable` / `@attr` values from DOM content inline
287+
3. Removes markers
288+
4. DOM is already correct — no `$update()` needed
321289

322-
// Side-effect imports register components
323-
import './my-app/my-app.js';
324-
import './my-item/my-item.js';
290+
**Client-created path** — for components created dynamically (e.g. inside `@for` loops):
291+
1. Clones cached template HTML (`cloneNode`, not `innerHTML`)
292+
2. Resolves binding locators from compiled metadata
293+
3. Calls `$update()` to flush initial state
325294

326-
TemplateElement.options({
327-
'my-app': { observerMap: 'all' },
328-
'my-item': { observerMap: 'all' },
329-
}).config({
330-
hydrationComplete() {
331-
performance.measure('app-hydration-completed', 'app-hydration-started');
332-
console.log('Hydration complete!');
333-
},
334-
}).define({
335-
name: 'f-template',
336-
});
337-
```
295+
### State seeding
296+
297+
The framework automatically reconstructs `@observable` state from SSR DOM:
298+
- `@observable count = 0` + SSR text `"42"``this.count = 42` (coerced to number)
299+
- `@observable active = false` + SSR attr `"true"``this.active = true` (coerced to boolean)
338300

339-
- List **every** component in `.options()` with `{ observerMap: 'all' }`.
340-
- `.define({ name: 'f-template' })` triggers hydration.
341-
- Performance marks measure hydration time.
301+
Do NOT manually read DOM values in `connectedCallback` — the framework does it for you.
342302

343303
## State (data/state.json)
344304

@@ -355,7 +315,7 @@ Provides initial values for SSR template rendering:
355315
}
356316
```
357317

358-
Top-level keys become template signals (`{{title}}`). Arrays drive `<for>` loops. The state is passed to the WebUI CLI via `--state`.
318+
Top-level keys become template signals (`{{title}}`). Arrays drive `<for>` loops.
359319

360320
## CSS design tokens
361321

@@ -369,12 +329,10 @@ Define design tokens as CSS custom properties in `index.html`:
369329
--border-radius-m: 6px;
370330
--font-family-base: 'Segoe UI', sans-serif;
371331
}
372-
* { box-sizing: border-box; margin: 0; padding: 0; }
373-
body { font-family: var(--font-family-base); }
374332
</style>
375333
```
376334

377-
Components reference tokens via `var(--token-name)`. WebUI hoists `var()` usages at build time into the protocol for host-language token resolution.
335+
Components reference tokens via `var(--token-name)`.
378336

379337
## Dev workflow
380338

@@ -387,7 +345,7 @@ pnpm start:client # esbuild watch
387345
pnpm start:server # microsoft-webui-cli dev server
388346

389347
# Production build
390-
webui build ./src --out ./dist --plugin=fast
348+
webui build ./src --out ./dist --plugin=webui
391349
```
392350

393351
The `cargo xtask dev` command auto-discovers apps from `examples/app/` — no registration needed.

.github/workflows/pr.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,19 @@ jobs:
120120
uses: actions/upload-artifact@v4
121121
with:
122122
name: e2e-updated-baselines
123-
path: examples/app/*/tests/*.spec.ts-snapshots/
123+
path: |
124+
examples/app/*/tests/*.spec.ts-snapshots/
125+
packages/*/tests/*.spec.ts-snapshots/
124126
retention-days: 7
125127

126128
- name: Upload test results (on failure)
127129
if: failure()
128130
uses: actions/upload-artifact@v4
129131
with:
130132
name: e2e-test-results
131-
path: examples/app/*/test-results/
133+
path: |
134+
examples/app/*/test-results/
135+
packages/*/test-results/
132136
retention-days: 7
133137

134138
# ── Phase 2: WASM (Ubuntu) ─────────────────────────────────────────

0 commit comments

Comments
 (0)