@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.
- package/README.md +61 -16
- 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 +367 -0
- package/dist/streaming-deferred.d.ts +22 -0
- package/dist/streaming-deferred.js +273 -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.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.d.ts +1 -0
- package/dist/template.js +46 -19
- package/package.json +9 -3
|
@@ -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,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 $
|
|
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;
|
package/dist/template-element.js
CHANGED
|
@@ -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
|
-
|
|
130
|
-
|
|
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
|
|
142
|
-
|
|
191
|
+
const resume = this[PENDING_ROOT_CONNECTED];
|
|
192
|
+
if (typeof resume === 'function')
|
|
193
|
+
resume.call(this);
|
|
143
194
|
return;
|
|
144
195
|
}
|
|
145
|
-
const meta =
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (
|
|
189
|
-
this.$
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
this.$
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (
|
|
207
|
-
this.$root
|
|
208
|
-
this.$
|
|
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.$
|
|
211
|
-
|
|
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
|
-
$
|
|
303
|
-
|
|
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(
|
|
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
|
package/dist/template.d.ts
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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')
|