@microsoft/webui-framework 0.0.29 → 3.0.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.
package/README.md CHANGED
@@ -12,7 +12,7 @@ This package is the browser-side runtime used by `webui build --plugin=webui`. I
12
12
 
13
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
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. For framework internals (hydration, path resolution, reactive update model), see [RENDERING.md](./RENDERING.md).
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. For framework internals (hydration, path resolution, reactive update model), see [the hydration contract](https://github.com/microsoft/webui/blob/main/specs/hydration.md).
16
16
 
17
17
  ## Install
18
18
 
@@ -328,7 +328,7 @@ update that DOM, so the write is dropped and the runtime logs a
328
328
  `[WebUI] Hydration mismatch` warning naming the properties. Seed such values in
329
329
  the SSR state, or assign them from `hydratedCallback()`. The warning is
330
330
  development-only and is dead-code-eliminated from production bundles via the
331
- `__WEBUI_DEV__` compile-time flag (on by default; `webui-press build` sets it to
331
+ `__WEBUI_DEV__` compile-time flag (on by default; `webui press build` sets it to
332
332
  `false`). See the
333
333
  [Interactivity Guide](https://microsoft.github.io/webui/guide/concepts/interactivity#setting-observable-state-during-setup).
334
334
 
@@ -396,36 +396,40 @@ In most components you do not call `$update()` directly. Property changes throug
396
396
 
397
397
  ### Static component assets
398
398
 
399
- `webui build --plugin=webui --emit-component-assets settings-dialog` emits
400
- `settings-dialog.webui.js` next to `protocol.bin`. Load the ESM asset before
401
- creating the component when you are not using `@microsoft/webui-router`:
399
+ Generate stable ESM inputs separately from deployable output:
402
400
 
403
- ```ts
404
- import { settingsAssets } from './lazy-assets.js';
405
-
406
- settingsAssets.preload('settings-dialog');
407
- panelSlot.replaceChildren(await settingsAssets.create('settings-dialog'));
401
+ ```bash
402
+ webui build ./src --out ./dist --plugin=webui \
403
+ --emit-component-assets settings-dialog \
404
+ --component-assets-out ./.webui
408
405
  ```
409
406
 
410
- ```ts
411
- // lazy-assets.ts
412
- import { defineComponentAssets } from '@microsoft/webui-framework/component-asset.js';
413
-
414
- export const settingsAssets = defineComponentAssets({
415
- 'settings-dialog': {
416
- asset: '/settings-dialog.webui.js',
417
- module: () => import('./settings-dialog/settings-dialog.js'),
418
- data: async () => await (await fetch('/settings-dialog-data.json')).json(),
419
- },
420
- });
407
+ ```typescript
408
+ import {
409
+ defineComponentAsset,
410
+ preloadComponentAssetStyles,
411
+ } from '@microsoft/webui-framework/component-asset-runtime.js';
412
+
413
+ const loadSettings = () => {
414
+ preloadComponentAssetStyles('settings-dialog');
415
+ return import('../../.webui/settings-dialog.webui.js')
416
+ .then(module => defineComponentAsset(module.default));
417
+ };
418
+
419
+ void loadSettings().then(asset => asset.preload()).catch(() => {});
420
+ panelSlot.replaceChildren(await (await loadSettings()).create());
421
421
  ```
422
422
 
423
- The asset graph keeps entry-owned templates external, leaves single-root
424
- dependencies inline, and emits dependencies shared by multiple roots once as
425
- flat dynamic chunks. Component assets cannot be combined with `<route>`. Load
426
- the normal entry bundle first so external prerequisites are registered.
427
- Current assets require version 3 and an atomically validated
428
- `componentStyles` catalog; any other version is rejected as unsupported.
423
+ WebUI emits one module per asset-owned component and a thin root entry with
424
+ static imports for its closure. The application bundler owns final splitting,
425
+ sharing, hashes, public paths, and caching. Entry-owned templates remain
426
+ external prerequisites, so load the normal application entry first. Component
427
+ assets cannot be combined with `<route>`. Current generated assets use version
428
+ 4 on the root. Imported component payloads contain template and style data,
429
+ not repeated root manifests. Compiler-proven coverage is checked at build time;
430
+ registration checks external entry templates and live style conflicts before
431
+ mutating the registries. The manifest-driven loader additionally validates
432
+ untrusted graphs.
429
433
 
430
434
  The compiler records final Link stylesheet filenames in the protocol. For
431
435
  Shadow builds, the handler emits that finite manifest as inert JSON in the
@@ -437,25 +441,23 @@ WebUI handler or `Protocol`, which emits `#webui-component-assets`. A shell that
437
441
  uses build artifacts without rendering the protocol still mounts safely through
438
442
  the native stylesheet guard, but it does not receive the earlier
439
443
  compiler-owned style preload.
440
- Shared chunk and content-hashed stylesheet filenames are generated and must not
441
- be copied into authored code. Each root asset carries its own dynamic imports;
442
- `--metafile` remains available for analysis and build tooling.
443
-
444
- In Shadow builds, `preload(tag)` reads the compiler-owned style metadata and
445
- starts Link styles beside the authored root asset, component module, and
446
- optional data request. Only the stable root asset URL remains in application
447
- code; shared chunks and content-hashed CSS stay compiler-owned.
448
-
449
- Bundler-generated loaders can use `asset: () => import('./settings-dialog.webui.js')`
450
- instead of a URL. This keeps chunk naming and public-path rewriting inside the
451
- bundler while preserving the same `preload(tag)` and `create(tag)` lifecycle.
452
- Concurrent roots share in-flight chunk and stylesheet work. `create(tag)`
453
- creates the element after template/module work is ready and does not block on
454
- optional data by default. Use
455
- `create(tag, { awaitData: true, dataTimeoutMs: 150 })` only when a component
456
- must wait briefly for state before mounting. A rejected root asset or authored
457
- module is evicted from the registry so a later `preload(tag)` or `create(tag)`
458
- retries it.
444
+ Only stable generated root input paths belong in authored imports. Final
445
+ JavaScript filenames are application-bundler outputs, while content-hashed
446
+ stylesheet filenames remain compiler-owned. `--metafile` exposes the static
447
+ input graph for analysis and build tooling.
448
+
449
+ Generated roots export semantic payloads only. Application-owned code loads
450
+ component behavior and data directly, so it also owns caching and recovery policy.
451
+ The runtime returns one facade per imported payload. Native ESM failures may stay
452
+ cached for the document lifetime: resetting an application promise does not
453
+ guarantee a new network request. Handle real open failures with visible errors
454
+ and bundler-owned recovery or an explicit page reload. Consume speculative
455
+ preload rejections without automatically reloading.
456
+ In Shadow builds, applications can import the small runtime eagerly, call
457
+ `preloadComponentAssetStyles(root)` before the dynamic import, and wrap the
458
+ default generated payload with `defineComponentAsset()`. This keeps templates
459
+ lazy while starting CSS and JavaScript in parallel. Concurrent roots share
460
+ in-flight registration and stylesheet work.
459
461
 
460
462
  ### `@observable`
461
463
 
@@ -1,24 +1,18 @@
1
1
  import type { CompiledConditionFn, TemplateMeta } from '../template.js';
2
2
  import { type ComponentStyles } from '../element/styles.js';
3
- export interface ComponentAssetImport {
4
- components: string[];
5
- href: string;
6
- load: () => Promise<unknown>;
3
+ export interface ComponentAssetPayload {
4
+ componentStyles: ComponentStyles;
5
+ templates: Record<string, TemplateMeta>;
6
+ templateFunctions?: Record<string, CompiledConditionFn[]>;
7
7
  }
8
8
  export interface ComponentAsset {
9
9
  type: 'webui-component-asset';
10
- version: 3;
11
- kind: 'root' | 'chunk';
12
- root?: string;
13
- components: string[];
14
- requiredComponents: string[];
10
+ version: 4;
11
+ root: string;
15
12
  externalComponents: string[];
16
- imports: ComponentAssetImport[];
13
+ imports: ComponentAssetPayload[];
17
14
  componentStyles: ComponentStyles;
18
- templates: Record<string, TemplateMeta>;
19
- templateFunctions?: Record<string, CompiledConditionFn[]>;
20
15
  }
21
16
  export declare function prepareAssetComponentStyles(value: unknown): ComponentStyles;
22
17
  export declare function readComponentAssetModule(module: unknown): unknown;
23
- export declare function validateAsset(value: unknown, expectedKind: ComponentAsset['kind']): asserts value is ComponentAsset;
24
- export declare function sameComponents(left: readonly string[], right: readonly string[]): boolean;
18
+ export declare function validateAsset(value: unknown): asserts value is ComponentAsset;
@@ -1,9 +1,7 @@
1
1
  import { requireComponentStyles, } from '../element/styles.js';
2
- const ASSET_TYPE = 'webui-component-asset';
3
- const COMPONENT_STYLES_ASSET_VERSION = 3;
4
2
  const preparedAssetStyles = new WeakMap();
5
3
  export function prepareAssetComponentStyles(value) {
6
- if (typeof value === 'object' && value !== null) {
4
+ if (isObject(value)) {
7
5
  const cached = preparedAssetStyles.get(value);
8
6
  if (cached)
9
7
  return cached;
@@ -19,169 +17,74 @@ export function readComponentAssetModule(module) {
19
17
  }
20
18
  return module.default;
21
19
  }
22
- export function validateAsset(value, expectedKind) {
20
+ export function validateAsset(value) {
23
21
  if (!isObject(value)) {
24
22
  throw new Error('[WebUI] Component asset default export must be an object.');
25
23
  }
26
- const asset = value;
27
- if (asset.type !== ASSET_TYPE) {
28
- throw new Error(`[WebUI] Invalid component asset type: ${String(asset.type)}`);
24
+ if (value.type !== 'webui-component-asset') {
25
+ throw new Error(`[WebUI] Invalid component asset type: ${String(value.type)}`);
29
26
  }
30
- if (asset.version !== COMPONENT_STYLES_ASSET_VERSION) {
31
- throw new Error(`[WebUI] Unsupported component asset version: ${String(asset.version)}`);
27
+ if (value.version !== 4) {
28
+ throw new Error(`[WebUI] Unsupported component asset version: ${String(value.version)}`);
32
29
  }
33
- if (asset.componentStyles === undefined) {
34
- throw new Error('[WebUI] Version 3 component assets require componentStyles.');
30
+ if (typeof value.root !== 'string' || value.root.length === 0) {
31
+ throw new Error('[WebUI] Component asset root must name its root component.');
35
32
  }
36
- if (asset.kind !== expectedKind) {
37
- throw new Error(`[WebUI] Expected component asset kind "${expectedKind}", received "${String(asset.kind)}".`);
33
+ if (value.componentStyles === undefined) {
34
+ throw new Error('[WebUI] Version 4 component assets require componentStyles.');
38
35
  }
39
- validateUniqueStringArray(asset.components, 'components');
40
- validateUniqueStringArray(asset.requiredComponents, 'requiredComponents');
41
- validateUniqueStringArray(asset.externalComponents, 'externalComponents');
42
- if (!Array.isArray(asset.imports)) {
36
+ prepareAssetComponentStyles(value.componentStyles);
37
+ validateExternalComponents(value.externalComponents);
38
+ if (!Array.isArray(value.imports)) {
43
39
  throw new Error('[WebUI] Component asset imports must be an array.');
44
40
  }
45
- prepareAssetComponentStyles(asset.componentStyles);
46
- if (!isObject(asset.templates)) {
47
- throw new Error('[WebUI] Component asset templates must be an object.');
48
- }
49
- let root;
50
- if (expectedKind === 'root') {
51
- if (typeof asset.root !== 'string' || asset.root.length === 0) {
52
- throw new Error('[WebUI] Component asset root must name its root component.');
41
+ const providers = new Set(value.externalComponents);
42
+ for (let i = 0; i < value.imports.length; i++) {
43
+ const tag = validatePayload(value.imports[i]);
44
+ if (providers.has(tag)) {
45
+ throw new Error(`[WebUI] Component asset assigns template <${tag}> to more than one import or external prerequisite.`);
53
46
  }
54
- root = asset.root;
55
- }
56
- else {
57
- validateChunkShape(asset.root, asset.imports, asset.externalComponents);
47
+ providers.add(tag);
58
48
  }
59
- for (let i = 0; i < asset.imports.length; i++) {
60
- validateAssetImport(asset.imports[i]);
61
- }
62
- validateAssetPayload(asset.components, asset.templates, asset.templateFunctions);
63
- validateAssetCoverage(root, asset.components, asset.requiredComponents, asset.externalComponents, asset.imports);
64
- }
65
- export function sameComponents(left, right) {
66
- if (left.length !== right.length)
67
- return false;
68
- for (let i = 0; i < left.length; i++) {
69
- if (left[i] !== right[i])
70
- return false;
49
+ if (!providers.has(value.root)) {
50
+ throw new Error(`[WebUI] Component asset root <${value.root}> has no imported payload or external prerequisite.`);
71
51
  }
72
- return true;
73
52
  }
74
53
  function isObject(value) {
75
54
  return typeof value === 'object' && value !== null && !Array.isArray(value);
76
55
  }
77
- function validateChunkShape(root, imports, externalComponents) {
78
- if (root !== undefined) {
79
- throw new Error('[WebUI] Shared component asset chunks cannot declare a root.');
80
- }
81
- if (imports.length !== 0) {
82
- throw new Error('[WebUI] Shared component asset chunks cannot import other chunks.');
83
- }
84
- if (externalComponents.length !== 0) {
85
- throw new Error('[WebUI] Shared component asset chunks cannot declare external components.');
86
- }
87
- }
88
- function validateAssetImport(value) {
89
- if (!isObject(value)) {
90
- throw new Error('[WebUI] Component asset import must be an object.');
91
- }
92
- const assetImport = value;
93
- validateUniqueStringArray(assetImport.components, 'import components');
94
- if (assetImport.components.length === 0) {
95
- throw new Error('[WebUI] Component asset imports must provide at least one component.');
96
- }
97
- if (typeof assetImport.href !== 'string' || assetImport.href.length === 0) {
98
- throw new Error('[WebUI] Component asset import href must be a non-empty string.');
99
- }
100
- if (typeof assetImport.load !== 'function') {
101
- throw new Error('[WebUI] Component asset import load must be a function.');
102
- }
103
- }
104
- function validateUniqueStringArray(value, field) {
105
- validateStringArray(value, field);
106
- const seen = new Set();
107
- for (let i = 0; i < value.length; i++) {
108
- if (seen.has(value[i])) {
109
- throw new Error(`[WebUI] Component asset ${field} cannot contain duplicate <${value[i]}> entries.`);
110
- }
111
- seen.add(value[i]);
112
- }
113
- }
114
- function validateStringArray(value, field) {
56
+ function validateExternalComponents(value) {
115
57
  if (!Array.isArray(value)) {
116
- throw new Error(`[WebUI] Component asset ${field} must be an array.`);
58
+ throw new Error('[WebUI] Component asset externalComponents must be an array.');
117
59
  }
60
+ const seen = new Set();
118
61
  for (let i = 0; i < value.length; i++) {
119
- if (typeof value[i] !== 'string' || value[i].length === 0) {
120
- throw new Error(`[WebUI] Component asset ${field} must contain non-empty strings.`);
121
- }
122
- }
123
- }
124
- function validateAssetCoverage(root, components, requiredComponents, externalComponents, imports) {
125
- const required = new Set(requiredComponents);
126
- if (root && !required.has(root)) {
127
- throw new Error(`[WebUI] Component asset root <${root}> must include itself in requiredComponents.`);
128
- }
129
- const providers = new Set();
130
- const addProvider = (component) => {
131
- if (!required.has(component)) {
132
- throw new Error(`[WebUI] Component asset provides undeclared template <${component}>. Add it to requiredComponents.`);
133
- }
134
- if (providers.has(component)) {
135
- throw new Error(`[WebUI] Component asset assigns required template <${component}> to more than one payload, import, or external prerequisite.`);
136
- }
137
- providers.add(component);
138
- };
139
- for (let i = 0; i < components.length; i++)
140
- addProvider(components[i]);
141
- for (let i = 0; i < externalComponents.length; i++)
142
- addProvider(externalComponents[i]);
143
- for (let i = 0; i < imports.length; i++) {
144
- const imported = imports[i].components;
145
- for (let j = 0; j < imported.length; j++)
146
- addProvider(imported[j]);
147
- }
148
- for (let i = 0; i < requiredComponents.length; i++) {
149
- const component = requiredComponents[i];
150
- if (!providers.has(component)) {
151
- throw new Error(`[WebUI] Component asset required template <${component}> has no payload, import, or external prerequisite.`);
62
+ const tag = value[i];
63
+ if (typeof tag !== 'string' || tag.length === 0 || seen.has(tag)) {
64
+ throw new Error('[WebUI] Component asset externalComponents must contain unique non-empty strings.');
152
65
  }
66
+ seen.add(tag);
153
67
  }
154
68
  }
155
- function validateAssetPayload(components, templates, templateFunctions) {
156
- const declared = new Set(components);
157
- const templateNames = Object.keys(templates);
158
- for (let i = 0; i < templateNames.length; i++) {
159
- const template = templateNames[i];
160
- if (!declared.has(template)) {
161
- throw new Error(`[WebUI] Component asset templates contain undeclared payload <${template}>.`);
162
- }
163
- }
164
- for (let i = 0; i < components.length; i++) {
165
- const component = components[i];
166
- if (!Object.prototype.hasOwnProperty.call(templates, component)) {
167
- throw new Error(`[WebUI] Component asset payload <${component}> is missing its template metadata.`);
168
- }
169
- }
170
- if (templateFunctions === undefined)
171
- return;
172
- if (!isObject(templateFunctions)) {
173
- throw new Error('[WebUI] Component asset templateFunctions must be an object.');
174
- }
175
- const functionNames = Object.keys(templateFunctions);
176
- for (let i = 0; i < functionNames.length; i++) {
177
- const component = functionNames[i];
178
- if (!declared.has(component)) {
179
- throw new Error(`[WebUI] Component asset templateFunctions contains undeclared payload <${component}>.`);
69
+ function validatePayload(value) {
70
+ if (!isObject(value) || !isObject(value.templates)) {
71
+ throw new Error('[WebUI] Component asset payload must contain its template metadata.');
72
+ }
73
+ const tags = Object.keys(value.templates);
74
+ if (tags.length !== 1 || tags[0].length === 0) {
75
+ throw new Error('[WebUI] Component asset payload must contain exactly one template.');
76
+ }
77
+ const tag = tags[0];
78
+ prepareAssetComponentStyles(value.componentStyles);
79
+ const functions = value.templateFunctions;
80
+ if (functions !== undefined) {
81
+ if (!isObject(functions) || Object.keys(functions).some(name => name !== tag)) {
82
+ throw new Error(`[WebUI] Component asset templateFunctions contains undeclared payload for <${tag}>.`);
180
83
  }
181
- const functions = templateFunctions[component];
182
- if (!Array.isArray(functions) ||
183
- functions.some(candidate => typeof candidate !== 'function')) {
184
- throw new Error(`[WebUI] Component asset templateFunctions for <${component}> must contain only functions.`);
84
+ const closures = functions[tag];
85
+ if (!Array.isArray(closures) || closures.some(candidate => typeof candidate !== 'function')) {
86
+ throw new Error(`[WebUI] Component asset templateFunctions for <${tag}> must contain only functions.`);
185
87
  }
186
88
  }
89
+ return tag;
187
90
  }
@@ -1,2 +1,4 @@
1
+ import { type ComponentAsset } from './asset.js';
1
2
  import type { ComponentAssetSource } from './manifest.js';
2
3
  export declare function loadComponentAsset(tag: string, source: ComponentAssetSource): Promise<void>;
4
+ export declare function registerComponentAsset(asset: ComponentAsset): Promise<void>;
@@ -1,7 +1,6 @@
1
- import { getTemplate, prepareAssetTemplateData, registerTemplateData, } from '../template.js';
2
- import { prepareAssetComponentStyles, readComponentAssetModule, sameComponents, validateAsset, } from './asset.js';
3
- import { hasRegisteredComponentStyleResource, registerPreparedComponentStyles, sameComponentStyleClosure, sameComponentStyleResource, validateComponentStylesRegistration, } from '../element/styles.js';
4
- import { prepareComponentStyleLinks, prepareRegisteredLinkStyles, } from '../element/link-styles.js';
1
+ import { prepareAssetComponentStyles, readComponentAssetModule, validateAsset, } from './asset.js';
2
+ import { registerComponentAssetGraph } from './registration.js';
3
+ import { validateComponentAssetGraph } from './validate-graph.js';
5
4
  const assetModulePromises = new Map();
6
5
  export function loadComponentAsset(tag, source) {
7
6
  if (typeof source === 'function') {
@@ -16,34 +15,11 @@ export function loadComponentAsset(tag, source) {
16
15
  }
17
16
  async function registerRootAsset(expectedRoot, href, imported) {
18
17
  const asset = readComponentAssetModule(imported);
19
- validateAsset(asset, 'root');
18
+ validateAsset(asset);
20
19
  if (asset.root !== expectedRoot) {
21
20
  throw new Error(`[WebUI] Component asset manifest expected <${expectedRoot}> but ${href} exports <${String(asset.root)}>.`);
22
21
  }
23
- validateExternalComponents(asset);
24
- const root = prepareComponentPayload(asset);
25
- const chunks = await prepareAssetImports(asset.imports);
26
- const graph = [...chunks, root];
27
- validatePreparedGraph(graph);
28
- for (let i = 0; i < chunks.length; i++) {
29
- registerComponentResources(chunks[i]);
30
- }
31
- registerComponentResources(root);
32
- let pendingStyles;
33
- for (let i = 0; i < chunks.length; i++) {
34
- const ready = chunks[i].linkStyles;
35
- if (!ready)
36
- continue;
37
- (pendingStyles ??= []).push(ready);
38
- }
39
- if (root.linkStyles)
40
- (pendingStyles ??= []).push(root.linkStyles);
41
- if (pendingStyles)
42
- await Promise.all(pendingStyles);
43
- for (let i = 0; i < chunks.length; i++) {
44
- registerComponentTemplates(chunks[i]);
45
- }
46
- registerComponentTemplates(root);
22
+ await registerComponentAssetGraph(asset, prepareAssetComponentStyles, validateComponentAssetGraph);
47
23
  }
48
24
  function loadAssetModule(href, load) {
49
25
  let promise = assetModulePromises.get(href);
@@ -57,124 +33,6 @@ function loadAssetModule(href, load) {
57
33
  assetModulePromises.set(href, promise);
58
34
  return promise;
59
35
  }
60
- async function prepareAssetImports(imports) {
61
- const pending = [];
62
- for (let i = 0; i < imports.length; i++) {
63
- const assetImport = imports[i];
64
- if (componentsAlreadyRegistered(assetImport.components))
65
- continue;
66
- pending.push(importAndPrepareChunk(assetImport));
67
- }
68
- return Promise.all(pending);
69
- }
70
- function importAndPrepareChunk(assetImport) {
71
- const href = new URL(assetImport.href, document.baseURI).href;
72
- return loadAssetModule(href, assetImport.load)
73
- .then(imported => {
74
- const chunk = readComponentAssetModule(imported);
75
- validateAsset(chunk, 'chunk');
76
- if (!sameComponents(chunk.components, assetImport.components)) {
77
- throw new Error(`[WebUI] Shared component asset ${href} does not provide the components declared by its root import.`);
78
- }
79
- return prepareComponentPayload(chunk);
80
- });
81
- }
82
- function prepareComponentPayload(asset) {
83
- const componentStyles = prepareAssetComponentStyles(asset.componentStyles);
84
- prepareAssetTemplateData(asset.templates, asset.templateFunctions);
85
- const templateLinkStyles = prepareRegisteredLinkStyles(asset.templates);
86
- const componentStyleLinks = prepareComponentStyleLinks(componentStyles);
87
- let linkStyles = templateLinkStyles;
88
- if (templateLinkStyles && componentStyleLinks) {
89
- linkStyles = Promise.all([templateLinkStyles, componentStyleLinks]).then(() => { });
90
- }
91
- else {
92
- linkStyles = templateLinkStyles ?? componentStyleLinks;
93
- }
94
- return {
95
- asset,
96
- componentStyles,
97
- linkStyles,
98
- };
99
- }
100
- function registerComponentResources(prepared) {
101
- registerPreparedComponentStyles(prepared.componentStyles);
102
- }
103
- function registerComponentTemplates(prepared) {
104
- const { asset } = prepared;
105
- if (componentsAlreadyRegistered(asset.components))
106
- return;
107
- registerTemplateData(asset.templates, asset.templateFunctions);
108
- }
109
- function validatePreparedGraph(graph) {
110
- const resources = new Map();
111
- const closures = new Map();
112
- const provided = new Set();
113
- for (let i = 0; i < graph.length; i++) {
114
- const prepared = graph[i];
115
- validateComponentStylesRegistration(prepared.componentStyles);
116
- for (const component of prepared.asset.components)
117
- provided.add(component);
118
- const styles = prepared.componentStyles;
119
- for (const id of Object.keys(styles.resources)) {
120
- const resource = styles.resources[id];
121
- const current = resources.get(id);
122
- if (current && !sameComponentStyleResource(current, resource)) {
123
- throw new Error(`[WebUI] Conflicting component style resource "${id}".`);
124
- }
125
- resources.set(id, resource);
126
- }
127
- for (const root of Object.keys(styles.closures)) {
128
- const closure = styles.closures[root];
129
- const current = closures.get(root);
130
- if (current && !sameComponentStyleClosure(current, closure)) {
131
- throw new Error(`[WebUI] Conflicting component style closure "${root}".`);
132
- }
133
- closures.set(root, closure);
134
- }
135
- }
136
- for (let i = 0; i < graph.length; i++) {
137
- const styles = graph[i].componentStyles;
138
- for (const root of Object.keys(styles.closures)) {
139
- for (const id of styles.closures[root]) {
140
- if (!resources.has(id) &&
141
- !hasRegisteredComponentStyleResource(id)) {
142
- throw new Error(`[WebUI] Component style closure "${root}" references missing resource "${id}".`);
143
- }
144
- }
145
- }
146
- }
147
- const missing = [];
148
- for (let i = 0; i < graph.length; i++) {
149
- const asset = graph[i].asset;
150
- for (const required of asset.requiredComponents) {
151
- if (provided.has(required) || getTemplate(required))
152
- continue;
153
- if (missing.indexOf(required) < 0)
154
- missing.push(required);
155
- }
156
- }
157
- if (missing.length === 0)
158
- return;
159
- throw new Error(`[WebUI] Component asset is missing required templ${missing.length === 1 ? 'ate' : 'ates'} ${missing.map(tag => `<${tag}>`).join(', ')}. Load the application entry bundle and protocol before deferred component assets.`);
160
- }
161
- function componentsAlreadyRegistered(components) {
162
- if (components.length === 0)
163
- return false;
164
- for (let i = 0; i < components.length; i++) {
165
- if (!getTemplate(components[i]))
166
- return false;
167
- }
168
- return true;
169
- }
170
- function validateExternalComponents(asset) {
171
- const missing = [];
172
- for (let i = 0; i < asset.externalComponents.length; i++) {
173
- const component = asset.externalComponents[i];
174
- if (!getTemplate(component))
175
- missing.push(component);
176
- }
177
- if (missing.length === 0)
178
- return;
179
- throw new Error(`[WebUI] Component asset requires entr${missing.length === 1 ? 'y template' : 'y templates'} ${missing.map(tag => `<${tag}>`).join(', ')}. Load the application entry bundle and protocol before deferred component assets.`);
36
+ export function registerComponentAsset(asset) {
37
+ return registerRootAsset(typeof asset.root === 'string' ? asset.root : '', 'generated component asset', { default: asset });
180
38
  }
@@ -0,0 +1,11 @@
1
+ import { type ComponentStyles } from '../element/styles.js';
2
+ import type { ComponentAsset, ComponentAssetPayload } from './asset.js';
3
+ export interface PreparedComponentAsset {
4
+ tag: string;
5
+ payload?: ComponentAssetPayload;
6
+ componentStyles: ComponentStyles;
7
+ }
8
+ type PrepareStyles = (value: unknown) => ComponentStyles;
9
+ export type ValidateComponentAssetGraph = (graph: readonly PreparedComponentAsset[]) => void;
10
+ export declare function registerComponentAssetGraph(asset: ComponentAsset, prepareStyles?: PrepareStyles, validateGraph?: ValidateComponentAssetGraph): Promise<void>;
11
+ export {};
@@ -0,0 +1,63 @@
1
+ import { getTemplate, prepareAssetTemplateData, registerTemplateData, } from '../template.js';
2
+ import { registerPreparedComponentStyles, validateComponentStylesRegistration, } from '../element/styles.js';
3
+ import { prepareComponentStyleLinks, prepareRegisteredLinkStyles, } from '../element/link-styles.js';
4
+ export async function registerComponentAssetGraph(asset, prepareStyles = trustedComponentStyles, validateGraph) {
5
+ const graph = [];
6
+ for (let i = 0; i < asset.imports.length; i++) {
7
+ const payload = asset.imports[i];
8
+ const styles = prepareStyles(payload.componentStyles);
9
+ validateComponentStylesRegistration(styles);
10
+ graph.push({
11
+ tag: Object.keys(payload.templates)[0],
12
+ payload,
13
+ componentStyles: styles,
14
+ });
15
+ }
16
+ const rootStyles = prepareStyles(asset.componentStyles);
17
+ validateComponentStylesRegistration(rootStyles);
18
+ graph.push({ tag: asset.root, componentStyles: rootStyles });
19
+ validateGraph?.(graph);
20
+ validateExternalTemplates(asset.externalComponents);
21
+ for (let i = 0; i < graph.length; i++) {
22
+ const prepared = graph[i];
23
+ const payload = prepared.payload;
24
+ if (payload && !getTemplate(prepared.tag)) {
25
+ prepareAssetTemplateData(payload.templates, payload.templateFunctions);
26
+ }
27
+ }
28
+ const pendingStyles = [];
29
+ for (let i = 0; i < graph.length; i++) {
30
+ const prepared = graph[i];
31
+ const payload = prepared.payload;
32
+ const templateLinks = payload && prepareRegisteredLinkStyles(payload.templates);
33
+ const componentLinks = prepareComponentStyleLinks(prepared.componentStyles);
34
+ if (templateLinks)
35
+ pendingStyles.push(templateLinks);
36
+ if (componentLinks)
37
+ pendingStyles.push(componentLinks);
38
+ }
39
+ for (let i = 0; i < graph.length; i++) {
40
+ registerPreparedComponentStyles(graph[i].componentStyles);
41
+ }
42
+ if (pendingStyles.length > 0)
43
+ await Promise.all(pendingStyles);
44
+ for (let i = 0; i < graph.length; i++) {
45
+ const payload = graph[i].payload;
46
+ if (payload && !getTemplate(graph[i].tag)) {
47
+ registerTemplateData(payload.templates, payload.templateFunctions);
48
+ }
49
+ }
50
+ }
51
+ function validateExternalTemplates(external) {
52
+ const missing = [];
53
+ for (let i = 0; i < external.length; i++) {
54
+ if (!getTemplate(external[i]))
55
+ missing.push(external[i]);
56
+ }
57
+ if (missing.length > 0) {
58
+ throw new Error(`[WebUI] Component asset requires entr${missing.length === 1 ? 'y template' : 'y templates'} ${missing.map(tag => `<${tag}>`).join(', ')}. Load the application entry bundle and protocol before deferred component assets.`);
59
+ }
60
+ }
61
+ function trustedComponentStyles(value) {
62
+ return value;
63
+ }
@@ -0,0 +1,2 @@
1
+ import type { PreparedComponentAsset } from './registration.js';
2
+ export declare function validateComponentAssetGraph(graph: readonly PreparedComponentAsset[]): void;
@@ -0,0 +1,42 @@
1
+ import { hasRegisteredComponentStyleResource, sameComponentStyleClosure, sameComponentStyleResource, } from '../element/styles.js';
2
+ export function validateComponentAssetGraph(graph) {
3
+ const resources = new Map();
4
+ const closures = new Map();
5
+ for (let i = 0; i < graph.length; i++) {
6
+ const styles = graph[i].componentStyles;
7
+ const resourceIds = Object.keys(styles.resources);
8
+ for (let j = 0; j < resourceIds.length; j++) {
9
+ const id = resourceIds[j];
10
+ const resource = styles.resources[id];
11
+ const current = resources.get(id);
12
+ if (current && !sameComponentStyleResource(current, resource)) {
13
+ throw new Error(`[WebUI] Conflicting component style resource "${id}".`);
14
+ }
15
+ resources.set(id, resource);
16
+ }
17
+ const roots = Object.keys(styles.closures);
18
+ for (let j = 0; j < roots.length; j++) {
19
+ const root = roots[j];
20
+ const closure = styles.closures[root];
21
+ const current = closures.get(root);
22
+ if (current && !sameComponentStyleClosure(current, closure)) {
23
+ throw new Error(`[WebUI] Conflicting component style closure "${root}".`);
24
+ }
25
+ closures.set(root, closure);
26
+ }
27
+ }
28
+ for (let i = 0; i < graph.length; i++) {
29
+ const styles = graph[i].componentStyles;
30
+ const roots = Object.keys(styles.closures);
31
+ for (let j = 0; j < roots.length; j++) {
32
+ const root = roots[j];
33
+ const closure = styles.closures[root];
34
+ for (let k = 0; k < closure.length; k++) {
35
+ const id = closure[k];
36
+ if (!resources.has(id) && !hasRegisteredComponentStyleResource(id)) {
37
+ throw new Error(`[WebUI] Component style closure "${root}" references missing resource "${id}".`);
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,7 @@
1
+ import type { ComponentAsset } from './component-asset/asset.js';
2
+ export interface GeneratedComponentAsset {
3
+ preload(): Promise<void>;
4
+ create(): Promise<HTMLElement>;
5
+ }
6
+ export declare function preloadComponentAssetStyles(root: string): void;
7
+ export declare function defineComponentAsset(asset: ComponentAsset): GeneratedComponentAsset;
@@ -0,0 +1,38 @@
1
+ import { registerComponentAssetGraph } from './component-asset/registration.js';
2
+ import { takeGeneratedComponentAssetStyles } from './component-asset/generated-manifest.js';
3
+ import { preloadComponentAssetStyles as preloadComponentAssetStylesInternal } from './element/link-styles.js';
4
+ const facades = new WeakMap();
5
+ export function preloadComponentAssetStyles(root) {
6
+ const styles = takeGeneratedComponentAssetStyles(root);
7
+ if (styles)
8
+ preloadComponentAssetStylesInternal(styles);
9
+ }
10
+ export function defineComponentAsset(asset) {
11
+ if (asset.type !== 'webui-component-asset' || asset.version !== 4) {
12
+ throw new Error('[WebUI] Expected a version 4 component asset root. Rebuild generated inputs with a compatible compiler.');
13
+ }
14
+ const existing = facades.get(asset);
15
+ if (existing)
16
+ return existing;
17
+ const root = asset.root;
18
+ let pending;
19
+ const preload = () => {
20
+ if (pending)
21
+ return pending;
22
+ preloadComponentAssetStyles(root);
23
+ const next = registerComponentAssetGraph(asset).catch((error) => {
24
+ if (pending === next)
25
+ pending = undefined;
26
+ throw error;
27
+ });
28
+ pending = next;
29
+ return next;
30
+ };
31
+ const create = async () => {
32
+ await preload();
33
+ return document.createElement(root);
34
+ };
35
+ const facade = { preload, create };
36
+ facades.set(asset, facade);
37
+ return facade;
38
+ }
@@ -1,4 +1,4 @@
1
1
  import type { ComponentAssetManifest, ComponentAssetRegistry } from './component-asset/manifest.js';
2
- export type { ComponentAsset, ComponentAssetImport, } from './component-asset/asset.js';
2
+ export type { ComponentAsset, ComponentAssetPayload, } from './component-asset/asset.js';
3
3
  export type { ComponentAssetCreateOptions, ComponentAssetManifest, ComponentAssetManifestEntry, ComponentAssetPreload, ComponentAssetRegistry, ComponentAssetSource, ComponentAssetState, } from './component-asset/manifest.js';
4
4
  export declare function defineComponentAssets(manifest: ComponentAssetManifest): ComponentAssetRegistry;
@@ -162,6 +162,12 @@ function createReactiveProperty(proto, name, attrDefinition) {
162
162
  return this[backingKey];
163
163
  },
164
164
  set(newValue) {
165
+ const captured = this['$preUpgradeProperties'];
166
+ if (captured?.has(name) && this.matches(':defined')) {
167
+ captured.delete(name);
168
+ if (captured.size === 0)
169
+ this['$preUpgradeProperties'] = undefined;
170
+ }
165
171
  const oldValue = this[backingKey];
166
172
  if (Object.is(oldValue, newValue)) {
167
173
  if (this['$deferredSSR'] === true) {
@@ -172,12 +178,33 @@ function createReactiveProperty(proto, name, attrDefinition) {
172
178
  return;
173
179
  }
174
180
  this[backingKey] = newValue;
175
- if (attrDefinition && this['$ready'] === true) {
176
- reflectPropertyToAttribute(this, attrDefinition, newValue);
181
+ let canReflect = false;
182
+ if (attrDefinition) {
183
+ const checkReflection = this['$canRunAuthoredEffects'];
184
+ if (typeof checkReflection === 'function' &&
185
+ checkReflection.call(this)) {
186
+ reflectPropertyToAttribute(this, attrDefinition, newValue);
187
+ canReflect = true;
188
+ }
177
189
  }
178
- const cb = this[changedKey];
179
- if (typeof cb === 'function') {
180
- cb.call(this, oldValue, newValue);
190
+ const callback = this[changedKey];
191
+ if (typeof callback === 'function') {
192
+ if (this['$authoredPhase'] === 1 &&
193
+ (attrDefinition ? canReflect : this.isConnected)) {
194
+ callback
195
+ .call(this, oldValue, newValue);
196
+ }
197
+ else {
198
+ const record = this['$recordPropertyChange'];
199
+ if (typeof record === 'function') {
200
+ record
201
+ .call(this, name, oldValue);
202
+ }
203
+ else {
204
+ callback
205
+ .call(this, oldValue, newValue);
206
+ }
207
+ }
181
208
  }
182
209
  if (this.isConnected ||
183
210
  this['$deferredSSR'] === true) {
@@ -234,9 +261,16 @@ function applyAttr(target, name, options) {
234
261
  const definition = attrDefinitionFor(this.constructor, attribute);
235
262
  if (definition !== undefined &&
236
263
  this[reflectingAttribute] !== attribute) {
237
- this[definition.property] = definition.boolean
238
- ? newVal !== null
239
- : newVal;
264
+ const captured = this['$preUpgradeProperties'];
265
+ if (captured)
266
+ this['$preUpgradeProperties'] = undefined;
267
+ try {
268
+ this[definition.property] = definition.boolean ? newVal !== null : newVal;
269
+ }
270
+ finally {
271
+ if (captured)
272
+ this['$preUpgradeProperties'] = captured;
273
+ }
240
274
  }
241
275
  if (origACB) {
242
276
  origACB.call(this, attribute, oldVal, newVal);
package/dist/element.d.ts CHANGED
@@ -4,6 +4,18 @@ import type { TemplateBlockMeta, TemplateMeta, TemplateNodeIndex } from './templ
4
4
  import type { ScopeFrame, TemplateInstance } from './element/types.js';
5
5
  export declare class WebUIElement extends TemplateElement {
6
6
  private $lazyHydrationMode;
7
+ private $authoredPhase;
8
+ private $pendingPropertyChanges;
9
+ private $preUpgradeProperties;
10
+ constructor();
11
+ connectedCallback(): void;
12
+ $destroy(): void;
13
+ $recordPropertyChange(name: string, oldValue: unknown): void;
14
+ protected $reconcileAuthoredState(): boolean;
15
+ $flushUpdates(): void;
16
+ private $flushAuthoredChanges;
17
+ private $invokePropertyChange;
18
+ private $releasePropertyChanges;
7
19
  protected $shouldDeferSSRHydration(meta?: TemplateMeta): boolean;
8
20
  protected $didDeferSSRHydration(): void;
9
21
  protected $didDeferStreamedSSRHydration(state: Record<string, unknown> | undefined): void;
package/dist/element.js CHANGED
@@ -7,6 +7,9 @@ const RENDER_ATTR = 'w-render';
7
7
  const HYDRATE_EAGER = 'eager';
8
8
  const INTERACTION_HYDRATION = 3;
9
9
  const LAZY_RENDER_INTERACTION = 4;
10
+ const AUTHORED_LIVE = 1;
11
+ const AUTHORED_PENDING = 2;
12
+ const AUTHORED_FLUSHING = 4;
10
13
  const DEV = typeof __WEBUI_DEV__ === 'undefined' || __WEBUI_DEV__;
11
14
  let warnedMissingLazyHydrationEntry = false;
12
15
  function warnMissingLazyHydrationEntry(tag, policy) {
@@ -22,6 +25,121 @@ function warnMissingLazyHydrationEntry(tag, policy) {
22
25
  'your component definitions to enable visibility-deferred hydration.');
23
26
  }
24
27
  export class WebUIElement extends TemplateElement {
28
+ constructor() {
29
+ super();
30
+ const names = getObservableNames(this.constructor);
31
+ for (const name of names) {
32
+ if (!Object.prototype.hasOwnProperty.call(this, name))
33
+ continue;
34
+ (this.$preUpgradeProperties ??= new Map()).set(name, this[name]);
35
+ delete this[name];
36
+ }
37
+ }
38
+ connectedCallback() {
39
+ const properties = this.$preUpgradeProperties;
40
+ if (properties) {
41
+ this.$preUpgradeProperties = undefined;
42
+ for (const [name, value] of properties) {
43
+ this[name] = value;
44
+ }
45
+ }
46
+ super.connectedCallback();
47
+ }
48
+ $destroy() {
49
+ if (this.$authoredPhase !== undefined)
50
+ this.$authoredPhase &= ~AUTHORED_LIVE;
51
+ super.$destroy();
52
+ }
53
+ $recordPropertyChange(name, oldValue) {
54
+ if (this.$authoredPhase === undefined)
55
+ return;
56
+ const changes = this.$pendingPropertyChanges;
57
+ if ((this.$authoredPhase & AUTHORED_LIVE) && this.$canRunAuthoredEffects()) {
58
+ if (changes?.has(name)) {
59
+ oldValue = changes.get(name);
60
+ changes.delete(name);
61
+ }
62
+ try {
63
+ this.$invokePropertyChange(name, oldValue);
64
+ }
65
+ finally {
66
+ this.$releasePropertyChanges();
67
+ }
68
+ }
69
+ else {
70
+ const queued = changes ?? (this.$pendingPropertyChanges = new Map());
71
+ if (!queued.has(name))
72
+ queued.set(name, oldValue);
73
+ this.$authoredPhase |= AUTHORED_PENDING;
74
+ }
75
+ }
76
+ $reconcileAuthoredState() {
77
+ if (!this.$canRunAuthoredEffects())
78
+ return !this.$pendingPropertyChanges;
79
+ if (this.$authoredPhase === undefined) {
80
+ const names = getObservableNames(this.constructor);
81
+ for (const name of names) {
82
+ if (typeof this[`${name}Changed`] !== 'function')
83
+ continue;
84
+ this.$authoredPhase = AUTHORED_LIVE;
85
+ if (Object.prototype.hasOwnProperty.call(this, `_${name}`)) {
86
+ (this.$pendingPropertyChanges ??= new Map()).set(name, undefined);
87
+ }
88
+ }
89
+ }
90
+ if (this.$authoredPhase === undefined)
91
+ return true;
92
+ this.$authoredPhase |= AUTHORED_LIVE;
93
+ if (this.$authoredPhase & AUTHORED_FLUSHING)
94
+ return false;
95
+ if (this.$pendingPropertyChanges) {
96
+ this.$authoredPhase |= AUTHORED_PENDING;
97
+ this.$flushAuthoredChanges();
98
+ }
99
+ return !this.$pendingPropertyChanges;
100
+ }
101
+ $flushUpdates() {
102
+ if (this.$pendingPropertyChanges)
103
+ this.$notifyHydrated();
104
+ super.$flushUpdates();
105
+ }
106
+ $flushAuthoredChanges() {
107
+ const changes = this.$pendingPropertyChanges;
108
+ if (!changes || !this.$canRunAuthoredEffects())
109
+ return;
110
+ this.$authoredPhase = AUTHORED_LIVE | AUTHORED_PENDING | AUTHORED_FLUSHING;
111
+ try {
112
+ for (const [name, oldValue] of changes) {
113
+ if (!this.$canRunAuthoredEffects()) {
114
+ this.$authoredPhase &= ~AUTHORED_LIVE;
115
+ break;
116
+ }
117
+ changes.delete(name);
118
+ this.$invokePropertyChange(name, oldValue);
119
+ }
120
+ }
121
+ finally {
122
+ this.$authoredPhase &= ~AUTHORED_FLUSHING;
123
+ this.$releasePropertyChanges();
124
+ }
125
+ }
126
+ $invokePropertyChange(name, oldValue) {
127
+ const value = this[name];
128
+ if (Object.is(oldValue, value))
129
+ return;
130
+ const callback = this[`${name}Changed`];
131
+ if (typeof callback === 'function') {
132
+ callback.call(this, oldValue, value);
133
+ }
134
+ }
135
+ $releasePropertyChanges() {
136
+ const phase = this.$authoredPhase;
137
+ if (phase !== undefined && this.$pendingPropertyChanges?.size === 0 &&
138
+ !(phase & AUTHORED_FLUSHING)) {
139
+ this.$pendingPropertyChanges = undefined;
140
+ this.$authoredPhase = phase & ~AUTHORED_PENDING;
141
+ }
142
+ }
25
143
  $shouldDeferSSRHydration(meta) {
26
144
  this.$lazyHydrationMode = this.$resolveLazyHydrationMode(meta);
27
145
  return this.$lazyHydrationMode !== undefined;
@@ -90,6 +208,8 @@ export class WebUIElement extends TemplateElement {
90
208
  this.$activateDeferredSSR(state);
91
209
  }
92
210
  disconnectedCallback() {
211
+ if (this.$authoredPhase !== undefined)
212
+ this.$authoredPhase &= ~AUTHORED_LIVE;
93
213
  if (this.$lazyHydrationMode !== undefined) {
94
214
  disconnectLazyHydration(this);
95
215
  }
@@ -103,7 +223,8 @@ export class WebUIElement extends TemplateElement {
103
223
  return attribute === undefined || !this.hasAttribute(attribute);
104
224
  }
105
225
  $syncAuthoredAttributes() {
106
- syncAttrProperties(this, this.constructor);
226
+ if (this.isConnected)
227
+ syncAttrProperties(this, this.constructor);
107
228
  }
108
229
  $emit(name, detail) {
109
230
  return this.dispatchEvent(new CustomEvent(name, {
@@ -12,7 +12,7 @@ export interface TemplateStylesheetDescriptor {
12
12
  readonly title: string;
13
13
  readonly type: string;
14
14
  }
15
- export declare function cloneTemplateContent(meta: TemplateBlockMeta): DocumentFragment;
15
+ export declare function cloneTemplateContent(meta: TemplateBlockMeta, ownerDocument: Document): DocumentFragment;
16
16
  export declare function getTemplateFragment(meta: TemplateBlockMeta): DocumentFragment;
17
17
  export declare function getTemplateStylesheets(meta: TemplateBlockMeta): readonly TemplateStylesheetDescriptor[] | undefined;
18
18
  export declare function templateHtmlMayContainLink(html: string): boolean;
@@ -2,8 +2,8 @@ import { setTemplateContent } from './trusted-types-policy.js';
2
2
  const EMPTY_STYLESHEETS = Object.freeze([]);
3
3
  const NO_LINK_STYLESHEETS = Object.freeze([]);
4
4
  const templateContentCache = new WeakMap();
5
- export function cloneTemplateContent(meta) {
6
- return getTemplateFragment(meta).cloneNode(true);
5
+ export function cloneTemplateContent(meta, ownerDocument) {
6
+ return ownerDocument.importNode(getTemplateFragment(meta), true);
7
7
  }
8
8
  export function getTemplateFragment(meta) {
9
9
  return getTemplateContent(meta).fragment;
@@ -19,7 +19,7 @@ export declare class TemplateElement extends HTMLElement {
19
19
  private $hydrated;
20
20
  private $deferredClientMount;
21
21
  private $resetClientShadow;
22
- private $hasMounted;
22
+ private $mountPhase;
23
23
  private $deferredSSR;
24
24
  private $activatingDeferredSSR;
25
25
  private $deferredAncestor;
@@ -78,7 +78,7 @@ export declare class TemplateElement extends HTMLElement {
78
78
  protected $shouldApplySSRState(_key: string): boolean;
79
79
  protected $syncAuthoredAttributes(): void;
80
80
  protected hydratedCallback(): void;
81
- private $notifyHydrated;
81
+ protected $notifyHydrated(): void;
82
82
  private $finishHydration;
83
83
  protected $shouldApplyTemplateStateFromSSR(_key: string): boolean;
84
84
  protected $setTemplateState(key: string, value: unknown): boolean;
@@ -93,6 +93,8 @@ export declare class TemplateElement extends HTMLElement {
93
93
  private $recordDeferredWrite;
94
94
  private $replayDeferredWrites;
95
95
  $flushUpdates(): void;
96
+ protected $reconcileAuthoredState(): boolean;
97
+ protected $canRunAuthoredEffects(): boolean;
96
98
  private $flush;
97
99
  private $recordPreReadyWrite;
98
100
  private $checkHydrationMismatch;
@@ -204,6 +204,8 @@ function hasAuthoredMember(instance, key) {
204
204
  }
205
205
  return false;
206
206
  }
207
+ const MOUNTED = 1;
208
+ const HYDRATION_NOTIFIED = 2;
207
209
  export class TemplateElement extends HTMLElement {
208
210
  constructor() {
209
211
  super(...arguments);
@@ -212,7 +214,7 @@ export class TemplateElement extends HTMLElement {
212
214
  this.$hydrated = false;
213
215
  this.$deferredClientMount = false;
214
216
  this.$resetClientShadow = false;
215
- this.$hasMounted = false;
217
+ this.$mountPhase = 0;
216
218
  this.$deferredSSR = false;
217
219
  this.$activatingDeferredSSR = false;
218
220
  this.$hasUnknownScopes = false;
@@ -322,6 +324,8 @@ export class TemplateElement extends HTMLElement {
322
324
  hydrationStart();
323
325
  try {
324
326
  this.$ready = true;
327
+ this.$syncAuthoredAttributes();
328
+ this.$notifyHydrated();
325
329
  this.$update();
326
330
  }
327
331
  finally {
@@ -329,7 +333,7 @@ export class TemplateElement extends HTMLElement {
329
333
  }
330
334
  return;
331
335
  }
332
- if (!this.$hasMounted &&
336
+ if (!this.$mountPhase &&
333
337
  isStreamingHydrationMode() &&
334
338
  this.hasAttribute(STREAMED_HOST_ATTR)) {
335
339
  this.$deferredSSR = true;
@@ -346,7 +350,7 @@ export class TemplateElement extends HTMLElement {
346
350
  return;
347
351
  }
348
352
  this.$meta = meta;
349
- if (this.$hasMounted) {
353
+ if (this.$mountPhase) {
350
354
  this.$remount(meta);
351
355
  }
352
356
  else {
@@ -856,10 +860,14 @@ export class TemplateElement extends HTMLElement {
856
860
  hydratedCallback() {
857
861
  }
858
862
  $notifyHydrated() {
859
- if (this.$hasMounted)
863
+ if (!this.$mountPhase)
864
+ this.$mountPhase = MOUNTED;
865
+ if (!this.$reconcileAuthoredState())
860
866
  return;
861
- this.$hasMounted = true;
862
- this.hydratedCallback();
867
+ if (this.$mountPhase === MOUNTED) {
868
+ this.$mountPhase = HYDRATION_NOTIFIED;
869
+ this.hydratedCallback();
870
+ }
863
871
  }
864
872
  $finishHydration() {
865
873
  let callbackFailed = false;
@@ -1031,9 +1039,18 @@ export class TemplateElement extends HTMLElement {
1031
1039
  this.$flush(true);
1032
1040
  }
1033
1041
  $flushUpdates() {
1042
+ if (this.$mountPhase === MOUNTED)
1043
+ this.$notifyHydrated();
1034
1044
  if (this.$pendingFlush)
1035
1045
  this.$flush();
1036
1046
  }
1047
+ $reconcileAuthoredState() {
1048
+ return true;
1049
+ }
1050
+ $canRunAuthoredEffects() {
1051
+ return this.$hydrated && this.$root !== null && this.$ready &&
1052
+ !this.$deferredClientMount && this.isConnected;
1053
+ }
1037
1054
  $flush(requireKnownState = this.$guardUnknownState === true) {
1038
1055
  if (!this.$ready || !this.$root) {
1039
1056
  this.$dirtyPaths = null;
@@ -1108,10 +1125,10 @@ export class TemplateElement extends HTMLElement {
1108
1125
  void import('./hydration-mismatch.js').then((m) => m.reportHydrationMismatch(tag, writes, index, ctx));
1109
1126
  }
1110
1127
  $parseTemplate(meta) {
1111
- return cloneTemplateContent(meta);
1128
+ return cloneTemplateContent(meta, this.ownerDocument);
1112
1129
  }
1113
1130
  $createStagingRoot(meta) {
1114
- const wrapper = document.createElement('div');
1131
+ const wrapper = this.ownerDocument.createElement('div');
1115
1132
  const fragment = this.$parseTemplate(meta);
1116
1133
  wrapper.appendChild(fragment);
1117
1134
  customElements.upgrade(wrapper);
@@ -1125,7 +1142,7 @@ export class TemplateElement extends HTMLElement {
1125
1142
  root.appendChild(first);
1126
1143
  return;
1127
1144
  }
1128
- const fragment = document.createDocumentFragment();
1145
+ const fragment = this.ownerDocument.createDocumentFragment();
1129
1146
  while (stagingRoot.firstChild) {
1130
1147
  fragment.appendChild(stagingRoot.firstChild);
1131
1148
  }
@@ -1157,6 +1174,7 @@ export class TemplateElement extends HTMLElement {
1157
1174
  }
1158
1175
  }
1159
1176
  $wire(root, meta, scope) {
1177
+ const ownerDocument = this.ownerDocument;
1160
1178
  const instance = {
1161
1179
  scope, container: root, nodes: childNodesArray(root),
1162
1180
  texts: bindingArray(meta.tx?.length ?? 0),
@@ -1181,8 +1199,8 @@ export class TemplateElement extends HTMLElement {
1181
1199
  let node;
1182
1200
  let end;
1183
1201
  if (raw) {
1184
- const start = document.createComment(rawMarker(rawIndex));
1185
- end = document.createComment(rawMarker(rawIndex, true));
1202
+ const start = ownerDocument.createComment(rawMarker(rawIndex));
1203
+ end = ownerDocument.createComment(rawMarker(rawIndex, true));
1186
1204
  rawIndex++;
1187
1205
  instance.texts.push({
1188
1206
  node: start,
@@ -1195,7 +1213,7 @@ export class TemplateElement extends HTMLElement {
1195
1213
  node = start;
1196
1214
  }
1197
1215
  else {
1198
- const textNode = document.createTextNode('');
1216
+ const textNode = ownerDocument.createTextNode('');
1199
1217
  instance.texts.push({ node: textNode, parts, scope });
1200
1218
  node = textNode;
1201
1219
  }
@@ -1217,7 +1235,7 @@ export class TemplateElement extends HTMLElement {
1217
1235
  const parent = elements[parentIndex];
1218
1236
  if (!parent || (parent.nodeType !== 1 && parent.nodeType !== 11))
1219
1237
  continue;
1220
- const anchor = document.createComment('');
1238
+ const anchor = ownerDocument.createComment('');
1221
1239
  instance.conds.push({
1222
1240
  condition: condition,
1223
1241
  blockIndex,
@@ -1243,7 +1261,7 @@ export class TemplateElement extends HTMLElement {
1243
1261
  const parent = elements[parentIndex];
1244
1262
  if (!parent || (parent.nodeType !== 1 && parent.nodeType !== 11))
1245
1263
  continue;
1246
- const anchor = document.createComment('');
1264
+ const anchor = ownerDocument.createComment('');
1247
1265
  const binding = {
1248
1266
  markerId: i, collection, itemVar, blockIndex,
1249
1267
  container: parent, start: anchor, end: null,
@@ -1271,6 +1289,7 @@ export class TemplateElement extends HTMLElement {
1271
1289
  return instance;
1272
1290
  }
1273
1291
  $hydrate(ssrRoot, meta, tplDom, scope, pathStart = 0) {
1292
+ const ownerDocument = this.ownerDocument;
1274
1293
  const instance = {
1275
1294
  scope,
1276
1295
  container: (pathStart > 0 ? ssrRoot.parentNode : ssrRoot),
@@ -1334,7 +1353,7 @@ export class TemplateElement extends HTMLElement {
1334
1353
  }
1335
1354
  let textNode = previous?.nodeType === 3 ? previous : null;
1336
1355
  if (textNode === null) {
1337
- textNode = document.createTextNode('');
1356
+ textNode = ownerDocument.createTextNode('');
1338
1357
  ssrParent.insertBefore(textNode, insertRef);
1339
1358
  }
1340
1359
  instance.texts.push({ node: textNode, parts, scope });
@@ -1356,7 +1375,7 @@ export class TemplateElement extends HTMLElement {
1356
1375
  condAnchor = marker;
1357
1376
  }
1358
1377
  else {
1359
- condAnchor = document.createComment('');
1378
+ condAnchor = ownerDocument.createComment('');
1360
1379
  const [, beforeIndex] = slotMeta;
1361
1380
  const insertRef = ssrParent.childNodes[beforeIndex ?? ssrParent.childNodes.length] ?? null;
1362
1381
  ssrParent.insertBefore(condAnchor, insertRef);
@@ -1405,7 +1424,7 @@ export class TemplateElement extends HTMLElement {
1405
1424
  anchor = marker;
1406
1425
  }
1407
1426
  else {
1408
- anchor = document.createComment('');
1427
+ anchor = ownerDocument.createComment('');
1409
1428
  const [, beforeIndex] = slotMeta;
1410
1429
  const tplParent = tplElements[parentIndex];
1411
1430
  const staticCount = tplParent ? tplParent.childNodes.length : 0;
@@ -1447,7 +1466,7 @@ export class TemplateElement extends HTMLElement {
1447
1466
  else {
1448
1467
  const itemParent = itemMarkers[j].parentNode;
1449
1468
  const nextBound = j + 1 < itemMarkers.length ? itemMarkers[j + 1] : endMarker;
1450
- const wrapper = document.createElement('div');
1469
+ const wrapper = ownerDocument.createElement('div');
1451
1470
  let cursor = itemMarkers[j].nextSibling;
1452
1471
  while (cursor && cursor !== nextBound) {
1453
1472
  const next = cursor.nextSibling;
@@ -1553,7 +1572,7 @@ export class TemplateElement extends HTMLElement {
1553
1572
  const condNodes = this.$collectConditionalRange(condAnchor);
1554
1573
  if (condNodes.length === 0)
1555
1574
  return null;
1556
- const wrapper = document.createElement('div');
1575
+ const wrapper = this.ownerDocument.createElement('div');
1557
1576
  for (let cn = 0; cn < condNodes.length; cn++)
1558
1577
  wrapper.appendChild(condNodes[cn]);
1559
1578
  const inst = this.$hydrate(wrapper, blockMeta, tplDom, scope);
@@ -2006,7 +2025,7 @@ export class TemplateElement extends HTMLElement {
2006
2025
  const first = instance.nodes[0] ?? null;
2007
2026
  const container = first?.parentNode;
2008
2027
  if (first && container) {
2009
- const anchor = document.createComment('');
2028
+ const anchor = this.ownerDocument.createComment('');
2010
2029
  container.insertBefore(anchor, first);
2011
2030
  this.$swapOwnedRange(c.owner, instance.nodes, anchor);
2012
2031
  c.anchor = anchor;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/webui-framework",
3
- "version": "0.0.29",
3
+ "version": "3.0.0",
4
4
  "publishConfig": {
5
5
  "tag": "latest"
6
6
  },
@@ -20,6 +20,12 @@
20
20
  "default": "./dist/component-asset.js"
21
21
  }
22
22
  },
23
+ "./component-asset-runtime.js": {
24
+ "import": {
25
+ "types": "./dist/component-asset-runtime.d.ts",
26
+ "default": "./dist/component-asset-runtime.js"
27
+ }
28
+ },
23
29
  "./streaming.js": {
24
30
  "import": {
25
31
  "types": "./dist/streaming-entry.d.ts",
@@ -43,7 +49,7 @@
43
49
  "dist/"
44
50
  ],
45
51
  "devDependencies": {
46
- "@microsoft/webui-test-support": "0.0.29",
52
+ "@microsoft/webui-test-support": "3.0.0",
47
53
  "@playwright/test": "^1.63.0",
48
54
  "@types/node": "^26.5.1",
49
55
  "typescript": "7.0.2"