@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.
Files changed (46) hide show
  1. package/README.md +73 -22
  2. package/dist/component-asset/asset.d.ts +22 -0
  3. package/dist/component-asset/asset.js +171 -0
  4. package/dist/component-asset/loader.d.ts +1 -0
  5. package/dist/component-asset/loader.js +104 -0
  6. package/dist/component-asset/manifest.d.ts +20 -0
  7. package/dist/component-asset/manifest.js +1 -0
  8. package/dist/component-asset/resources.d.ts +4 -0
  9. package/dist/component-asset/resources.js +83 -0
  10. package/dist/component-asset.d.ts +3 -29
  11. package/dist/component-asset.js +1 -135
  12. package/dist/element.d.ts +0 -4
  13. package/dist/element.js +8 -67
  14. package/dist/lifecycle.d.ts +17 -0
  15. package/dist/lifecycle.js +66 -3
  16. package/dist/static-host.js +4 -2
  17. package/dist/streaming-activation.d.ts +2 -0
  18. package/dist/streaming-activation.js +13 -0
  19. package/dist/streaming-bootstrap.d.ts +2 -0
  20. package/dist/streaming-bootstrap.js +48 -0
  21. package/dist/streaming-cleanup.d.ts +4 -0
  22. package/dist/streaming-cleanup.js +66 -0
  23. package/dist/streaming-coordinator.d.ts +7 -0
  24. package/dist/streaming-coordinator.js +364 -0
  25. package/dist/streaming-deferred.d.ts +22 -0
  26. package/dist/streaming-deferred.js +287 -0
  27. package/dist/streaming-dom.d.ts +32 -0
  28. package/dist/streaming-dom.js +148 -0
  29. package/dist/streaming-entry.d.ts +1 -0
  30. package/dist/streaming-entry.js +2 -0
  31. package/dist/streaming-install.d.ts +2 -0
  32. package/dist/streaming-install.js +25 -0
  33. package/dist/streaming-mode.d.ts +5 -0
  34. package/dist/streaming-mode.js +14 -0
  35. package/dist/streaming-protocol.d.ts +32 -0
  36. package/dist/streaming-protocol.js +27 -0
  37. package/dist/streaming-state.d.ts +1 -0
  38. package/dist/streaming-state.js +13 -0
  39. package/dist/streaming.d.ts +5 -0
  40. package/dist/streaming.js +9 -0
  41. package/dist/template-element.d.ts +9 -1
  42. package/dist/template-element.js +112 -42
  43. package/dist/template-types.d.ts +1 -1
  44. package/dist/template.d.ts +2 -0
  45. package/dist/template.js +74 -25
  46. 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 after `super.connectedCallback()`. The warning is
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
- `super.connectedCallback()` is the synchronous hydration boundary for an
155
- authored component. When it returns, bindings, events, and `w-ref` references
156
- are wired. Use a parser-inserted, non-async ES module script or a classic
157
- `defer` script. A blocking classic script must follow every SSR instance it may
158
- upgrade. Descendants must not structurally mutate a containing component's SSR
159
- subtree before it hydrates, because hydration relies on stable compiled paths.
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 module carries the component's template and style payload. Use
222
- `preload(tag)` to start template, module, and optional data work early, then
223
- `create(tag)` to create the element after template/module work is ready.
224
- Concurrent asset requests share one in-flight load and CSS module styles are
225
- 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
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`). No marker comments, no
364
- data attributes — just path-based node resolution. The hydration walk
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 DOM nodes via template-parallel path traversal.
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 any marker
494
- comments or data attributes:
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?: boolean; // Shadow DOM flag for client-created
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 content, this framework uses **compiled template paths** — arrays of
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 { 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;