@microsoft/webui-framework 0.0.19 → 0.0.20

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.
@@ -0,0 +1,148 @@
1
+ export const BOUNDARY_SCRIPT_ATTR = 'data-webui-boundary';
2
+ export const MAX_ELEMENTS_PER_BOUNDARY = 10_000;
3
+ export const MAX_MARKER_SCAN_NODES = 50_000;
4
+ export const MAX_BOUNDARY_SCRIPT_SCAN = 8;
5
+ export function streamingErrorMessage(error) {
6
+ return error instanceof Error ? error.message : String(error);
7
+ }
8
+ const BOUNDARY_START_PREFIX = 'wb:';
9
+ const BOUNDARY_END_PREFIX = '/wb:';
10
+ const MARKERLESS_RANGE = { start: null, end: null };
11
+ export function resolveBoundaryRange(scriptEl, sequence, terminal) {
12
+ const end = findEndMarker(scriptEl, sequence);
13
+ if (end) {
14
+ if (terminal) {
15
+ return {
16
+ ok: false,
17
+ reason: `terminal boundary ${sequence} must be markerless`,
18
+ truncated: false,
19
+ };
20
+ }
21
+ const start = findStartMarkerBefore(end, sequence);
22
+ if (!start) {
23
+ return {
24
+ ok: false,
25
+ reason: `missing start marker for boundary ${sequence}`,
26
+ truncated: false,
27
+ };
28
+ }
29
+ return { ok: true, range: { start, end } };
30
+ }
31
+ if (terminal)
32
+ return { ok: true, range: MARKERLESS_RANGE };
33
+ return {
34
+ ok: false,
35
+ reason: `missing end marker for boundary ${sequence}`,
36
+ truncated: true,
37
+ start: findStartMarkerBefore(scriptEl, sequence),
38
+ };
39
+ }
40
+ export function findBoundaryScript(sentinel) {
41
+ let node = sentinel.previousElementSibling;
42
+ for (let hops = 0; node && hops < MAX_BOUNDARY_SCRIPT_SCAN; hops++) {
43
+ if (node.tagName === 'SCRIPT' &&
44
+ node.hasAttribute(BOUNDARY_SCRIPT_ATTR)) {
45
+ return node;
46
+ }
47
+ node = node.previousElementSibling;
48
+ }
49
+ return null;
50
+ }
51
+ function findEndMarker(scriptEl, sequence) {
52
+ const marker = previousComment(scriptEl);
53
+ return marker?.data === `${BOUNDARY_END_PREFIX}${sequence}`
54
+ ? marker
55
+ : null;
56
+ }
57
+ function findStartMarkerBefore(nodeBefore, sequence) {
58
+ return findCommentBefore(nodeBefore, `${BOUNDARY_START_PREFIX}${sequence}`);
59
+ }
60
+ export function findEndMarkerByPrefix(scriptEl) {
61
+ const marker = previousComment(scriptEl);
62
+ return marker?.data.startsWith(BOUNDARY_END_PREFIX) ? marker : null;
63
+ }
64
+ export function findStartMarkerByPrefix(endMarker) {
65
+ return findCommentBefore(endMarker, `${BOUNDARY_START_PREFIX}${endMarker.data.slice(BOUNDARY_END_PREFIX.length)}`);
66
+ }
67
+ function previousComment(node) {
68
+ const previous = node.previousSibling;
69
+ return previous?.nodeType === 8
70
+ ? previous
71
+ : null;
72
+ }
73
+ function findCommentBefore(nodeBefore, target) {
74
+ let node = nodeBefore.previousSibling;
75
+ for (let hops = 0; node && hops < MAX_MARKER_SCAN_NODES; hops++) {
76
+ if (node.nodeType === 8 &&
77
+ node.data === target) {
78
+ return node;
79
+ }
80
+ node = node.previousSibling;
81
+ }
82
+ return null;
83
+ }
84
+ export function safeRemove(node) {
85
+ try {
86
+ node?.remove?.();
87
+ }
88
+ catch {
89
+ }
90
+ }
91
+ export function safeRemoveAttribute(el, name) {
92
+ try {
93
+ el.removeAttribute?.(name);
94
+ }
95
+ catch {
96
+ }
97
+ }
98
+ export function removeBoundaryScaffolding(sentinel, scriptEl, startMarker, endMarker) {
99
+ if (!scriptEl) {
100
+ safeRemove(sentinel);
101
+ safeRemove(endMarker);
102
+ safeRemove(startMarker);
103
+ return;
104
+ }
105
+ let node = sentinel.previousElementSibling;
106
+ safeRemove(sentinel);
107
+ for (let hops = 0; node && hops <= MAX_BOUNDARY_SCRIPT_SCAN; hops++) {
108
+ const previous = node.previousElementSibling;
109
+ const wasBoundaryScript = node === scriptEl;
110
+ safeRemove(node);
111
+ if (wasBoundaryScript)
112
+ break;
113
+ node = previous;
114
+ }
115
+ safeRemove(endMarker);
116
+ safeRemove(startMarker);
117
+ }
118
+ export function firstNodeWithin(root) {
119
+ return root.shadowRoot?.firstChild ?? root.firstChild;
120
+ }
121
+ export function nextWithinRoot(node, root) {
122
+ if (node.nodeType === 1) {
123
+ const shadowRoot = node.shadowRoot;
124
+ if (shadowRoot?.firstChild)
125
+ return shadowRoot.firstChild;
126
+ }
127
+ if (node.firstChild)
128
+ return node.firstChild;
129
+ return nextAfterSubtreeWithin(node, root);
130
+ }
131
+ export function nextAfterSubtreeWithin(node, root) {
132
+ let current = node;
133
+ while (current && current !== root) {
134
+ if (current.nodeType === 11) {
135
+ const host = current.host ?? null;
136
+ if (host) {
137
+ if (host.firstChild)
138
+ return host.firstChild;
139
+ current = host;
140
+ continue;
141
+ }
142
+ }
143
+ if (current.nextSibling)
144
+ return current.nextSibling;
145
+ current = current.parentNode;
146
+ }
147
+ return null;
148
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { installStreamingCoordinator } from './streaming.js';
2
+ installStreamingCoordinator();
@@ -0,0 +1,2 @@
1
+ export declare function installStreamingCoordinator(): void;
2
+ export declare function resetStreamingInstallForTests(): void;
@@ -0,0 +1,25 @@
1
+ import { beginStreamingGate } from './lifecycle.js';
2
+ import { installTemplateElementRuntime } from './static-host.js';
3
+ import { enqueueStreamingSentinel, installStreamingTruncationGuard, } from './streaming-coordinator.js';
4
+ import { isStreamingHydrationMode } from './streaming-mode.js';
5
+ const SENTINEL_TAG = 'webui-hydrate';
6
+ let installed = false;
7
+ class WebUiHydrateSentinel extends HTMLElement {
8
+ connectedCallback() {
9
+ enqueueStreamingSentinel(this);
10
+ }
11
+ }
12
+ export function installStreamingCoordinator() {
13
+ if (installed || !isStreamingHydrationMode())
14
+ return;
15
+ installed = true;
16
+ if (!customElements.get(SENTINEL_TAG)) {
17
+ customElements.define(SENTINEL_TAG, WebUiHydrateSentinel);
18
+ }
19
+ beginStreamingGate();
20
+ installStreamingTruncationGuard();
21
+ installTemplateElementRuntime();
22
+ }
23
+ export function resetStreamingInstallForTests() {
24
+ installed = false;
25
+ }
@@ -0,0 +1,5 @@
1
+ export declare const STREAMING_BOUNDARY_ACTIVATE: unique symbol;
2
+ export declare const PENDING_ROOT_CONNECTED: unique symbol;
3
+ export declare const STREAMED_HOST_ATTR = "data-ws";
4
+ export declare function isStreamingHydrationMode(): boolean;
5
+ export declare function resetStreamingModeForTests(): void;
@@ -0,0 +1,14 @@
1
+ let cached;
2
+ export const STREAMING_BOUNDARY_ACTIVATE = Symbol.for('microsoft.webui.boundaryActivate');
3
+ export const PENDING_ROOT_CONNECTED = Symbol.for('microsoft.webui.pendingRootConnected');
4
+ export const STREAMED_HOST_ATTR = 'data-ws';
5
+ export function isStreamingHydrationMode() {
6
+ if (cached !== undefined)
7
+ return cached;
8
+ cached = typeof document !== 'undefined' &&
9
+ !!document.querySelector('meta[name="webui-streaming"][content="1"]');
10
+ return cached;
11
+ }
12
+ export function resetStreamingModeForTests() {
13
+ cached = undefined;
14
+ }
@@ -0,0 +1,32 @@
1
+ import type { TemplateMeta } from './template.js';
2
+ export interface BoundaryBootstrap {
3
+ state?: Record<string, unknown>;
4
+ templates?: Record<string, TemplateMeta>;
5
+ inventory?: string;
6
+ nonce?: string;
7
+ chain?: unknown[];
8
+ css?: string[];
9
+ styles?: string[];
10
+ [key: string]: unknown;
11
+ }
12
+ export declare const RECORD_KIND_FINAL_CHECKPOINT = 0;
13
+ export declare const RECORD_KIND_UPDATABLE_CHECKPOINT = 1;
14
+ export declare const RECORD_KIND_STATE_UPDATE = 2;
15
+ export declare const RECORD_KIND_TERMINAL = 3;
16
+ export type BoundaryRecordKind = typeof RECORD_KIND_FINAL_CHECKPOINT | typeof RECORD_KIND_UPDATABLE_CHECKPOINT | typeof RECORD_KIND_STATE_UPDATE | typeof RECORD_KIND_TERMINAL;
17
+ export type BoundaryRecordPayload = BoundaryBootstrap | Record<string, unknown>;
18
+ export type BoundaryEnvelope = readonly [
19
+ version: number,
20
+ recordSequence: number,
21
+ kind: BoundaryRecordKind,
22
+ target: number,
23
+ payload: BoundaryRecordPayload
24
+ ];
25
+ export type ParseBoundaryEnvelopeResult = {
26
+ readonly ok: true;
27
+ readonly envelope: BoundaryEnvelope;
28
+ } | {
29
+ readonly ok: false;
30
+ readonly reason: string;
31
+ };
32
+ export declare function parseBoundaryEnvelope(text: string): ParseBoundaryEnvelopeResult;
@@ -0,0 +1,27 @@
1
+ const SUPPORTED_VERSION = 1;
2
+ export const RECORD_KIND_FINAL_CHECKPOINT = 0;
3
+ export const RECORD_KIND_UPDATABLE_CHECKPOINT = 1;
4
+ export const RECORD_KIND_STATE_UPDATE = 2;
5
+ export const RECORD_KIND_TERMINAL = 3;
6
+ function invalid(reason) {
7
+ return { ok: false, reason };
8
+ }
9
+ export function parseBoundaryEnvelope(text) {
10
+ let parsed;
11
+ try {
12
+ parsed = JSON.parse(text);
13
+ }
14
+ catch {
15
+ return invalid('boundary payload is not valid JSON');
16
+ }
17
+ if (!Array.isArray(parsed) || parsed.length !== 5) {
18
+ return invalid('boundary envelope must be a 5-element [version, recordSequence, kind, target, payload] array');
19
+ }
20
+ if (parsed[0] !== SUPPORTED_VERSION) {
21
+ return invalid(`unsupported boundary envelope version ${JSON.stringify(parsed[0])}`);
22
+ }
23
+ return {
24
+ ok: true,
25
+ envelope: parsed,
26
+ };
27
+ }
@@ -0,0 +1,5 @@
1
+ export { enqueueStreamingSentinel as __enqueueSentinelForTests, elementHasPendingStateForTests as __elementHasPendingStateForTests, installStreamingTruncationGuard as __installTruncationGuardForTests, isStreamingHaltedForTests as __isHaltedForTests, pendingTagWaiterCountForTests as __pendingTagWaiterCountForTests, pendingUndefinedRootCountForTests as __pendingUndefinedRootCountForTests, streamingRetentionStateForTests as __streamingRetentionStateForTests, } from './streaming-coordinator.js';
2
+ export { installStreamingCoordinator } from './streaming-install.js';
3
+ export { parseBoundaryEnvelope } from './streaming-protocol.js';
4
+ export type { BoundaryBootstrap, BoundaryEnvelope, ParseBoundaryEnvelopeResult, } from './streaming-protocol.js';
5
+ export declare function __resetStreamingCoordinatorForTests(): void;
@@ -0,0 +1,9 @@
1
+ import { resetStreamingCoordinatorStateForTests, } from './streaming-coordinator.js';
2
+ import { resetStreamingInstallForTests } from './streaming-install.js';
3
+ export { enqueueStreamingSentinel as __enqueueSentinelForTests, elementHasPendingStateForTests as __elementHasPendingStateForTests, installStreamingTruncationGuard as __installTruncationGuardForTests, isStreamingHaltedForTests as __isHaltedForTests, pendingTagWaiterCountForTests as __pendingTagWaiterCountForTests, pendingUndefinedRootCountForTests as __pendingUndefinedRootCountForTests, streamingRetentionStateForTests as __streamingRetentionStateForTests, } from './streaming-coordinator.js';
4
+ export { installStreamingCoordinator } from './streaming-install.js';
5
+ export { parseBoundaryEnvelope } from './streaming-protocol.js';
6
+ export function __resetStreamingCoordinatorForTests() {
7
+ resetStreamingCoordinatorStateForTests();
8
+ resetStreamingInstallForTests();
9
+ }
@@ -1,11 +1,14 @@
1
1
  import type { TemplateBlockMeta, TemplateNodePath } from './template.js';
2
+ import { STREAMING_BOUNDARY_ACTIVATE } from './streaming-mode.js';
2
3
  import type { ScopeFrame, TemplateInstance } from './element/types.js';
3
4
  declare const WEBUI_SET_STATE_KEY: unique symbol;
5
+ declare const STREAMING_BOUNDARY_ABANDON: unique symbol;
4
6
  export declare class TemplateElement extends HTMLElement {
5
7
  private $root;
6
8
  private $meta?;
7
9
  private $ready;
8
10
  private $hydrated;
11
+ private $hydratedCallbackCalled;
9
12
  private $deferredSSR;
10
13
  private $activatingDeferredSSR;
11
14
  private $hasUnknownScopes;
@@ -17,6 +20,8 @@ export declare class TemplateElement extends HTMLElement {
17
20
  private $pathIndex?;
18
21
  private $wildcardBindings?;
19
22
  [WEBUI_SET_STATE_KEY](key: string, value: unknown): boolean;
23
+ [STREAMING_BOUNDARY_ACTIVATE](state?: Record<string, unknown>): number;
24
+ [STREAMING_BOUNDARY_ABANDON](): void;
20
25
  static define(tagName: string): void;
21
26
  connectedCallback(): void;
22
27
  private $mount;
@@ -30,10 +35,13 @@ export declare class TemplateElement extends HTMLElement {
30
35
  protected $beforeExternalStateWrite(): void;
31
36
  protected $afterExternalStateWrite(_applied: boolean): void;
32
37
  protected $shouldDeferSSRHydration(): boolean;
33
- protected $activateDeferredSSR(): void;
38
+ protected $shouldActivateOnBoundaryCommit(): boolean;
39
+ protected $activateDeferredSSR(state?: Record<string, unknown>): void;
34
40
  protected $shouldApplySSRBootstrapState(): boolean;
35
41
  protected $shouldApplySSRState(_key: string): boolean;
36
42
  protected $syncAuthoredAttributes(): void;
43
+ protected hydratedCallback(): void;
44
+ private $notifyHydrated;
37
45
  protected $shouldApplyTemplateStateFromSSR(_key: string): boolean;
38
46
  protected $setTemplateState(key: string, value: unknown): boolean;
39
47
  private $writeTemplateState;
@@ -1,5 +1,6 @@
1
- import { getTemplate } from './template.js';
1
+ import { deferTemplateDefinition, getTemplate } from './template.js';
2
2
  import { hydrationStart, hydrationEnd } from './lifecycle.js';
3
+ import { isStreamingHydrationMode, PENDING_ROOT_CONNECTED, STREAMED_HOST_ATTR, STREAMING_BOUNDARY_ACTIVATE, } from './streaming-mode.js';
3
4
  import { createRepeatKeyState, seedHydratedRepeatKeys, syncRepeat, dotWalk, } from './element/diff.js';
4
5
  import { collectItemMarkers, nextElement, findByOrdinal, MARKER_COND_START, MARKER_COND_END, MARKER_REPEAT_START, } from './element/markers.js';
5
6
  import { injectModuleStyle, } from './element/styles.js';
@@ -35,6 +36,10 @@ function getTplOrdinals(tplNode) {
35
36
  const EMPTY_ARR = [];
36
37
  const EMPTY_SET = Object.freeze(new Set());
37
38
  const WEBUI_SET_STATE_KEY = Symbol.for('microsoft.webui.setStateKey');
39
+ const STREAMING_BOUNDARY_ABANDON = Symbol.for('microsoft.webui.boundaryAbandon');
40
+ const ACTIVATION_ACTIVATED = 1;
41
+ const ACTIVATION_STATIC_HOST_OPT_OUT = 2;
42
+ const ACTIVATION_MISSING_TEMPLATE = 3;
38
43
  const templateMetaByCtor = new WeakMap();
39
44
  function childNodesArray(parent) {
40
45
  const children = parent.childNodes;
@@ -53,8 +58,7 @@ function getTemplateDom(meta) {
53
58
  templateDOMCache.set(meta, div);
54
59
  return div;
55
60
  }
56
- function installTemplateObservedAttributes(ctor, tagName) {
57
- const meta = getTemplate(tagName);
61
+ function installTemplateObservedAttributes(ctor, tagName, meta = getTemplate(tagName)) {
58
62
  if (!meta)
59
63
  return;
60
64
  templateMetaByCtor.set(ctor, meta);
@@ -90,6 +94,10 @@ function installTemplateObservedAttributes(ctor, tagName) {
90
94
  configurable: true,
91
95
  });
92
96
  }
97
+ function defineTemplateConstructor(ctor, tagName, meta) {
98
+ installTemplateObservedAttributes(ctor, tagName, meta);
99
+ customElements.define(tagName, ctor);
100
+ }
93
101
  function hasAuthoredMember(instance, key) {
94
102
  if (Object.prototype.hasOwnProperty.call(instance, key))
95
103
  return true;
@@ -107,6 +115,7 @@ export class TemplateElement extends HTMLElement {
107
115
  this.$root = null;
108
116
  this.$ready = false;
109
117
  this.$hydrated = false;
118
+ this.$hydratedCallbackCalled = false;
110
119
  this.$deferredSSR = false;
111
120
  this.$activatingDeferredSSR = false;
112
121
  this.$hasUnknownScopes = false;
@@ -125,9 +134,39 @@ export class TemplateElement extends HTMLElement {
125
134
  this.$update(key);
126
135
  return owned;
127
136
  }
137
+ [STREAMING_BOUNDARY_ACTIVATE](state) {
138
+ if (!this.$deferredSSR && this.hasAttribute(STREAMED_HOST_ATTR)) {
139
+ this.$deferredSSR = true;
140
+ this.$ready = true;
141
+ }
142
+ if (!this.$deferredSSR)
143
+ return ACTIVATION_ACTIVATED;
144
+ const meta = this.$templateMeta();
145
+ if (!meta) {
146
+ return ACTIVATION_MISSING_TEMPLATE;
147
+ }
148
+ this.$meta = meta;
149
+ if (!this.$shouldActivateOnBoundaryCommit())
150
+ return ACTIVATION_STATIC_HOST_OPT_OUT;
151
+ this.$activateDeferredSSR(state);
152
+ return ACTIVATION_ACTIVATED;
153
+ }
154
+ [STREAMING_BOUNDARY_ABANDON]() {
155
+ this.$deferredSSR = false;
156
+ this.$activatingDeferredSSR = false;
157
+ this.$ready = false;
158
+ this.$preReadyWrites = null;
159
+ }
128
160
  static define(tagName) {
129
- installTemplateObservedAttributes(this, tagName);
130
- customElements.define(tagName, this);
161
+ const ctor = this;
162
+ const meta = getTemplate(tagName);
163
+ if (!meta && isStreamingHydrationMode()) {
164
+ deferTemplateDefinition(tagName, ctor, () => {
165
+ defineTemplateConstructor(ctor, tagName, getTemplate(tagName));
166
+ });
167
+ return;
168
+ }
169
+ defineTemplateConstructor(ctor, tagName, meta);
131
170
  }
132
171
  connectedCallback() {
133
172
  const tag = this.tagName.toLowerCase();
@@ -137,12 +176,24 @@ export class TemplateElement extends HTMLElement {
137
176
  }
138
177
  if (this.$hydrated && this.$root) {
139
178
  hydrationStart();
179
+ try {
180
+ this.$ready = true;
181
+ this.$update();
182
+ }
183
+ finally {
184
+ hydrationEnd();
185
+ }
186
+ return;
187
+ }
188
+ if (isStreamingHydrationMode() && this.hasAttribute(STREAMED_HOST_ATTR)) {
189
+ this.$deferredSSR = true;
140
190
  this.$ready = true;
141
- this.$update();
142
- hydrationEnd();
191
+ const resume = this[PENDING_ROOT_CONNECTED];
192
+ if (typeof resume === 'function')
193
+ resume.call(this);
143
194
  return;
144
195
  }
145
- const meta = getTemplate(tag);
196
+ const meta = this.$templateMeta();
146
197
  if (!meta) {
147
198
  console.warn(`[WebUI] Template metadata for <${tag}> not found. ` +
148
199
  `Ensure the component is included in the SSR output or registered via __webui.templates.`);
@@ -151,7 +202,7 @@ export class TemplateElement extends HTMLElement {
151
202
  this.$meta = meta;
152
203
  this.$mount(meta, false);
153
204
  }
154
- $mount(meta, forceSSR) {
205
+ $mount(meta, forceSSR, ssrState) {
155
206
  if (this.$hydrated)
156
207
  return;
157
208
  const hasShadow = !!this.shadowRoot;
@@ -182,35 +233,41 @@ export class TemplateElement extends HTMLElement {
182
233
  return;
183
234
  }
184
235
  hydrationStart();
185
- if (meta.sa)
186
- injectModuleStyle(meta.sa, this.shadowRoot);
187
- if (isSSR) {
188
- if (this.$shouldApplySSRBootstrapState())
189
- this.$applySSRState();
190
- this.$root = this.$hydrate(root, meta, getTemplateDom(meta));
191
- }
192
- else {
193
- clientRoot = this.$createStagingRoot(meta);
194
- this.$root = this.$wire(clientRoot, meta);
195
- }
196
- this.$meta = meta;
197
- this.$hydrated = true;
198
- this.$ready = true;
199
- this.$syncAuthoredAttributes();
200
- if (isSSR && DEV)
201
- this.$checkHydrationMismatch();
202
- else
203
- this.$preReadyWrites = null;
204
- if (!isSSR && clientRoot) {
205
- this.$updateInstance(this.$root);
206
- if (this.$root.repeats.length !== 0 || this.$root.conds.length !== 0) {
207
- this.$root.nodes = childNodesArray(clientRoot);
208
- this.$replaceInstanceContainer(this.$root, clientRoot, root);
236
+ try {
237
+ if (meta.sa)
238
+ injectModuleStyle(meta.sa, this.shadowRoot);
239
+ if (isSSR) {
240
+ if (this.$shouldApplySSRBootstrapState()) {
241
+ this.$applySSRState(forceSSR ? ssrState : window.__webui?.state);
242
+ }
243
+ this.$root = this.$hydrate(root, meta, getTemplateDom(meta));
244
+ }
245
+ else {
246
+ clientRoot = this.$createStagingRoot(meta);
247
+ this.$root = this.$wire(clientRoot, meta);
248
+ }
249
+ this.$meta = meta;
250
+ this.$hydrated = true;
251
+ this.$ready = true;
252
+ this.$syncAuthoredAttributes();
253
+ if (isSSR && DEV)
254
+ this.$checkHydrationMismatch();
255
+ else
256
+ this.$preReadyWrites = null;
257
+ if (!isSSR && clientRoot) {
258
+ this.$updateInstance(this.$root);
259
+ if (this.$root.repeats.length !== 0 || this.$root.conds.length !== 0) {
260
+ this.$root.nodes = childNodesArray(clientRoot);
261
+ this.$replaceInstanceContainer(this.$root, clientRoot, root);
262
+ }
263
+ this.$appendStagedChildren(root, clientRoot);
264
+ this.$root.container = root;
209
265
  }
210
- this.$appendStagedChildren(root, clientRoot);
211
- this.$root.container = root;
266
+ this.$notifyHydrated();
267
+ }
268
+ finally {
269
+ hydrationEnd();
212
270
  }
213
- hydrationEnd();
214
271
  }
215
272
  disconnectedCallback() {
216
273
  if (this.$root) {
@@ -299,15 +356,21 @@ export class TemplateElement extends HTMLElement {
299
356
  $shouldDeferSSRHydration() {
300
357
  return false;
301
358
  }
302
- $activateDeferredSSR() {
303
- if (!this.$deferredSSR || !this.$meta)
359
+ $shouldActivateOnBoundaryCommit() {
360
+ return true;
361
+ }
362
+ $activateDeferredSSR(state) {
363
+ if (!this.$deferredSSR)
364
+ return;
365
+ const meta = this.$meta;
366
+ if (!meta)
304
367
  return;
305
368
  this.$deferredSSR = false;
306
369
  this.$ready = false;
307
370
  this.$preReadyWrites = null;
308
371
  this.$activatingDeferredSSR = true;
309
372
  try {
310
- this.$mount(this.$meta, true);
373
+ this.$mount(meta, true, state);
311
374
  }
312
375
  finally {
313
376
  this.$activatingDeferredSSR = false;
@@ -321,6 +384,14 @@ export class TemplateElement extends HTMLElement {
321
384
  }
322
385
  $syncAuthoredAttributes() {
323
386
  }
387
+ hydratedCallback() {
388
+ }
389
+ $notifyHydrated() {
390
+ if (this.$hydratedCallbackCalled)
391
+ return;
392
+ this.$hydratedCallbackCalled = true;
393
+ this.hydratedCallback();
394
+ }
324
395
  $shouldApplyTemplateStateFromSSR(_key) {
325
396
  return true;
326
397
  }
@@ -368,8 +439,7 @@ export class TemplateElement extends HTMLElement {
368
439
  }
369
440
  return false;
370
441
  }
371
- $applySSRState() {
372
- const state = window.__webui?.state;
442
+ $applySSRState(state) {
373
443
  if (!state || typeof state !== 'object')
374
444
  return;
375
445
  const observableNames = this.$observableNames();
@@ -783,7 +853,6 @@ export class TemplateElement extends HTMLElement {
783
853
  const insertRef = ssrParent.childNodes[Math.min(beforeIndex ?? staticCount, ssrParent.childNodes.length)] ?? null;
784
854
  ssrParent.insertBefore(anchor, insertRef);
785
855
  }
786
- lastRepMarker = anchor;
787
856
  const repeatInsts = [];
788
857
  const hasCollectionState = this.$hasStateRoot(collection, scope);
789
858
  const itemsArr = this.$resolveValue(collection, scope);
@@ -791,6 +860,7 @@ export class TemplateElement extends HTMLElement {
791
860
  const { items: itemMarkers, end: endMarker } = marker
792
861
  ? collectItemMarkers(anchor)
793
862
  : { items: [], end: null };
863
+ lastRepMarker = endMarker ?? anchor;
794
864
  if (blockMeta && blockTplDom && anchor.parentNode && itemMarkers.length > 0) {
795
865
  if (!this.$activatingDeferredSSR
796
866
  && hasCollectionState
@@ -14,3 +14,4 @@ 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 deferTemplateDefinition(tagName: string, ctor: Function, define: () => void): void;
package/dist/template.js CHANGED
@@ -1,22 +1,9 @@
1
- import { dispatchTemplatesRegistered } from './template-events.js';
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
4
  let webuiDataLoaded = false;
5
- export function getTemplate(name) {
6
- let meta = window.__webui?.templates?.[name];
7
- if (!meta) {
8
- loadWebUIDataBlock();
9
- meta = window.__webui?.templates?.[name];
10
- }
11
- if (meta)
12
- normalizeTemplate(name, meta);
13
- return meta;
14
- }
15
- export function getTemplateRegistry() {
16
- loadWebUIDataBlock();
17
- return window.__webui?.templates;
18
- }
19
- export function registerTemplateData(templates, templateFns) {
5
+ const pendingTemplateDefinitions = new Map();
6
+ function acceptTemplateData(templates, templateFns) {
20
7
  const w = window;
21
8
  if (!w.__webui)
22
9
  w.__webui = {};
@@ -32,16 +19,56 @@ export function registerTemplateData(templates, templateFns) {
32
19
  }
33
20
  }
34
21
  const names = Object.keys(templates);
35
- let hasTemplates = false;
36
22
  for (let i = 0; i < names.length; i++) {
37
23
  const tag = names[i];
38
24
  const meta = templates[tag];
39
25
  w.__webui.templates[tag] = meta;
40
26
  normalizeTemplate(tag, meta);
41
- hasTemplates = true;
42
27
  }
43
- if (hasTemplates)
28
+ for (let i = 0; i < names.length; i++) {
29
+ const pending = pendingTemplateDefinitions.get(names[i]);
30
+ if (!pending)
31
+ continue;
32
+ pendingTemplateDefinitions.delete(names[i]);
33
+ pending.define();
34
+ }
35
+ return names.length > 0;
36
+ }
37
+ if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
38
+ window.addEventListener(TEMPLATES_REGISTERED_EVENT, (event) => {
39
+ const templates = templateRegistrationDetail(event)?.templates;
40
+ if (templates)
41
+ acceptTemplateData(templates);
42
+ });
43
+ }
44
+ export function getTemplate(name) {
45
+ let meta = window.__webui?.templates?.[name];
46
+ if (!meta) {
47
+ loadWebUIDataBlock();
48
+ meta = window.__webui?.templates?.[name];
49
+ }
50
+ if (meta)
51
+ normalizeTemplate(name, meta);
52
+ return meta;
53
+ }
54
+ export function getTemplateRegistry() {
55
+ loadWebUIDataBlock();
56
+ return window.__webui?.templates;
57
+ }
58
+ export function registerTemplateData(templates, templateFns) {
59
+ if (acceptTemplateData(templates, templateFns)) {
44
60
  dispatchTemplatesRegistered(templates);
61
+ }
62
+ }
63
+ export function deferTemplateDefinition(tagName, ctor, define) {
64
+ const pending = pendingTemplateDefinitions.get(tagName);
65
+ if (pending) {
66
+ if (pending.ctor === ctor) {
67
+ throw new Error(`[WebUI] <${tagName}> is already pending definition.`);
68
+ }
69
+ throw new Error(`[WebUI] <${tagName}> already has a pending authored definition.`);
70
+ }
71
+ pendingTemplateDefinitions.set(tagName, { ctor, define });
45
72
  }
46
73
  function loadWebUIDataBlock() {
47
74
  if (webuiDataLoaded || window.__webui?.state !== undefined || typeof document === 'undefined')