@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 +12 -6
- package/dist/component-asset/asset.d.ts +22 -0
- package/dist/component-asset/asset.js +171 -0
- package/dist/component-asset/loader.d.ts +1 -0
- package/dist/component-asset/loader.js +104 -0
- package/dist/component-asset/manifest.d.ts +20 -0
- package/dist/component-asset/manifest.js +1 -0
- package/dist/component-asset/resources.d.ts +4 -0
- package/dist/component-asset/resources.js +83 -0
- package/dist/component-asset.d.ts +3 -29
- package/dist/component-asset.js +1 -135
- package/dist/streaming-activation.js +1 -1
- package/dist/streaming-coordinator.js +11 -14
- package/dist/streaming-deferred.d.ts +2 -2
- package/dist/streaming-deferred.js +47 -33
- package/dist/streaming-state.d.ts +1 -0
- package/dist/streaming-state.js +13 -0
- package/dist/template-types.d.ts +1 -1
- package/dist/template.d.ts +1 -0
- package/dist/template.js +28 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -257,11 +257,17 @@ export const settingsAssets = defineComponentAssets({
|
|
|
257
257
|
});
|
|
258
258
|
```
|
|
259
259
|
|
|
260
|
-
The asset
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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?:
|
|
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 {
|
|
2
|
-
export
|
|
3
|
-
|
|
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;
|
package/dist/component-asset.js
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import {
|
|
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,
|
|
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, {
|
|
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.
|
|
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.
|
|
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
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
if (
|
|
30
|
-
return
|
|
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 =
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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 (
|
|
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
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
+
}
|
package/dist/template-types.d.ts
CHANGED
package/dist/template.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
34
|
+
"@microsoft/webui-test-support": "0.0.22"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsc",
|