@microsoft/webui-framework 0.0.20 → 0.0.22

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
@@ -257,11 +257,17 @@ export const settingsAssets = defineComponentAssets({
257
257
  });
258
258
  ```
259
259
 
260
- The asset module carries the component's template and style payload. Use
261
- `preload(tag)` to start template, module, and optional data work early, then
262
- `create(tag)` to create the element after template/module work is ready.
263
- Concurrent asset requests share one in-flight load and CSS module styles are
264
- deduped. `create(tag)` does not block on optional data by default. Use
260
+ The asset graph keeps entry-owned templates external, leaves single-root
261
+ dependencies inline, and emits dependencies shared by multiple roots once as
262
+ flat dynamic chunks. Component assets cannot be combined with `<route>`. Load
263
+ the normal entry bundle first so external prerequisites are registered.
264
+
265
+ Shared chunk filenames are generated and must not be copied into the manifest.
266
+ Each root asset carries its own dynamic imports; `--metafile` is available for
267
+ analysis and build tooling. `preload(tag)` starts template, module, and optional
268
+ data work. Concurrent roots share in-flight chunk imports by resolved URL and
269
+ CSS module styles are deduped. `create(tag)` creates the element after
270
+ template/module work is ready and does not block on optional data by default. Use
265
271
  `create(tag, { awaitData: true, dataTimeoutMs: 150 })` only when a component must
266
272
  wait briefly for state before mounting.
267
273
 
@@ -602,7 +608,7 @@ interface TemplateMeta {
602
608
  eg?: [event, [[handler, argSpecs, targetPath, usesEvent?]]][]; // Events
603
609
  b?: TemplateBlockMeta[]; // Nested block metadata
604
610
  sa?: string; // Adopted stylesheet specifier
605
- sd?: boolean; // Shadow DOM flag for client-created
611
+ sd?: 1; // Shadow DOM flag for client-created
606
612
  re?: [event, handler, argSpecs][]; // Root-level events
607
613
  tr?: string[]; // Template state roots
608
614
  ta?: string[]; // Host attributes aligned with tr
@@ -0,0 +1,22 @@
1
+ import type { CompiledConditionFn, TemplateMeta } from '../template.js';
2
+ export interface ComponentAssetImport {
3
+ components: string[];
4
+ href: string;
5
+ load: () => Promise<unknown>;
6
+ }
7
+ export interface ComponentAsset {
8
+ type: 'webui-component-asset';
9
+ version: 2;
10
+ kind: 'root' | 'chunk';
11
+ root?: string;
12
+ components: string[];
13
+ requiredComponents: string[];
14
+ externalComponents: string[];
15
+ imports: ComponentAssetImport[];
16
+ templateStyles: string[];
17
+ templates: Record<string, TemplateMeta>;
18
+ templateFunctions?: Record<string, CompiledConditionFn[]>;
19
+ }
20
+ export declare function readComponentAssetModule(module: unknown): unknown;
21
+ export declare function validateAsset(value: unknown, expectedKind: ComponentAsset['kind']): asserts value is ComponentAsset;
22
+ export declare function sameComponents(left: readonly string[], right: readonly string[]): boolean;
@@ -0,0 +1,171 @@
1
+ const ASSET_TYPE = 'webui-component-asset';
2
+ const ASSET_VERSION = 2;
3
+ export function readComponentAssetModule(module) {
4
+ if (!isObject(module) || !isObject(module.default)) {
5
+ throw new Error('[WebUI] Component asset module must default-export an asset object.');
6
+ }
7
+ return module.default;
8
+ }
9
+ export function validateAsset(value, expectedKind) {
10
+ if (!isObject(value)) {
11
+ throw new Error('[WebUI] Component asset default export must be an object.');
12
+ }
13
+ const asset = value;
14
+ if (asset.type !== ASSET_TYPE) {
15
+ throw new Error(`[WebUI] Invalid component asset type: ${String(asset.type)}`);
16
+ }
17
+ if (asset.version !== ASSET_VERSION) {
18
+ throw new Error(`[WebUI] Unsupported component asset version: ${String(asset.version)}`);
19
+ }
20
+ if (asset.kind !== expectedKind) {
21
+ throw new Error(`[WebUI] Expected component asset kind "${expectedKind}", received "${String(asset.kind)}".`);
22
+ }
23
+ validateUniqueStringArray(asset.components, 'components');
24
+ validateUniqueStringArray(asset.requiredComponents, 'requiredComponents');
25
+ validateUniqueStringArray(asset.externalComponents, 'externalComponents');
26
+ if (!Array.isArray(asset.imports)) {
27
+ throw new Error('[WebUI] Component asset imports must be an array.');
28
+ }
29
+ validateStringArray(asset.templateStyles, 'templateStyles');
30
+ if (!isObject(asset.templates)) {
31
+ throw new Error('[WebUI] Component asset templates must be an object.');
32
+ }
33
+ let root;
34
+ if (expectedKind === 'root') {
35
+ if (typeof asset.root !== 'string' || asset.root.length === 0) {
36
+ throw new Error('[WebUI] Component asset root must name its root component.');
37
+ }
38
+ root = asset.root;
39
+ }
40
+ else {
41
+ validateChunkShape(asset.root, asset.imports, asset.externalComponents);
42
+ }
43
+ for (let i = 0; i < asset.imports.length; i++) {
44
+ validateAssetImport(asset.imports[i]);
45
+ }
46
+ validateAssetPayload(asset.components, asset.templates, asset.templateFunctions);
47
+ validateAssetCoverage(root, asset.components, asset.requiredComponents, asset.externalComponents, asset.imports);
48
+ }
49
+ export function sameComponents(left, right) {
50
+ if (left.length !== right.length)
51
+ return false;
52
+ for (let i = 0; i < left.length; i++) {
53
+ if (left[i] !== right[i])
54
+ return false;
55
+ }
56
+ return true;
57
+ }
58
+ function isObject(value) {
59
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
60
+ }
61
+ function validateChunkShape(root, imports, externalComponents) {
62
+ if (root !== undefined) {
63
+ throw new Error('[WebUI] Shared component asset chunks cannot declare a root.');
64
+ }
65
+ if (imports.length !== 0) {
66
+ throw new Error('[WebUI] Shared component asset chunks cannot import other chunks.');
67
+ }
68
+ if (externalComponents.length !== 0) {
69
+ throw new Error('[WebUI] Shared component asset chunks cannot declare external components.');
70
+ }
71
+ }
72
+ function validateAssetImport(value) {
73
+ if (!isObject(value)) {
74
+ throw new Error('[WebUI] Component asset import must be an object.');
75
+ }
76
+ const assetImport = value;
77
+ validateUniqueStringArray(assetImport.components, 'import components');
78
+ if (assetImport.components.length === 0) {
79
+ throw new Error('[WebUI] Component asset imports must provide at least one component.');
80
+ }
81
+ if (typeof assetImport.href !== 'string' || assetImport.href.length === 0) {
82
+ throw new Error('[WebUI] Component asset import href must be a non-empty string.');
83
+ }
84
+ if (typeof assetImport.load !== 'function') {
85
+ throw new Error('[WebUI] Component asset import load must be a function.');
86
+ }
87
+ }
88
+ function validateUniqueStringArray(value, field) {
89
+ validateStringArray(value, field);
90
+ const seen = new Set();
91
+ for (let i = 0; i < value.length; i++) {
92
+ if (seen.has(value[i])) {
93
+ throw new Error(`[WebUI] Component asset ${field} cannot contain duplicate <${value[i]}> entries.`);
94
+ }
95
+ seen.add(value[i]);
96
+ }
97
+ }
98
+ function validateStringArray(value, field) {
99
+ if (!Array.isArray(value)) {
100
+ throw new Error(`[WebUI] Component asset ${field} must be an array.`);
101
+ }
102
+ for (let i = 0; i < value.length; i++) {
103
+ if (typeof value[i] !== 'string' || value[i].length === 0) {
104
+ throw new Error(`[WebUI] Component asset ${field} must contain non-empty strings.`);
105
+ }
106
+ }
107
+ }
108
+ function validateAssetCoverage(root, components, requiredComponents, externalComponents, imports) {
109
+ const required = new Set(requiredComponents);
110
+ if (root && !required.has(root)) {
111
+ throw new Error(`[WebUI] Component asset root <${root}> must include itself in requiredComponents.`);
112
+ }
113
+ const providers = new Set();
114
+ const addProvider = (component) => {
115
+ if (!required.has(component)) {
116
+ throw new Error(`[WebUI] Component asset provides undeclared template <${component}>. Add it to requiredComponents.`);
117
+ }
118
+ if (providers.has(component)) {
119
+ throw new Error(`[WebUI] Component asset assigns required template <${component}> to more than one payload, import, or external prerequisite.`);
120
+ }
121
+ providers.add(component);
122
+ };
123
+ for (let i = 0; i < components.length; i++)
124
+ addProvider(components[i]);
125
+ for (let i = 0; i < externalComponents.length; i++)
126
+ addProvider(externalComponents[i]);
127
+ for (let i = 0; i < imports.length; i++) {
128
+ const imported = imports[i].components;
129
+ for (let j = 0; j < imported.length; j++)
130
+ addProvider(imported[j]);
131
+ }
132
+ for (let i = 0; i < requiredComponents.length; i++) {
133
+ const component = requiredComponents[i];
134
+ if (!providers.has(component)) {
135
+ throw new Error(`[WebUI] Component asset required template <${component}> has no payload, import, or external prerequisite.`);
136
+ }
137
+ }
138
+ }
139
+ function validateAssetPayload(components, templates, templateFunctions) {
140
+ const declared = new Set(components);
141
+ const templateNames = Object.keys(templates);
142
+ for (let i = 0; i < templateNames.length; i++) {
143
+ const template = templateNames[i];
144
+ if (!declared.has(template)) {
145
+ throw new Error(`[WebUI] Component asset templates contain undeclared payload <${template}>.`);
146
+ }
147
+ }
148
+ for (let i = 0; i < components.length; i++) {
149
+ const component = components[i];
150
+ if (!Object.prototype.hasOwnProperty.call(templates, component)) {
151
+ throw new Error(`[WebUI] Component asset payload <${component}> is missing its template metadata.`);
152
+ }
153
+ }
154
+ if (templateFunctions === undefined)
155
+ return;
156
+ if (!isObject(templateFunctions)) {
157
+ throw new Error('[WebUI] Component asset templateFunctions must be an object.');
158
+ }
159
+ const functionNames = Object.keys(templateFunctions);
160
+ for (let i = 0; i < functionNames.length; i++) {
161
+ const component = functionNames[i];
162
+ if (!declared.has(component)) {
163
+ throw new Error(`[WebUI] Component asset templateFunctions contains undeclared payload <${component}>.`);
164
+ }
165
+ const functions = templateFunctions[component];
166
+ if (!Array.isArray(functions) ||
167
+ functions.some(candidate => typeof candidate !== 'function')) {
168
+ throw new Error(`[WebUI] Component asset templateFunctions for <${component}> must contain only functions.`);
169
+ }
170
+ }
171
+ }
@@ -0,0 +1 @@
1
+ export declare function loadComponentAsset(tag: string, url: string | URL): Promise<void>;
@@ -0,0 +1,104 @@
1
+ import { getTemplate, prepareAssetTemplateData, registerTemplateData, } from '../template.js';
2
+ import { readComponentAssetModule, sameComponents, validateAsset, } from './asset.js';
3
+ import { prepareAssetStyles, readNonce, registerAssetStyles, } from './resources.js';
4
+ const assetModulePromises = new Map();
5
+ export function loadComponentAsset(tag, url) {
6
+ const assetUrl = new URL(url, document.baseURI);
7
+ const href = assetUrl.href;
8
+ return loadAssetModule(href, () => import(assetUrl.href))
9
+ .then(imported => registerRootAsset(tag, href, imported));
10
+ }
11
+ async function registerRootAsset(expectedRoot, href, imported) {
12
+ const asset = readComponentAssetModule(imported);
13
+ validateAsset(asset, 'root');
14
+ if (asset.root !== expectedRoot) {
15
+ throw new Error(`[WebUI] Component asset manifest expected <${expectedRoot}> but ${href} exports <${String(asset.root)}>.`);
16
+ }
17
+ validateExternalComponents(asset);
18
+ const root = prepareComponentPayload(asset);
19
+ const chunks = await prepareAssetImports(asset.imports);
20
+ for (let i = 0; i < chunks.length; i++) {
21
+ registerComponentPayload(chunks[i]);
22
+ }
23
+ registerComponentPayload(root);
24
+ }
25
+ function loadAssetModule(href, load) {
26
+ let promise = assetModulePromises.get(href);
27
+ if (promise)
28
+ return promise;
29
+ promise = Promise.resolve()
30
+ .then(load)
31
+ .finally(() => {
32
+ assetModulePromises.delete(href);
33
+ });
34
+ assetModulePromises.set(href, promise);
35
+ return promise;
36
+ }
37
+ async function prepareAssetImports(imports) {
38
+ const pending = [];
39
+ for (let i = 0; i < imports.length; i++) {
40
+ const assetImport = imports[i];
41
+ if (componentsAlreadyRegistered(assetImport.components))
42
+ continue;
43
+ pending.push(importAndPrepareChunk(assetImport));
44
+ }
45
+ return Promise.all(pending);
46
+ }
47
+ function importAndPrepareChunk(assetImport) {
48
+ const href = new URL(assetImport.href, document.baseURI).href;
49
+ return loadAssetModule(href, assetImport.load)
50
+ .then(imported => {
51
+ const chunk = readComponentAssetModule(imported);
52
+ validateAsset(chunk, 'chunk');
53
+ if (!sameComponents(chunk.components, assetImport.components)) {
54
+ throw new Error(`[WebUI] Shared component asset ${href} does not provide the components declared by its root import.`);
55
+ }
56
+ return prepareComponentPayload(chunk);
57
+ });
58
+ }
59
+ function prepareComponentPayload(asset) {
60
+ prepareAssetTemplateData(asset.templates, asset.templateFunctions);
61
+ return {
62
+ asset,
63
+ styles: prepareAssetStyles(asset.templateStyles),
64
+ };
65
+ }
66
+ function registerComponentPayload(prepared) {
67
+ const { asset, styles } = prepared;
68
+ if (componentsAlreadyRegistered(asset.components))
69
+ return;
70
+ registerAssetStyles(styles, readNonce());
71
+ registerTemplateData(asset.templates, asset.templateFunctions);
72
+ validateRequiredComponents(asset);
73
+ }
74
+ function componentsAlreadyRegistered(components) {
75
+ if (components.length === 0)
76
+ return false;
77
+ for (let i = 0; i < components.length; i++) {
78
+ if (!getTemplate(components[i]))
79
+ return false;
80
+ }
81
+ return true;
82
+ }
83
+ function validateExternalComponents(asset) {
84
+ const missing = [];
85
+ for (let i = 0; i < asset.externalComponents.length; i++) {
86
+ const component = asset.externalComponents[i];
87
+ if (!getTemplate(component))
88
+ missing.push(component);
89
+ }
90
+ if (missing.length === 0)
91
+ return;
92
+ 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.`);
93
+ }
94
+ function validateRequiredComponents(asset) {
95
+ const missing = [];
96
+ for (let i = 0; i < asset.requiredComponents.length; i++) {
97
+ const component = asset.requiredComponents[i];
98
+ if (!getTemplate(component))
99
+ missing.push(component);
100
+ }
101
+ if (missing.length === 0)
102
+ return;
103
+ throw new Error(`[WebUI] Component asset is missing required template${missing.length === 1 ? '' : 's'}: ${missing.map(tag => `<${tag}>`).join(', ')}. Ensure the application entry bundle and protocol are loaded before deferred component assets.`);
104
+ }
@@ -0,0 +1,20 @@
1
+ export type ComponentAssetState = Record<string, unknown>;
2
+ export interface ComponentAssetManifestEntry<Data extends ComponentAssetState = ComponentAssetState> {
3
+ asset: string | URL;
4
+ module?: () => Promise<unknown>;
5
+ data?: () => Promise<Data>;
6
+ }
7
+ export type ComponentAssetManifest = Record<string, ComponentAssetManifestEntry>;
8
+ export interface ComponentAssetPreload<Data extends ComponentAssetState = ComponentAssetState> {
9
+ asset: Promise<void>;
10
+ module?: Promise<unknown>;
11
+ data?: Promise<Data>;
12
+ }
13
+ export interface ComponentAssetCreateOptions {
14
+ awaitData?: boolean;
15
+ dataTimeoutMs?: number;
16
+ }
17
+ export interface ComponentAssetRegistry {
18
+ preload<Data extends ComponentAssetState = ComponentAssetState>(tag: string): ComponentAssetPreload<Data>;
19
+ create(tag: string, options?: ComponentAssetCreateOptions): Promise<HTMLElement>;
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export type PreparedAssetStyles = Record<string, string>[];
2
+ export declare function prepareAssetStyles(templateStyles: readonly string[]): PreparedAssetStyles;
3
+ export declare function readNonce(): string;
4
+ export declare function registerAssetStyles(preparedStyles: readonly Record<string, string>[], nonce: string): void;
@@ -0,0 +1,83 @@
1
+ const injectedAssetStyles = new Set();
2
+ let assetStylesSeeded = false;
3
+ export function prepareAssetStyles(templateStyles) {
4
+ const prepared = new Array(templateStyles.length);
5
+ for (let i = 0; i < templateStyles.length; i++) {
6
+ prepared[i] = parseImportMap(templateStyles[i]);
7
+ }
8
+ return prepared;
9
+ }
10
+ export function readNonce() {
11
+ const nonce = assetGlobal()?.nonce;
12
+ if (nonce)
13
+ return nonce;
14
+ const meta = document.querySelector('meta[name="webui-nonce"]');
15
+ return meta?.content ?? '';
16
+ }
17
+ export function registerAssetStyles(preparedStyles, nonce) {
18
+ if (preparedStyles.length === 0)
19
+ return;
20
+ seedAssetStyleSet();
21
+ for (let i = 0; i < preparedStyles.length; i++) {
22
+ const imports = preparedStyles[i];
23
+ const nextImports = {};
24
+ let hasNewImport = false;
25
+ const specifiers = Object.keys(imports);
26
+ for (let j = 0; j < specifiers.length; j++) {
27
+ const specifier = specifiers[j];
28
+ if (injectedAssetStyles.has(specifier))
29
+ continue;
30
+ injectedAssetStyles.add(specifier);
31
+ nextImports[specifier] = imports[specifier];
32
+ hasNewImport = true;
33
+ }
34
+ if (!hasNewImport)
35
+ continue;
36
+ const script = document.createElement('script');
37
+ script.type = 'importmap';
38
+ if (nonce)
39
+ script.nonce = nonce;
40
+ script.textContent = JSON.stringify({ imports: nextImports });
41
+ document.head.appendChild(script);
42
+ }
43
+ }
44
+ function assetGlobal() {
45
+ return window.__webui;
46
+ }
47
+ function seedAssetStyleSet() {
48
+ if (assetStylesSeeded)
49
+ return;
50
+ assetStylesSeeded = true;
51
+ const styles = assetGlobal()?.styles;
52
+ if (!styles)
53
+ return;
54
+ for (let i = 0; i < styles.length; i++) {
55
+ injectedAssetStyles.add(styles[i]);
56
+ }
57
+ }
58
+ function parseImportMap(scriptMarkup) {
59
+ const trimmed = scriptMarkup.trim();
60
+ if (!trimmed.startsWith('<script type="importmap"')) {
61
+ throw new Error('[WebUI] Component asset templateStyles entry must be a <script type="importmap"> tag.');
62
+ }
63
+ const openTagEnd = trimmed.indexOf('>');
64
+ const closeTagStart = trimmed.lastIndexOf('</script>');
65
+ if (openTagEnd < 0 || closeTagStart <= openTagEnd) {
66
+ throw new Error('[WebUI] Component asset importmap tag is malformed.');
67
+ }
68
+ const parsed = JSON.parse(trimmed.substring(openTagEnd + 1, closeTagStart));
69
+ if (!parsed.imports || typeof parsed.imports !== 'object') {
70
+ throw new Error('[WebUI] Component asset importmap is missing an imports object.');
71
+ }
72
+ const imports = {};
73
+ const specifiers = Object.keys(parsed.imports);
74
+ for (let i = 0; i < specifiers.length; i++) {
75
+ const specifier = specifiers[i];
76
+ const uri = parsed.imports[specifier];
77
+ if (typeof uri !== 'string' || !uri.startsWith('data:text/css,')) {
78
+ throw new Error(`[WebUI] Component asset importmap entry "${specifier}" must be a data:text/css URI.`);
79
+ }
80
+ imports[specifier] = uri;
81
+ }
82
+ return imports;
83
+ }
@@ -1,30 +1,4 @@
1
- import { type CompiledConditionFn, type TemplateMeta } from './template.js';
2
- export interface ComponentAsset {
3
- type?: 'webui-component-asset';
4
- version?: number;
5
- components?: string[];
6
- templateStyles?: string[];
7
- templates?: Record<string, TemplateMeta>;
8
- templateFunctions?: Record<string, CompiledConditionFn[]>;
9
- }
10
- export type ComponentAssetState = Record<string, unknown>;
11
- export interface ComponentAssetManifestEntry<Data extends ComponentAssetState = ComponentAssetState> {
12
- asset: string | URL;
13
- module?: () => Promise<unknown>;
14
- data?: () => Promise<Data>;
15
- }
16
- export type ComponentAssetManifest = Record<string, ComponentAssetManifestEntry>;
17
- export interface ComponentAssetPreload<Data extends ComponentAssetState = ComponentAssetState> {
18
- asset: Promise<void>;
19
- module?: Promise<unknown>;
20
- data?: Promise<Data>;
21
- }
22
- export interface ComponentAssetCreateOptions {
23
- awaitData?: boolean;
24
- dataTimeoutMs?: number;
25
- }
26
- export interface ComponentAssetRegistry {
27
- preload<Data extends ComponentAssetState = ComponentAssetState>(tag: string): ComponentAssetPreload<Data>;
28
- create(tag: string, options?: ComponentAssetCreateOptions): Promise<HTMLElement>;
29
- }
1
+ import type { ComponentAssetManifest, ComponentAssetRegistry } from './component-asset/manifest.js';
2
+ export type { ComponentAsset, ComponentAssetImport, } from './component-asset/asset.js';
3
+ export type { ComponentAssetCreateOptions, ComponentAssetManifest, ComponentAssetManifestEntry, ComponentAssetPreload, ComponentAssetRegistry, ComponentAssetState, } from './component-asset/manifest.js';
30
4
  export declare function defineComponentAssets(manifest: ComponentAssetManifest): ComponentAssetRegistry;
@@ -1,12 +1,4 @@
1
- import { getTemplate, registerTemplateData, } from './template.js';
2
- const ASSET_TYPE = 'webui-component-asset';
3
- const ASSET_VERSION = 1;
4
- const injectedAssetStyles = new Set();
5
- const assetLoadPromises = new Map();
6
- let assetStylesSeeded = false;
7
- function assetGlobal() {
8
- return window.__webui;
9
- }
1
+ import { loadComponentAsset } from './component-asset/loader.js';
10
2
  export function defineComponentAssets(manifest) {
11
3
  const preloads = new Map();
12
4
  function preload(tag) {
@@ -85,129 +77,3 @@ function dataWithTimeout(data, timeoutMs) {
85
77
  }),
86
78
  ]);
