@microsoft/webui-framework 0.0.30 → 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
@@ -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;
@@ -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;
@@ -1125,10 +1125,10 @@ export class TemplateElement extends HTMLElement {
1125
1125
  void import('./hydration-mismatch.js').then((m) => m.reportHydrationMismatch(tag, writes, index, ctx));
1126
1126
  }
1127
1127
  $parseTemplate(meta) {
1128
- return cloneTemplateContent(meta);
1128
+ return cloneTemplateContent(meta, this.ownerDocument);
1129
1129
  }
1130
1130
  $createStagingRoot(meta) {
1131
- const wrapper = document.createElement('div');
1131
+ const wrapper = this.ownerDocument.createElement('div');
1132
1132
  const fragment = this.$parseTemplate(meta);
1133
1133
  wrapper.appendChild(fragment);
1134
1134
  customElements.upgrade(wrapper);
@@ -1142,7 +1142,7 @@ export class TemplateElement extends HTMLElement {
1142
1142
  root.appendChild(first);
1143
1143
  return;
1144
1144
  }
1145
- const fragment = document.createDocumentFragment();
1145
+ const fragment = this.ownerDocument.createDocumentFragment();
1146
1146
  while (stagingRoot.firstChild) {
1147
1147
  fragment.appendChild(stagingRoot.firstChild);
1148
1148
  }
@@ -1174,6 +1174,7 @@ export class TemplateElement extends HTMLElement {
1174
1174
  }
1175
1175
  }
1176
1176
  $wire(root, meta, scope) {
1177
+ const ownerDocument = this.ownerDocument;
1177
1178
  const instance = {
1178
1179
  scope, container: root, nodes: childNodesArray(root),
1179
1180
  texts: bindingArray(meta.tx?.length ?? 0),
@@ -1198,8 +1199,8 @@ export class TemplateElement extends HTMLElement {
1198
1199
  let node;
1199
1200
  let end;
1200
1201
  if (raw) {
1201
- const start = document.createComment(rawMarker(rawIndex));
1202
- end = document.createComment(rawMarker(rawIndex, true));
1202
+ const start = ownerDocument.createComment(rawMarker(rawIndex));
1203
+ end = ownerDocument.createComment(rawMarker(rawIndex, true));
1203
1204
  rawIndex++;
1204
1205
  instance.texts.push({
1205
1206
  node: start,
@@ -1212,7 +1213,7 @@ export class TemplateElement extends HTMLElement {
1212
1213
  node = start;
1213
1214
  }
1214
1215
  else {
1215
- const textNode = document.createTextNode('');
1216
+ const textNode = ownerDocument.createTextNode('');
1216
1217
  instance.texts.push({ node: textNode, parts, scope });
1217
1218
  node = textNode;
1218
1219
  }
@@ -1234,7 +1235,7 @@ export class TemplateElement extends HTMLElement {
1234
1235
  const parent = elements[parentIndex];
1235
1236
  if (!parent || (parent.nodeType !== 1 && parent.nodeType !== 11))
1236
1237
  continue;
1237
- const anchor = document.createComment('');
1238
+ const anchor = ownerDocument.createComment('');
1238
1239
  instance.conds.push({
1239
1240
  condition: condition,
1240
1241
  blockIndex,
@@ -1260,7 +1261,7 @@ export class TemplateElement extends HTMLElement {
1260
1261
  const parent = elements[parentIndex];
1261
1262
  if (!parent || (parent.nodeType !== 1 && parent.nodeType !== 11))
1262
1263
  continue;
1263
- const anchor = document.createComment('');
1264
+ const anchor = ownerDocument.createComment('');
1264
1265
  const binding = {
1265
1266
  markerId: i, collection, itemVar, blockIndex,
1266
1267
  container: parent, start: anchor, end: null,
@@ -1288,6 +1289,7 @@ export class TemplateElement extends HTMLElement {
1288
1289
  return instance;
1289
1290
  }
1290
1291
  $hydrate(ssrRoot, meta, tplDom, scope, pathStart = 0) {
1292
+ const ownerDocument = this.ownerDocument;
1291
1293
  const instance = {
1292
1294
  scope,
1293
1295
  container: (pathStart > 0 ? ssrRoot.parentNode : ssrRoot),
@@ -1351,7 +1353,7 @@ export class TemplateElement extends HTMLElement {
1351
1353
  }
1352
1354
  let textNode = previous?.nodeType === 3 ? previous : null;
1353
1355
  if (textNode === null) {
1354
- textNode = document.createTextNode('');
1356
+ textNode = ownerDocument.createTextNode('');
1355
1357
  ssrParent.insertBefore(textNode, insertRef);
1356
1358
  }
1357
1359
  instance.texts.push({ node: textNode, parts, scope });
@@ -1373,7 +1375,7 @@ export class TemplateElement extends HTMLElement {
1373
1375
  condAnchor = marker;
1374
1376
  }
1375
1377
  else {
1376
- condAnchor = document.createComment('');
1378
+ condAnchor = ownerDocument.createComment('');
1377
1379
  const [, beforeIndex] = slotMeta;
1378
1380
  const insertRef = ssrParent.childNodes[beforeIndex ?? ssrParent.childNodes.length] ?? null;
1379
1381
  ssrParent.insertBefore(condAnchor, insertRef);
@@ -1422,7 +1424,7 @@ export class TemplateElement extends HTMLElement {
1422
1424
  anchor = marker;
1423
1425
  }
1424
1426
  else {
1425
- anchor = document.createComment('');
1427
+ anchor = ownerDocument.createComment('');
1426
1428
  const [, beforeIndex] = slotMeta;
1427
1429
  const tplParent = tplElements[parentIndex];
1428
1430
  const staticCount = tplParent ? tplParent.childNodes.length : 0;
@@ -1464,7 +1466,7 @@ export class TemplateElement extends HTMLElement {
1464
1466
  else {
1465
1467
  const itemParent = itemMarkers[j].parentNode;
1466
1468
  const nextBound = j + 1 < itemMarkers.length ? itemMarkers[j + 1] : endMarker;
1467
- const wrapper = document.createElement('div');
1469
+ const wrapper = ownerDocument.createElement('div');
1468
1470
  let cursor = itemMarkers[j].nextSibling;
1469
1471
  while (cursor && cursor !== nextBound) {
1470
1472
  const next = cursor.nextSibling;
@@ -1570,7 +1572,7 @@ export class TemplateElement extends HTMLElement {
1570
1572
  const condNodes = this.$collectConditionalRange(condAnchor);
1571
1573
  if (condNodes.length === 0)
1572
1574
  return null;
1573
- const wrapper = document.createElement('div');
1575
+ const wrapper = this.ownerDocument.createElement('div');
1574
1576
  for (let cn = 0; cn < condNodes.length; cn++)
1575
1577
  wrapper.appendChild(condNodes[cn]);
1576
1578
  const inst = this.$hydrate(wrapper, blockMeta, tplDom, scope);
@@ -2023,7 +2025,7 @@ export class TemplateElement extends HTMLElement {
2023
2025
  const first = instance.nodes[0] ?? null;
2024
2026
  const container = first?.parentNode;
2025
2027
  if (first && container) {
2026
- const anchor = document.createComment('');
2028
+ const anchor = this.ownerDocument.createComment('');
2027
2029
  container.insertBefore(anchor, first);
2028
2030
  this.$swapOwnedRange(c.owner, instance.nodes, anchor);
2029
2031
  c.anchor = anchor;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/webui-framework",
3
- "version": "0.0.30",
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.30",
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"