@microsoft/webui-framework 0.0.19 → 0.0.21
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 +73 -22
- 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/element.d.ts +0 -4
- package/dist/element.js +8 -67
- package/dist/lifecycle.d.ts +17 -0
- package/dist/lifecycle.js +66 -3
- package/dist/static-host.js +4 -2
- package/dist/streaming-activation.d.ts +2 -0
- package/dist/streaming-activation.js +13 -0
- package/dist/streaming-bootstrap.d.ts +2 -0
- package/dist/streaming-bootstrap.js +48 -0
- package/dist/streaming-cleanup.d.ts +4 -0
- package/dist/streaming-cleanup.js +66 -0
- package/dist/streaming-coordinator.d.ts +7 -0
- package/dist/streaming-coordinator.js +364 -0
- package/dist/streaming-deferred.d.ts +22 -0
- package/dist/streaming-deferred.js +287 -0
- package/dist/streaming-dom.d.ts +32 -0
- package/dist/streaming-dom.js +148 -0
- package/dist/streaming-entry.d.ts +1 -0
- package/dist/streaming-entry.js +2 -0
- package/dist/streaming-install.d.ts +2 -0
- package/dist/streaming-install.js +25 -0
- package/dist/streaming-mode.d.ts +5 -0
- package/dist/streaming-mode.js +14 -0
- package/dist/streaming-protocol.d.ts +32 -0
- package/dist/streaming-protocol.js +27 -0
- package/dist/streaming-state.d.ts +1 -0
- package/dist/streaming-state.js +13 -0
- package/dist/streaming.d.ts +5 -0
- package/dist/streaming.js +9 -0
- package/dist/template-element.d.ts +9 -1
- package/dist/template-element.js +112 -42
- package/dist/template-types.d.ts +1 -1
- package/dist/template.d.ts +2 -0
- package/dist/template.js +74 -25
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -126,6 +126,36 @@ The plugin alone preserves full server state. To emit exact `@observable` and
|
|
|
126
126
|
`--projection-manifest`. The manifest tooling is build-only; this runtime
|
|
127
127
|
package does not depend on esbuild or TypeScript.
|
|
128
128
|
|
|
129
|
+
### Progressive streaming hydration
|
|
130
|
+
|
|
131
|
+
Streaming applications opt into a separate side-effect entry:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import '@microsoft/webui-framework/streaming.js';
|
|
135
|
+
import './counter-card.js';
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Import it before component registration modules and load the application entry
|
|
139
|
+
early with `<script type="module" async>` in `<head>`. The server must render
|
|
140
|
+
authored `<boundary>` directives through
|
|
141
|
+
`WebUIHandler::render_streaming`. The default
|
|
142
|
+
`@microsoft/webui-framework` entry has no dependency on the coordinator, so
|
|
143
|
+
normal applications pay no streaming bundle or initialization cost.
|
|
144
|
+
|
|
145
|
+
Each committed boundary receives its own ephemeral state object directly during
|
|
146
|
+
activation. The coordinator does not publish that state to
|
|
147
|
+
`window.__webui.state`, and it removes generated checkpoint scaffolding after
|
|
148
|
+
commit. Every commit also emits a `performance.mark()` — `webui:boundary:<id>`,
|
|
149
|
+
`webui:boundary:<id>:update`, or `webui:streaming:terminal` — which needs no
|
|
150
|
+
flag and no listener, so tooling that loads after hydration can still read it.
|
|
151
|
+
Set `window.__WEBUI_STREAMING_DEBUG__ = true` only when tooling needs the live
|
|
152
|
+
`webui:boundary-hydrated` event as well.
|
|
153
|
+
|
|
154
|
+
Set `window.__WEBUI_STREAMING_SLICE_MS__` to a positive millisecond budget to
|
|
155
|
+
make the coordinator yield between boundaries instead of draining its queue in
|
|
156
|
+
one pass. That is for pages where an intermediary coalesces the response into a
|
|
157
|
+
single chunk; it costs total hydration time, so leave it unset otherwise.
|
|
158
|
+
|
|
129
159
|
### Property binding lifecycle
|
|
130
160
|
|
|
131
161
|
Property bindings use the `:` prefix to pass values directly to child DOM properties:
|
|
@@ -145,18 +175,26 @@ re-render it. An `@observable` written before hydration finishes — in a field
|
|
|
145
175
|
initializer, the `constructor`, or before `super.connectedCallback()` — cannot
|
|
146
176
|
update that DOM, so the write is dropped and the runtime logs a
|
|
147
177
|
`[WebUI] Hydration mismatch` warning naming the properties. Seed such values in
|
|
148
|
-
the SSR state, or assign them
|
|
178
|
+
the SSR state, or assign them from `hydratedCallback()`. The warning is
|
|
149
179
|
development-only and is dead-code-eliminated from production bundles via the
|
|
150
180
|
`__WEBUI_DEV__` compile-time flag (on by default; `webui-press build` sets it to
|
|
151
181
|
`false`). See the
|
|
152
182
|
[Interactivity Guide](https://microsoft.github.io/webui/guide/concepts/interactivity#setting-observable-state-during-setup).
|
|
153
183
|
|
|
154
|
-
`
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
184
|
+
Override the protected `hydratedCallback()` hook for work that requires the
|
|
185
|
+
component's bindings, events, and `w-ref` references to be ready. It runs
|
|
186
|
+
synchronously exactly once after the first successful ordinary SSR hydration,
|
|
187
|
+
client-created mount, deferred streamed activation, or dormant static-host wake.
|
|
188
|
+
Its once-latch is set before author code runs, so a thrown callback is not
|
|
189
|
+
retried on reconnect.
|
|
190
|
+
|
|
191
|
+
`connectedCallback()` remains a native per-connection lifecycle. On ordinary
|
|
192
|
+
SSR and client-created mounts, `super.connectedCallback()` hydrates
|
|
193
|
+
synchronously, but a streamed `data-ws` root returns while still deferred and
|
|
194
|
+
hydrates only when its boundary commits. Therefore `connectedCallback()` cannot
|
|
195
|
+
be used as a universal post-hydration signal. Descendants must not structurally
|
|
196
|
+
mutate a containing component's SSR subtree before it hydrates, because
|
|
197
|
+
hydration relies on stable compiled paths.
|
|
160
198
|
|
|
161
199
|
### DOM strategy (`--dom`)
|
|
162
200
|
|
|
@@ -186,6 +224,7 @@ Base class for framework components.
|
|
|
186
224
|
| Member | Purpose |
|
|
187
225
|
|--------|---------|
|
|
188
226
|
| `static define(tagName)` | Register the class as a custom element |
|
|
227
|
+
| `protected hydratedCallback()` | Run once after the first successful hydration or client mount |
|
|
189
228
|
| `$emit(name, detail?)` | Dispatch a bubbling, composed `CustomEvent` |
|
|
190
229
|
| `$update()` | Force a reactive update (normally called automatically) |
|
|
191
230
|
| `disconnectedCallback()` | Override for cleanup (global listeners, etc.) |
|
|
@@ -218,11 +257,17 @@ export const settingsAssets = defineComponentAssets({
|
|
|
218
257
|
});
|
|
219
258
|
```
|
|
220
259
|
|
|
221
|
-
The asset
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
226
271
|
`create(tag, { awaitData: true, dataTimeoutMs: 150 })` only when a component must
|
|
227
272
|
wait briefly for state before mounting.
|
|
228
273
|
|
|
@@ -360,9 +405,9 @@ resource-constrained devices.
|
|
|
360
405
|
|
|
361
406
|
5. **Single-pass hydration via path mapping.**
|
|
362
407
|
SSR DOM is matched to compiled template bindings through
|
|
363
|
-
template-parallel traversal (`$resolveSSR`).
|
|
364
|
-
data attributes
|
|
365
|
-
touches each DOM node exactly once.
|
|
408
|
+
template-parallel traversal (`$resolveSSR`). Ordinary buffered hydration
|
|
409
|
+
needs no marker comments or data attributes for binding resolution. The
|
|
410
|
+
hydration walk touches each DOM node exactly once.
|
|
366
411
|
|
|
367
412
|
6. **Keep the framework out of the GC's way.**
|
|
368
413
|
Fewer JS objects = fewer GC pauses. Binding arrays are pre-built at
|
|
@@ -424,7 +469,10 @@ Angular all require a JavaScript runtime on the server. This framework's SSR
|
|
|
424
469
|
is driven by data (template metadata + state values), not code. Any language
|
|
425
470
|
that can read the compiled metadata and produce HTML can serve as the SSR
|
|
426
471
|
backend. No comment markers or data attributes are needed — the runtime
|
|
427
|
-
resolves SSR
|
|
472
|
+
resolves ordinary buffered SSR nodes via template-parallel path traversal.
|
|
473
|
+
Progressive streaming uses temporary checkpoint scaffolding only to delay
|
|
474
|
+
activation until a complete region arrives; it removes that scaffolding after
|
|
475
|
+
commit.
|
|
428
476
|
|
|
429
477
|
### Build → Serve → Hydrate → Update
|
|
430
478
|
|
|
@@ -490,8 +538,8 @@ When the server renders a component, it emits HTML content (as a declarative
|
|
|
490
538
|
shadow root or as light DOM children) along with an inert `#webui-data`
|
|
491
539
|
JSON payload. The browser parses this DOM before any JavaScript runs.
|
|
492
540
|
When the component's JS loads and `connectedCallback` fires, the framework
|
|
493
|
-
uses compiled template paths to resolve SSR DOM nodes without
|
|
494
|
-
|
|
541
|
+
uses compiled template paths to resolve ordinary buffered SSR DOM nodes without
|
|
542
|
+
binding markers:
|
|
495
543
|
|
|
496
544
|
```mermaid
|
|
497
545
|
sequenceDiagram
|
|
@@ -511,6 +559,7 @@ sequenceDiagram
|
|
|
511
559
|
FW->>FW: $resolveSSR() — match SSR nodes via ordinal traversal
|
|
512
560
|
FW->>FW: $wireEvents() + $wireRefs()
|
|
513
561
|
FW->>FW: $buildPathIndex(), $ready = true
|
|
562
|
+
FW->>CE: hydratedCallback() (once)
|
|
514
563
|
Note over FW: DOM is already correct from SSR.<br/>No $update() call needed.
|
|
515
564
|
```
|
|
516
565
|
|
|
@@ -535,6 +584,7 @@ sequenceDiagram
|
|
|
535
584
|
FW->>FW: $wireEvents() + $wireRefs()
|
|
536
585
|
FW->>FW: $buildPathIndex(), $ready = true
|
|
537
586
|
FW->>FW: $update() — flush initial property values
|
|
587
|
+
FW->>CE: hydratedCallback() (once)
|
|
538
588
|
```
|
|
539
589
|
|
|
540
590
|
---
|
|
@@ -558,7 +608,7 @@ interface TemplateMeta {
|
|
|
558
608
|
eg?: [event, [[handler, argSpecs, targetPath, usesEvent?]]][]; // Events
|
|
559
609
|
b?: TemplateBlockMeta[]; // Nested block metadata
|
|
560
610
|
sa?: string; // Adopted stylesheet specifier
|
|
561
|
-
sd?:
|
|
611
|
+
sd?: 1; // Shadow DOM flag for client-created
|
|
562
612
|
re?: [event, handler, argSpecs][]; // Root-level events
|
|
563
613
|
tr?: string[]; // Template state roots
|
|
564
614
|
ta?: string[]; // Host attributes aligned with tr
|
|
@@ -734,10 +784,11 @@ stylesheet specifier for a component.
|
|
|
734
784
|
|
|
735
785
|
## Path-Based Binding Resolution
|
|
736
786
|
|
|
737
|
-
Unlike frameworks that use comment markers or data attributes to locate
|
|
738
|
-
dynamic
|
|
787
|
+
Unlike frameworks that use comment markers or data attributes to locate each
|
|
788
|
+
dynamic binding, this framework uses **compiled template paths** — arrays of
|
|
739
789
|
child-node indices that describe exactly where each binding lives in the DOM
|
|
740
|
-
tree.
|
|
790
|
+
tree. Progressive streaming's temporary boundary markers locate complete
|
|
791
|
+
activation regions, not individual bindings.
|
|
741
792
|
|
|
742
793
|
### Client-created resolution (`$resolve`)
|
|
743
794
|
|
|
@@ -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;
|