87
79
  }
88
- function loadComponentAsset(tag, url) {
89
- if (getTemplate(tag))
90
- return Promise.resolve();
91
- const assetUrl = new URL(url, document.baseURI);
92
- const href = assetUrl.href;
93
- let promise = assetLoadPromises.get(href);
94
- if (promise)
95
- return promise;
96
- promise = importAndRegisterComponentAsset(assetUrl)
97
- .finally(() => {
98
- assetLoadPromises.delete(href);
99
- });
100
- assetLoadPromises.set(href, promise);
101
- return promise;
102
- }
103
- function registerComponentAsset(asset) {
104
- validateAsset(asset);
105
- if (asset.templates && templatesAlreadyRegistered(asset.templates))
106
- return;
107
- registerAssetStyles(asset.templateStyles, readNonce());
108
- if (asset.templates) {
109
- registerTemplateData(asset.templates, asset.templateFunctions);
110
- }
111
- }
112
- async function importAndRegisterComponentAsset(assetUrl) {
113
- const imported = await import(assetUrl.href);
114
- registerComponentAsset(readComponentAssetModule(imported));
115
- }
116
- function readComponentAssetModule(module) {
117
- if (!isObject(module) || !isObject(module.default)) {
118
- throw new Error('[WebUI] Component asset module must default-export an asset object.');
119
- }
120
- return module.default;
121
- }
122
- function isObject(value) {
123
- return typeof value === 'object' && value !== null;
124
- }
125
- function validateAsset(asset) {
126
- if (asset.type !== ASSET_TYPE) {
127
- throw new Error(`[WebUI] Invalid component asset type: ${String(asset.type)}`);
128
- }
129
- if (asset.version !== ASSET_VERSION) {
130
- throw new Error(`[WebUI] Unsupported component asset version: ${String(asset.version)}`);
131
- }
132
- }
133
- function templatesAlreadyRegistered(templates) {
134
- const tags = Object.keys(templates);
135
- if (tags.length === 0)
136
- return false;
137
- for (let i = 0; i < tags.length; i++) {
138
- if (!getTemplate(tags[i]))
139
- return false;
140
- }
141
- return true;
142
- }
143
- function readNonce() {
144
- const nonce = assetGlobal()?.nonce;
145
- if (nonce)
146
- return nonce;
147
- const meta = document.querySelector('meta[name="webui-nonce"]');
148
- return meta?.content ?? '';
149
- }
150
- function seedAssetStyleSet() {
151
- if (assetStylesSeeded)
152
- return;
153
- assetStylesSeeded = true;
154
- const styles = assetGlobal()?.styles;
155
- if (!styles)
156
- return;
157
- for (let i = 0; i < styles.length; i++) {
158
- injectedAssetStyles.add(styles[i]);
159
- }
160
- }
161
- function registerAssetStyles(templateStyles, nonce) {
162
- if (!templateStyles || templateStyles.length === 0)
163
- return;
164
- seedAssetStyleSet();
165
- for (let i = 0; i < templateStyles.length; i++) {
166
- const imports = parseImportMap(templateStyles[i]);
167
- const nextImports = {};
168
- let hasNewImport = false;
169
- const specifiers = Object.keys(imports);
170
- for (let j = 0; j < specifiers.length; j++) {
171
- const specifier = specifiers[j];
172
- if (injectedAssetStyles.has(specifier))
173
- continue;
174
- injectedAssetStyles.add(specifier);
175
- nextImports[specifier] = imports[specifier];
176
- hasNewImport = true;
177
- }
178
- if (!hasNewImport)
179
- continue;
180
- const script = document.createElement('script');
181
- script.type = 'importmap';
182
- if (nonce)
183
- script.nonce = nonce;
184
- script.textContent = JSON.stringify({ imports: nextImports });
185
- document.head.appendChild(script);
186
- }
187
- }
188
- function parseImportMap(scriptMarkup) {
189
- const trimmed = scriptMarkup.trim();
190
- if (!trimmed.startsWith('<script')) {
191
- throw new Error('[WebUI] Component asset templateStyles entry must be a <script type="importmap"> tag.');
192
- }
193
- const openTagEnd = trimmed.indexOf('>');
194
- const closeTagStart = trimmed.lastIndexOf('</script>');
195
- if (openTagEnd < 0 || closeTagStart <= openTagEnd) {
196
- throw new Error('[WebUI] Component asset importmap tag is malformed.');
197
- }
198
- const parsed = JSON.parse(trimmed.substring(openTagEnd + 1, closeTagStart));
199
- if (!parsed.imports || typeof parsed.imports !== 'object') {
200
- throw new Error('[WebUI] Component asset importmap is missing an imports object.');
201
- }
202
- const imports = {};
203
- const specifiers = Object.keys(parsed.imports);
204
- for (let i = 0; i < specifiers.length; i++) {
205
- const specifier = specifiers[i];
206
- const uri = parsed.imports[specifier];
207
- if (typeof uri !== 'string' || !uri.startsWith('data:text/css,')) {
208
- throw new Error(`[WebUI] Component asset importmap entry "${specifier}" must be a data:text/css URI.`);
209
- }
210
- imports[specifier] = uri;
211
- }
212
- return imports;
213
- }
@@ -5,7 +5,7 @@ export function activateRootsBetween(startMarker, endMarker, state, updates) {
5
5
  if (!root) {
6
6
  throw new Error('boundary start marker was detached before activation');
7
7
  }
8
- const failure = activateDeferredTree(startMarker.nextSibling, root, endMarker, state, updates ? { updates, retainedRoots: updates.roots } : undefined);
8
+ const failure = activateDeferredTree(startMarker.nextSibling, root, endMarker, state, updates ? { updates, countRetention: true } : undefined);
9
9
  if (!failure)
10
10
  return;
11
11
  abandonDeferredRange(startMarker, endMarker);
@@ -5,6 +5,7 @@ import { abandonDeferredDocumentRoots, abandonDeferredRange, } from './streaming
5
5
  import { abandonPendingWaiters, configureStreamingFailureHandler, elementHasPendingStateForTests, pendingTagWaiterCountForTests, pendingUndefinedRootCountForTests, resetDeferredActivationForTests, } from './streaming-deferred.js';
6
6
  import { findBoundaryScript, findEndMarkerByPrefix, findStartMarkerByPrefix, removeBoundaryScaffolding, resolveBoundaryRange, streamingErrorMessage, } from './streaming-dom.js';
7
7
  import { parseBoundaryEnvelope, RECORD_KIND_STATE_UPDATE, RECORD_KIND_TERMINAL, RECORD_KIND_UPDATABLE_CHECKPOINT, } from './streaming-protocol.js';
8
+ import { applyStateUpdate } from './streaming-state.js';
8
9
  const MAX_QUEUED_BOUNDARIES = 512;
9
10
  const MAX_UPDATABLE_BOUNDARIES = 128;
10
11
  const MAX_RETAINED_UPDATE_ROOTS = 50_000;
@@ -201,14 +202,18 @@ function commitCheckpoint(bootstrap, range, sequence, target, updatable, sentine
201
202
  applyBoundaryBootstrap(bootstrap);
202
203
  if (range.start && range.end) {
203
204
  const boundary = updatable
204
- ? { roots: [], pendingRoots: 0 }
205
+ ? { roots: [], retained: 0, pendingRoots: 0 }
205
206
  : undefined;
206
207
  activateRootsBetween(range.start, range.end, bootstrap.state, boundary);
207
208
  if (boundary)
208
209
  retainUpdatableBoundary(target, boundary);
209
210
  }
210
211
  else if (updatable) {
211
- retainUpdatableBoundary(target, { roots: [], pendingRoots: 0 });
212
+ retainUpdatableBoundary(target, {
213
+ roots: [],
214
+ retained: 0,
215
+ pendingRoots: 0,
216
+ });
212
217
  }
213
218
  committed = true;
214
219
  }
@@ -227,11 +232,11 @@ function retainUpdatableBoundary(target, boundary) {
227
232
  if (updatableBoundaries.size >= MAX_UPDATABLE_BOUNDARIES) {
228
233
  throw new Error(`updatable boundary count exceeds ${MAX_UPDATABLE_BOUNDARIES}`);
229
234
  }
230
- if (retainedUpdateRoots + boundary.roots.length >
235
+ if (retainedUpdateRoots + boundary.retained >
231
236
  MAX_RETAINED_UPDATE_ROOTS) {
232
237
  throw new Error(`retained update root count exceeds ${MAX_RETAINED_UPDATE_ROOTS}`);
233
238
  }
234
- retainedUpdateRoots += boundary.roots.length;
239
+ retainedUpdateRoots += boundary.retained;
235
240
  updatableBoundaries.set(target, boundary);
236
241
  }
237
242
  function commitStateUpdate(boundary, patch, target, sequence, sentinel, scriptEl) {
@@ -246,16 +251,8 @@ function commitStateUpdate(boundary, patch, target, sequence, sentinel, scriptEl
246
251
  }
247
252
  for (let i = 0; i < boundary.roots.length; i++) {
248
253
  const root = boundary.roots[i];
249
- if (root.hasAttribute('data-ws'))
250
- continue;
251
- try {
252
- if (typeof root.setState !== 'function') {
253
- throw new Error('no setState() method');
254
- }
255
- root.setState(patch);
256
- }
257
- catch (error) {
258
- console.error(`[WebUI] streaming: state update failed for <${root.tagName.toLowerCase()}>: ${streamingErrorMessage(error)}`);
254
+ if (!applyStateUpdate(root, patch)) {
255
+ throw new Error(`<${root.tagName.toLowerCase()}> activated without a setState() method`);
259
256
  }
260
257
  }
261
258
  notifyCommit(`${BOUNDARY_MARK_PREFIX}${target}${UPDATE_MARK_SUFFIX}`, sequence, 'update');
@@ -5,15 +5,15 @@ export declare const ELEMENT_LIMIT_FAILURE = 5;
5
5
  export declare const MAX_PENDING_UNDEFINED_ROOTS = 50000;
6
6
  export interface PendingBoundaryUpdates {
7
7
  readonly roots: Element[];
8
+ retained: number;
8
9
  pendingRoots: number;
9
10
  patch?: Record<string, unknown>;
10
11
  }
11
12
  export interface DeferredActivationOptions {
12
13
  updates?: PendingBoundaryUpdates;
13
- retainedRoots?: Element[];
14
+ countRetention?: boolean;
14
15
  }
15
16
  export declare function configureStreamingFailureHandler(handler: (reason: string) => void): void;
16
- export declare function activateElement(el: Element, state: Record<string, unknown> | undefined, updates?: PendingBoundaryUpdates): number;
17
17
  export declare function activateDeferredTree(first: Node | null, root: Node, end: Node | null, state: Record<string, unknown> | undefined, options?: DeferredActivationOptions): string | null;
18
18
  export declare function abandonPendingWaiters(): void;
19
19
  export declare function resetDeferredActivationForTests(): void;
@@ -2,6 +2,7 @@ import { markLateActivationPending, settleLateActivation, } from './lifecycle.js
2
2
  import { abandonDeferredDescendants, abandonDeferredElement, } from './streaming-cleanup.js';
3
3
  import { firstNodeWithin, MAX_ELEMENTS_PER_BOUNDARY, MAX_MARKER_SCAN_NODES, nextAfterSubtreeWithin, nextWithinRoot, safeRemoveAttribute, streamingErrorMessage, } from './streaming-dom.js';
4
4
  import { PENDING_ROOT_CONNECTED, STREAMED_HOST_ATTR, STREAMING_BOUNDARY_ACTIVATE, } from './streaming-mode.js';
5
+ import { applyStateUpdate } from './streaming-state.js';
5
6
  const ACTIVATION_ACTIVATED = 1;
6
7
  const ACTIVATION_STATIC_HOST_OPT_OUT = 2;
7
8
  export const ACTIVATION_MISSING_TEMPLATE = 3;
@@ -25,9 +26,12 @@ function fail(reason) {
25
26
  }
26
27
  failureHandler(reason);
27
28
  }
28
- export function activateElement(el, state, updates) {
29
- if (!el.hasAttribute(STREAMED_HOST_ATTR))
30
- return ELEMENT_IGNORED;
29
+ function requireStateUpdate(el, patch) {
30
+ if (applyStateUpdate(el, patch))
31
+ return;
32
+ fail(`<${el.tagName.toLowerCase()}> activated without a setState() method`);
33
+ }
34
+ function activateMarkedElement(el, state, updates) {
31
35
  const tag = el.tagName.toLowerCase();
32
36
  if (tag.indexOf('-') === -1)
33
37
  return ELEMENT_IGNORED;
@@ -99,7 +103,7 @@ function activatePendingRoot(tag, waiter, el) {
99
103
  let updates;
100
104
  try {
101
105
  updates = takePendingUpdates(el);
102
- const state = mergePendingUpdates(takePendingState(el), updates);
106
+ const state = takePendingState(el);
103
107
  const outcome = invokeActivationHook(el, state);
104
108
  if (outcome === ACTIVATION_MISSING_TEMPLATE) {
105
109
  abandonDeferredDescendants(el);
@@ -107,6 +111,11 @@ function activatePendingRoot(tag, waiter, el) {
107
111
  fail(`template metadata missing while activating <${tag}>`);
108
112
  return;
109
113
  }
114
+ if (updates) {
115
+ updates.roots.push(el);
116
+ if (updates.patch)
117
+ requireStateUpdate(el, updates.patch);
118
+ }
110
119
  const failure = activateDeferredTree(firstNodeWithin(el), el, null, state, updates ? { updates } : undefined);
111
120
  if (failure)
112
121
  fail(failure);
@@ -126,6 +135,8 @@ function activatePendingRoot(tag, waiter, el) {
126
135
  }
127
136
  }
128
137
  export function activateDeferredTree(first, root, end, state, options) {
138
+ const updates = options?.updates;
139
+ const countRetention = options?.countRetention === true && updates !== undefined;
129
140
  let node = first;
130
141
  let resumeAfterDeferred = null;
131
142
  let skippingDeferredDescendants = false;
@@ -136,15 +147,14 @@ export function activateDeferredTree(first, root, end, state, options) {
136
147
  return `streaming boundary walk exceeds ${MAX_MARKER_SCAN_NODES} nodes`;
137
148
  }
138
149
  visited++;
139
- if (node.nodeType === 1) {
140
- const el = node;
141
- if (options?.retainedRoots &&
142
- el.hasAttribute(STREAMED_HOST_ATTR)) {
143
- if (options.retainedRoots.length >= MAX_ELEMENTS_PER_BOUNDARY) {
144
- return `updatable streaming boundary exceeds ${MAX_ELEMENTS_PER_BOUNDARY} roots`;
145
- }
146
- options.retainedRoots.push(el);
150
+ const isElement = node.nodeType === 1;
151
+ const marked = isElement &&
152
+ node.hasAttribute(STREAMED_HOST_ATTR);
153
+ if (countRetention && marked) {
154
+ if (updates.retained >= MAX_ELEMENTS_PER_BOUNDARY) {
155
+ return `updatable streaming boundary exceeds ${MAX_ELEMENTS_PER_BOUNDARY} roots`;
147
156
  }
157
+ updates.retained++;
148
158
  }
149
159
  if (skippingDeferredDescendants &&
150
160
  node !== resumeAfterDeferred) {
@@ -152,27 +162,38 @@ export function activateDeferredTree(first, root, end, state, options) {
152
162
  continue;
153
163
  }
154
164
  skippingDeferredDescendants = false;
155
- if (node.nodeType === 1) {
165
+ if (isElement) {
156
166
  if (elements >= MAX_ELEMENTS_PER_BOUNDARY) {
157
167
  return `streaming boundary exceeds ${MAX_ELEMENTS_PER_BOUNDARY} elements`;
158
168
  }
159
169
  elements++;
160
- try {
161
- const outcome = activateElement(node, state, options?.updates);
162
- if (outcome === ACTIVATION_MISSING_TEMPLATE) {
163
- return `template metadata missing while activating <${node.tagName.toLowerCase()}>`;
164
- }
165
- if (outcome === ELEMENT_LIMIT_FAILURE) {
166
- return `pending undefined root count exceeds ${MAX_PENDING_UNDEFINED_ROOTS}`;
170
+ if (marked) {
171
+ const el = node;
172
+ try {
173
+ const outcome = activateMarkedElement(el, state, updates);
174
+ if (outcome === ACTIVATION_MISSING_TEMPLATE) {
175
+ return `template metadata missing while activating <${el.tagName.toLowerCase()}>`;
176
+ }
177
+ if (outcome === ELEMENT_LIMIT_FAILURE) {
178
+ return `pending undefined root count exceeds ${MAX_PENDING_UNDEFINED_ROOTS}`;
179
+ }
180
+ if (outcome === ELEMENT_DEFERRED) {
181
+ resumeAfterDeferred = nextAfterSubtreeWithin(node, root);
182
+ skippingDeferredDescendants = true;
183
+ }
184
+ else if (updates &&
185
+ (outcome === ACTIVATION_ACTIVATED ||
186
+ outcome === ACTIVATION_STATIC_HOST_OPT_OUT)) {
187
+ updates.roots.push(el);
188
+ if (updates.patch) {
189
+ requireStateUpdate(el, updates.patch);
190
+ }
191
+ }
167
192
  }
168
- if (outcome === ELEMENT_DEFERRED) {
169
- resumeAfterDeferred = nextAfterSubtreeWithin(node, root);
170
- skippingDeferredDescendants = true;
193
+ catch (error) {
194
+ reportActivationFailure(el.tagName.toLowerCase(), error);
171
195
  }
172
196
  }
173
- catch (error) {
174
- reportActivationFailure(node.tagName.toLowerCase(), error);
175
- }
176
197
  }
177
198
  node = nextWithinRoot(node, root);
178
199
  }
@@ -232,13 +253,6 @@ function takePendingUpdates(el) {
232
253
  updates.pendingRoots--;
233
254
  return updates;
234
255
  }
235
- function mergePendingUpdates(state, updates) {
236
- if (!updates?.patch)
237
- return state;
238
- return state
239
- ? { ...state, ...updates.patch }
240
- : { ...updates.patch };
241
- }
242
256
  function invokeActivationHook(el, state) {
243
257
  const hook = el[STREAMING_BOUNDARY_ACTIVATE];
244
258
  if (typeof hook !== 'function')
@@ -0,0 +1 @@
1
+ export declare function applyStateUpdate(el: Element, patch: Record<string, unknown>): boolean;
@@ -0,0 +1,13 @@
1
+ import { streamingErrorMessage } from './streaming-dom.js';
2
+ export function applyStateUpdate(el, patch) {
3
+ const root = el;
4
+ if (typeof root.setState !== 'function')
5
+ return false;
6
+ try {
7
+ root.setState(patch);
8
+ }
9
+ catch (error) {
10
+ console.error(`[WebUI] streaming: state update failed for <${root.tagName.toLowerCase()}>: ${streamingErrorMessage(error)}`);
11
+ }
12
+ return true;
13
+ }
@@ -48,6 +48,6 @@ export interface TemplateMeta extends TemplateBlockMeta {
48
48
  re?: [string, string, CompiledEventArgs][];
49
49
  tr?: string[];
50
50
  ta?: string[];
51
- sd?: boolean;
51
+ sd?: 1;
52
52
  th?: boolean | 1;
53
53
  }
@@ -14,4 +14,5 @@ declare global {
14
14
  export declare function getTemplate(name: string): TemplateMeta | undefined;
15
15
  export declare function getTemplateRegistry(): Record<string, TemplateMeta> | undefined;
16
16
  export declare function registerTemplateData(templates: Record<string, TemplateMeta>, templateFns?: Record<string, CompiledConditionFn[]>): void;
17
+ export declare function prepareAssetTemplateData(templates: Record<string, TemplateMeta>, templateFns?: Record<string, CompiledConditionFn[]>): void;
17
18
  export declare function deferTemplateDefinition(tagName: string, ctor: Function, define: () => void): void;
package/dist/template.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { dispatchTemplatesRegistered, templateRegistrationDetail, TEMPLATES_REGISTERED_EVENT, } from './template-events.js';
2
2
  const WEBUI_DATA_ID = 'webui-data';
3
3
  const normalizedTemplates = new WeakSet();
4
+ const assetNormalizedTemplates = new WeakSet();
4
5
  let webuiDataLoaded = false;
5
6
  const pendingTemplateDefinitions = new Map();
6
7
  function acceptTemplateData(templates, templateFns) {
8
+ prepareTemplateData(templates, templateFns);
7
9
  const w = window;
8
10
  if (!w.__webui)
9
11
  w.__webui = {};
@@ -21,9 +23,7 @@ function acceptTemplateData(templates, templateFns) {
21
23
  const names = Object.keys(templates);
22
24
  for (let i = 0; i < names.length; i++) {
23
25
  const tag = names[i];
24
- const meta = templates[tag];
25
- w.__webui.templates[tag] = meta;
26
- normalizeTemplate(tag, meta);
26
+ w.__webui.templates[tag] = templates[tag];
27
27
  }
28
28
  for (let i = 0; i < names.length; i++) {
29
29
  const pending = pendingTemplateDefinitions.get(names[i]);
@@ -60,6 +60,25 @@ export function registerTemplateData(templates, templateFns) {
60
60
  dispatchTemplatesRegistered(templates);
61
61
  }
62
62
  }
63
+ function prepareTemplateData(templates, templateFns) {
64
+ const names = Object.keys(templates);
65
+ for (let i = 0; i < names.length; i++) {
66
+ const name = names[i];
67
+ normalizeTemplate(name, templates[name], templateFns?.[name]);
68
+ }
69
+ }
70
+ export function prepareAssetTemplateData(templates, templateFns) {
71
+ const names = Object.keys(templates);
72
+ for (let i = 0; i < names.length; i++) {
73
+ const name = names[i];
74
+ const meta = templates[name];
75
+ if (assetNormalizedTemplates.has(meta))
76
+ continue;
77
+ normalizeTemplateConditions(name, meta, templateFns?.[name] ?? []);
78
+ normalizedTemplates.add(meta);
79
+ assetNormalizedTemplates.add(meta);
80
+ }
81
+ }
63
82
  export function deferTemplateDefinition(tagName, ctor, define) {
64
83
  const pending = pendingTemplateDefinitions.get(tagName);
65
84
  if (pending) {
@@ -89,10 +108,14 @@ function loadWebUIDataBlock() {
89
108
  el.remove();
90
109
  webuiDataLoaded = true;
91
110
  }
92
- function normalizeTemplate(name, meta) {
111
+ function normalizeTemplate(name, meta, suppliedFns) {
93
112
  if (normalizedTemplates.has(meta))
94
113
  return;
95
- const fns = window.__webui?.templateFns?.[name] ?? [];
114
+ const fns = suppliedFns ?? window.__webui?.templateFns?.[name] ?? [];
115
+ normalizeTemplateConditions(name, meta, fns);
116
+ normalizedTemplates.add(meta);
117
+ }
118
+ function normalizeTemplateConditions(name, meta, fns) {
96
119
  const stack = [meta];
97
120
  while (stack.length > 0) {
98
121
  const block = stack.pop();
@@ -116,7 +139,6 @@ function normalizeTemplate(name, meta) {
116
139
  stack.push(children[i]);
117
140
  }
118
141
  }
119
- normalizedTemplates.add(meta);
120
142
  }
121
143
  function normalizeCondition(tagName, condition, fns) {
122
144
  const first = condition[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/webui-framework",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "type": "module",
5
5
  "description": "WebUI Framework Next — Preact-inspired lightweight Web Component runtime with SSR hydration and compiled-template path mapping.",
6
6
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "@playwright/test": "^1.61.1",
32
32
  "@types/node": "^26.1.0",
33
33
  "typescript": "^6.0.3",
34
- "@microsoft/webui-test-support": "0.0.20"
34
+ "@microsoft/webui-test-support": "0.0.22"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsc",