@microsoft/webui-framework 0.0.25 → 0.0.26

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 (53) hide show
  1. package/README.md +209 -53
  2. package/dist/component-asset/asset.d.ts +4 -2
  3. package/dist/component-asset/asset.js +19 -3
  4. package/dist/component-asset/generated-manifest.d.ts +1 -0
  5. package/dist/component-asset/generated-manifest.js +32 -0
  6. package/dist/component-asset/loader.d.ts +2 -1
  7. package/dist/component-asset/loader.js +98 -22
  8. package/dist/component-asset/manifest.d.ts +2 -1
  9. package/dist/component-asset.d.ts +1 -1
  10. package/dist/component-asset.js +12 -2
  11. package/dist/element/link-styles.d.ts +20 -0
  12. package/dist/element/link-styles.js +1030 -0
  13. package/dist/element/markers.d.ts +4 -0
  14. package/dist/element/markers.js +60 -5
  15. package/dist/element/styles.d.ts +39 -1
  16. package/dist/element/styles.js +623 -13
  17. package/dist/element/types.d.ts +5 -3
  18. package/dist/element.js +9 -1
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +1 -0
  21. package/dist/interaction-hydration.d.ts +8 -0
  22. package/dist/interaction-hydration.js +167 -0
  23. package/dist/static-host.js +43 -1
  24. package/dist/streaming-activation.d.ts +2 -2
  25. package/dist/streaming-activation.js +4 -2
  26. package/dist/streaming-bootstrap.d.ts +2 -2
  27. package/dist/streaming-bootstrap.js +7 -1
  28. package/dist/streaming-cleanup.d.ts +1 -0
  29. package/dist/streaming-cleanup.js +66 -14
  30. package/dist/streaming-coordinator.d.ts +3 -2
  31. package/dist/streaming-coordinator.js +107 -47
  32. package/dist/streaming-deferred.d.ts +14 -4
  33. package/dist/streaming-deferred.js +200 -87
  34. package/dist/streaming-dom.d.ts +12 -3
  35. package/dist/streaming-dom.js +41 -27
  36. package/dist/streaming-mode.d.ts +5 -0
  37. package/dist/streaming-mode.js +4 -0
  38. package/dist/streaming-protocol.d.ts +19 -3
  39. package/dist/streaming-protocol.js +4 -3
  40. package/dist/streaming-spans.d.ts +9 -0
  41. package/dist/streaming-spans.js +200 -0
  42. package/dist/template-content.d.ts +17 -0
  43. package/dist/template-content.js +106 -0
  44. package/dist/template-element.d.ts +17 -2
  45. package/dist/template-element.js +493 -177
  46. package/dist/template-events.d.ts +7 -0
  47. package/dist/template-events.js +7 -1
  48. package/dist/template-types.d.ts +1 -2
  49. package/dist/template.d.ts +7 -1
  50. package/dist/template.js +79 -16
  51. package/package.json +9 -3
  52. package/dist/component-asset/resources.d.ts +0 -4
  53. package/dist/component-asset/resources.js +0 -83
package/dist/index.d.ts CHANGED
@@ -2,4 +2,6 @@ export { WebUIElement } from './element.js';
2
2
  export { observable, attr } from './decorators.js';
3
3
  export { getTemplate, registerTemplateData } from './template.js';
4
4
  export type { TemplateMeta } from './template.js';
5
+ export { installComponentStyles, prepareComponentStyles, registerComponentStyles, } from './element/styles.js';
6
+ export type { ComponentStyleResource, ComponentStyles, } from './element/styles.js';
5
7
  export { hydrationStart, hydrationEnd } from './lifecycle.js';
package/dist/index.js CHANGED
@@ -3,4 +3,5 @@ setTimeout(installTemplateElementRuntime, 0);
3
3
  export { WebUIElement } from './element.js';
4
4
  export { observable, attr } from './decorators.js';
5
5
  export { getTemplate, registerTemplateData } from './template.js';
6
+ export { installComponentStyles, prepareComponentStyles, registerComponentStyles, } from './element/styles.js';
6
7
  export { hydrationStart, hydrationEnd } from './lifecycle.js';
@@ -0,0 +1,8 @@
1
+ export interface InteractionHydrationOptions {
2
+ load: () => Promise<unknown>;
3
+ onError?: (error: unknown) => void;
4
+ root?: Element;
5
+ }
6
+ export declare function isInteractionReplay(event: Event): boolean;
7
+ export declare function installInteractionHydration(options: InteractionHydrationOptions): () => void;
8
+ export declare function wakeInteractionHydration(root?: Element): Promise<void>;
@@ -0,0 +1,167 @@
1
+ const INTERACTION_ROOT_SELECTOR = '[data-webui-interaction]';
2
+ const WAKE_EVENTS = [
3
+ 'pointerdown',
4
+ 'focusin',
5
+ 'keydown',
6
+ ];
7
+ const installedBoundaries = new WeakMap();
8
+ let compilerRoot;
9
+ const interactionReplays = new WeakMap();
10
+ export function isInteractionReplay(event) {
11
+ return interactionReplays.has(event);
12
+ }
13
+ export function installInteractionHydration(options) {
14
+ const root = options.root ?? compilerMarkedRoot();
15
+ const existing = installedBoundaries.get(root);
16
+ if (existing)
17
+ return existing.dispose;
18
+ let loading;
19
+ let listening = true;
20
+ let disposed = false;
21
+ const disposal = {};
22
+ const dispose = createDisposer(disposal);
23
+ const remove = () => {
24
+ if (!listening)
25
+ return;
26
+ listening = false;
27
+ for (let i = 0; i < WAKE_EVENTS.length; i++) {
28
+ root.removeEventListener(WAKE_EVENTS[i], wake, true);
29
+ }
30
+ root.removeEventListener('click', replay, true);
31
+ installedBoundaries.delete(root);
32
+ disposal.callback = undefined;
33
+ };
34
+ disposal.callback = () => {
35
+ disposed = true;
36
+ remove();
37
+ };
38
+ const ensureLoaded = () => {
39
+ loading ??= Promise.resolve().then(options.load).then(() => {
40
+ remove();
41
+ }, (error) => {
42
+ remove();
43
+ reportFailure(error, options.onError);
44
+ throw error;
45
+ });
46
+ return loading;
47
+ };
48
+ const wake = () => {
49
+ void ensureLoaded().catch(() => { });
50
+ };
51
+ const replay = (event) => {
52
+ if (!listening || hasTraversed(event, root))
53
+ return;
54
+ const target = event.composedPath()[0] ?? event.target;
55
+ const replayEvent = target ? cloneClick(event, target, root) : null;
56
+ if (!replayEvent) {
57
+ wake();
58
+ return;
59
+ }
60
+ event.preventDefault();
61
+ event.stopImmediatePropagation();
62
+ const dispatchReplay = () => {
63
+ if (!disposed)
64
+ target.dispatchEvent(replayEvent);
65
+ };
66
+ void ensureLoaded().then(dispatchReplay, dispatchReplay);
67
+ };
68
+ installedBoundaries.set(root, { dispose, wake: ensureLoaded });
69
+ for (let i = 0; i < WAKE_EVENTS.length; i++) {
70
+ root.addEventListener(WAKE_EVENTS[i], wake, true);
71
+ }
72
+ root.addEventListener('click', replay, true);
73
+ return dispose;
74
+ }
75
+ function createDisposer(slot) {
76
+ return () => slot.callback?.();
77
+ }
78
+ export function wakeInteractionHydration(root) {
79
+ const target = root ?? compilerMarkedRoot();
80
+ const boundary = installedBoundaries.get(target);
81
+ if (!boundary) {
82
+ throw new Error('[WebUI] interaction hydration boundary is not installed.');
83
+ }
84
+ return boundary.wake();
85
+ }
86
+ function compilerMarkedRoot() {
87
+ const cached = compilerRoot?.deref();
88
+ if (cached && cached.isConnected !== false)
89
+ return cached;
90
+ const roots = compilerMarkedRoots();
91
+ if (roots.length !== 1) {
92
+ throw new Error(`[WebUI] interaction hydration requires exactly one compiler-marked root; found ${roots.length}.`);
93
+ }
94
+ const root = roots[0];
95
+ root.removeAttribute('data-webui-interaction');
96
+ compilerRoot = new WeakRef(root);
97
+ return root;
98
+ }
99
+ function compilerMarkedRoots() {
100
+ const matches = [];
101
+ const scopes = [document];
102
+ for (let scopeIndex = 0; scopeIndex < scopes.length; scopeIndex++) {
103
+ const scope = scopes[scopeIndex];
104
+ const marked = scope.querySelectorAll(INTERACTION_ROOT_SELECTOR);
105
+ for (let i = 0; i < marked.length; i++)
106
+ matches.push(marked[i]);
107
+ const elements = scope.querySelectorAll('*');
108
+ for (let i = 0; i < elements.length; i++) {
109
+ const shadowRoot = elements[i].shadowRoot;
110
+ if (shadowRoot)
111
+ scopes.push(shadowRoot);
112
+ }
113
+ }
114
+ return matches;
115
+ }
116
+ function cloneClick(event, target, boundary) {
117
+ const ownerDocument = 'ownerDocument' in target
118
+ ? target
119
+ .ownerDocument
120
+ : undefined;
121
+ const view = (ownerDocument?.defaultView);
122
+ const MouseEventConstructor = view?.MouseEvent
123
+ ?? (typeof MouseEvent === 'function' ? MouseEvent : undefined);
124
+ if (!MouseEventConstructor
125
+ || !(event instanceof MouseEventConstructor)
126
+ || !event.cancelable
127
+ || event.defaultPrevented
128
+ || event.button !== 0
129
+ || event.altKey
130
+ || event.ctrlKey
131
+ || event.metaKey
132
+ || event.shiftKey) {
133
+ return null;
134
+ }
135
+ const PointerEventConstructor = view?.PointerEvent
136
+ ?? (typeof PointerEvent === 'function' ? PointerEvent : undefined);
137
+ const replayEvent = PointerEventConstructor
138
+ && event instanceof PointerEventConstructor
139
+ ? new PointerEventConstructor('click', event)
140
+ : new MouseEventConstructor('click', event);
141
+ interactionReplays.set(replayEvent, {
142
+ boundary,
143
+ previous: interactionReplays.get(event),
144
+ });
145
+ return replayEvent;
146
+ }
147
+ function hasTraversed(event, boundary) {
148
+ let trail = interactionReplays.get(event);
149
+ while (trail) {
150
+ if (trail.boundary === boundary)
151
+ return true;
152
+ trail = trail.previous;
153
+ }
154
+ return false;
155
+ }
156
+ function reportFailure(error, onError) {
157
+ if (onError) {
158
+ try {
159
+ onError(error);
160
+ }
161
+ catch (onErrorFailure) {
162
+ console.error('[WebUI] interaction hydration error handler failed:', onErrorFailure);
163
+ }
164
+ return;
165
+ }
166
+ console.error(`[WebUI] interaction hydration failed: ${error instanceof Error ? error.message : String(error)}`);
167
+ }
@@ -1,8 +1,22 @@
1
- import { TemplateElement } from './template-element.js';
1
+ import { consumePendingParentState, TemplateElement, } from './template-element.js';
2
+ import { hasComponentStyleWork } from './element/styles.js';
2
3
  import { getTemplateRegistry } from './template.js';
3
4
  import { templateNeedsStaticHost } from './template-roots.js';
5
+ import { ACTIVATION_STATIC_HOST_OPT_OUT, isStreamingHydrationMode, STREAMED_HOST_ATTR, STREAMING_BOUNDARY_ACTIVATE, } from './streaming-mode.js';
4
6
  import { TEMPLATES_REGISTERED_EVENT, templateRegistrationDetail, } from './template-events.js';
5
7
  let runtimeInstalled = false;
8
+ function applyPendingNoopHostState(host) {
9
+ const pending = consumePendingParentState(host);
10
+ if (!pending)
11
+ return;
12
+ const target = host;
13
+ const keys = Object.keys(pending.values);
14
+ for (let i = 0; i < keys.length; i++) {
15
+ const key = keys[i];
16
+ if (!Object.hasOwn(target, key))
17
+ target[key] = pending.values[key];
18
+ }
19
+ }
6
20
  function defineTemplateHost(tag, meta) {
7
21
  const w = window;
8
22
  if (!w.__webui)
@@ -11,6 +25,22 @@ function defineTemplateHost(tag, meta) {
11
25
  w.__webui.templates = {};
12
26
  if (!w.__webui.templates[tag])
13
27
  w.__webui.templates[tag] = meta;
28
+ if (templateIsNoopHost(meta) && !hasComponentStyleWork(tag)) {
29
+ customElements.define(tag, class extends HTMLElement {
30
+ connectedCallback() {
31
+ if (isStreamingHydrationMode() &&
32
+ this.hasAttribute(STREAMED_HOST_ATTR)) {
33
+ return;
34
+ }
35
+ applyPendingNoopHostState(this);
36
+ }
37
+ [STREAMING_BOUNDARY_ACTIVATE]() {
38
+ applyPendingNoopHostState(this);
39
+ return ACTIVATION_STATIC_HOST_OPT_OUT;
40
+ }
41
+ });
42
+ return;
43
+ }
14
44
  class StaticTemplateHost extends TemplateElement {
15
45
  $afterExternalStateWrite(applied) {
16
46
  if (applied)
@@ -28,6 +58,18 @@ function defineTemplateHost(tag, meta) {
28
58
  }
29
59
  StaticTemplateHost.define(tag);
30
60
  }
61
+ function templateIsNoopHost(meta) {
62
+ return meta.h.length === 0
63
+ && meta.tx === undefined
64
+ && meta.a === undefined
65
+ && meta.c === undefined
66
+ && meta.r === undefined
67
+ && meta.eg === undefined
68
+ && meta.b === undefined
69
+ && meta.re === undefined
70
+ && meta.tr === undefined
71
+ && meta.sd === undefined;
72
+ }
31
73
  function defineMissingTemplateHost(tag, meta) {
32
74
  if (!templateNeedsStaticHost(meta) ||
33
75
  window.__webui?.templateHostExclusions?.has(tag) ||
@@ -1,2 +1,2 @@
1
- import type { PendingBoundaryUpdates } from './streaming-deferred.js';
2
- export declare function activateRootsBetween(startMarker: Comment, endMarker: Comment, state: Record<string, unknown> | undefined, updates?: PendingBoundaryUpdates): void;
1
+ import type { PendingBoundaryUpdates, SpanBypass } from './streaming-deferred.js';
2
+ export declare function activateRootsBetween(startMarker: Comment, endMarker: Comment, state: Record<string, unknown> | undefined, updates?: PendingBoundaryUpdates, bypass?: SpanBypass): void;
@@ -1,11 +1,13 @@
1
1
  import { abandonDeferredRange, } from './streaming-cleanup.js';
2
2
  import { activateDeferredTree, } from './streaming-deferred.js';
3
- export function activateRootsBetween(startMarker, endMarker, state, updates) {
3
+ export function activateRootsBetween(startMarker, endMarker, state, updates, bypass) {
4
4
  const root = startMarker.parentNode;
5
5
  if (!root) {
6
6
  throw new Error('boundary start marker was detached before activation');
7
7
  }
8
- const failure = activateDeferredTree(startMarker.nextSibling, root, endMarker, state, updates ? { updates, countRetention: true } : undefined);
8
+ const failure = activateDeferredTree(startMarker.nextSibling, root, endMarker, state, updates || bypass
9
+ ? { updates, bypass, countRetention: updates !== undefined }
10
+ : undefined);
9
11
  if (!failure)
10
12
  return;
11
13
  abandonDeferredRange(startMarker, endMarker);
@@ -1,2 +1,2 @@
1
- import type { BoundaryBootstrap } from './streaming-protocol.js';
2
- export declare function applyBoundaryBootstrap(bootstrap: BoundaryBootstrap): void;
1
+ import type { BoundaryBootstrap, SpanCompletionPayload } from './streaming-protocol.js';
2
+ export declare function applyBoundaryBootstrap(bootstrap: BoundaryBootstrap | SpanCompletionPayload): void;
@@ -1,5 +1,7 @@
1
1
  import { registerTemplateData } from './template.js';
2
+ import { registerComponentStyles } from './element/styles.js';
2
3
  export function applyBoundaryBootstrap(bootstrap) {
4
+ registerComponentStyles(bootstrap.componentStyles);
3
5
  if (bootstrap.templates)
4
6
  registerTemplateData(bootstrap.templates);
5
7
  const w = window;
@@ -8,7 +10,11 @@ export function applyBoundaryBootstrap(bootstrap) {
8
10
  const keys = Object.keys(bootstrap);
9
11
  for (let i = 0; i < keys.length; i++) {
10
12
  const key = keys[i];
11
- if (key === 'templates' || key === 'state')
13
+ if (key === 'templates' ||
14
+ key === 'state' ||
15
+ key === 'componentStyles' ||
16
+ key === 'declarationId' ||
17
+ key === 'enclosingSpanInstanceId')
12
18
  continue;
13
19
  if (key === 'inventory') {
14
20
  w.__webui.inventory = mergeInventory(w.__webui.inventory, bootstrap.inventory);
@@ -1,4 +1,5 @@
1
1
  export declare function abandonDeferredElement(el: Element): void;
2
+ export declare function removeStreamingAttributes(el: Element): void;
2
3
  export declare function abandonDeferredDescendants(root: Element): void;
3
4
  export declare function abandonDeferredRange(startMarker: Comment, endMarker: Node): void;
4
5
  export declare function abandonDeferredDocumentRoots(): void;
@@ -1,13 +1,19 @@
1
- import { firstNodeWithin, MAX_MARKER_SCAN_NODES, nextWithinRoot, safeRemoveAttribute, streamingErrorMessage, } from './streaming-dom.js';
1
+ import { BOUNDARY_SCRIPT_ATTR, BOUNDARY_START_PREFIX, firstNodeWithin, isRangeEndMarker, MAX_MARKER_SCAN_NODES, nextAfterSubtreeWithin, nextWithinRoot, safeRemoveAttribute, safeRemove, SPAN_START_PREFIX, STREAMING_ENCLOSING_SPAN_ATTR, STREAMING_SPAN_HOST_ATTR, streamingErrorMessage, } from './streaming-dom.js';
2
2
  import { STREAMED_HOST_ATTR } from './streaming-mode.js';
3
3
  const STREAMING_BOUNDARY_ABANDON = Symbol.for('microsoft.webui.boundaryAbandon');
4
+ const STREAMING_ROOT_ATTRS = [
5
+ STREAMED_HOST_ATTR,
6
+ STREAMING_SPAN_HOST_ATTR,
7
+ STREAMING_ENCLOSING_SPAN_ATTR,
8
+ ];
4
9
  export function abandonDeferredElement(el) {
10
+ const marked = hasStreamingAttribute(el);
5
11
  try {
6
- if (!el.hasAttribute(STREAMED_HOST_ATTR))
12
+ if (!marked)
7
13
  return;
8
14
  }
9
15
  catch {
10
- safeRemoveAttribute(el, STREAMED_HOST_ATTR);
16
+ removeStreamingAttributes(el);
11
17
  return;
12
18
  }
13
19
  try {
@@ -19,7 +25,19 @@ export function abandonDeferredElement(el) {
19
25
  console.error(`[WebUI] streaming: abandon failed for <${el.tagName?.toLowerCase?.() ?? '?'}>: ${streamingErrorMessage(error)}`);
20
26
  }
21
27
  finally {
22
- safeRemoveAttribute(el, STREAMED_HOST_ATTR);
28
+ removeStreamingAttributes(el);
29
+ }
30
+ }
31
+ function hasStreamingAttribute(el) {
32
+ for (let i = 0; i < STREAMING_ROOT_ATTRS.length; i++) {
33
+ if (el.hasAttribute(STREAMING_ROOT_ATTRS[i]))
34
+ return true;
35
+ }
36
+ return false;
37
+ }
38
+ export function removeStreamingAttributes(el) {
39
+ for (let i = 0; i < STREAMING_ROOT_ATTRS.length; i++) {
40
+ safeRemoveAttribute(el, STREAMING_ROOT_ATTRS[i]);
23
41
  }
24
42
  }
25
43
  export function abandonDeferredDescendants(root) {
@@ -47,20 +65,54 @@ export function abandonDeferredDocumentRoots() {
47
65
  typeof document.getElementsByTagName !== 'function') {
48
66
  return;
49
67
  }
68
+ const documentRoot = document.documentElement;
69
+ if (documentRoot) {
70
+ abandonStreamingNodes(documentRoot.firstChild, documentRoot);
71
+ abandonDeferredElement(documentRoot);
72
+ return;
73
+ }
50
74
  const elements = document.getElementsByTagName('*');
51
- let visited = 0;
52
- for (let i = 0; i < elements.length && visited < MAX_MARKER_SCAN_NODES; i++) {
75
+ const limit = elements.length < MAX_MARKER_SCAN_NODES
76
+ ? elements.length
77
+ : MAX_MARKER_SCAN_NODES;
78
+ for (let i = 0; i < limit; i++) {
53
79
  const el = elements[i];
54
- visited++;
55
80
  abandonDeferredElement(el);
56
81
  const shadowRoot = el.shadowRoot;
57
- let node = shadowRoot?.firstChild ?? null;
58
- while (node && visited < MAX_MARKER_SCAN_NODES) {
59
- visited++;
60
- if (node.nodeType === 1) {
61
- abandonDeferredElement(node);
62
- }
63
- node = nextWithinRoot(node, shadowRoot);
82
+ if (shadowRoot)
83
+ abandonStreamingNodes(shadowRoot.firstChild, shadowRoot);
84
+ }
85
+ }
86
+ function abandonStreamingNodes(first, root) {
87
+ let node = first;
88
+ let visited = 0;
89
+ while (node && visited < MAX_MARKER_SCAN_NODES) {
90
+ visited++;
91
+ if (node.nodeType === 8) {
92
+ const next = nextWithinRoot(node, root);
93
+ if (isStreamingMarker(node.data))
94
+ safeRemove(node);
95
+ node = next;
96
+ continue;
64
97
  }
98
+ if (node.nodeType === 1) {
99
+ const el = node;
100
+ const scaffold = el.tagName === 'WEBUI-HYDRATE' ||
101
+ el.hasAttribute(BOUNDARY_SCRIPT_ATTR);
102
+ const next = scaffold
103
+ ? nextAfterSubtreeWithin(node, root)
104
+ : nextWithinRoot(node, root);
105
+ abandonDeferredElement(el);
106
+ if (scaffold)
107
+ safeRemove(el);
108
+ node = next;
109
+ continue;
110
+ }
111
+ node = nextWithinRoot(node, root);
65
112
  }
66
113
  }
114
+ function isStreamingMarker(data) {
115
+ return data.startsWith(BOUNDARY_START_PREFIX) ||
116
+ data.startsWith(SPAN_START_PREFIX) ||
117
+ isRangeEndMarker(data);
118
+ }
@@ -1,7 +1,8 @@
1
- import { elementHasPendingStateForTests, pendingTagWaiterCountForTests, pendingUndefinedRootCountForTests } from './streaming-deferred.js';
1
+ import { elementHasPendingStateForTests, pendingBarrierRootCountForTests, pendingTagWaiterCountForTests, pendingUndefinedRootCountForTests } from './streaming-deferred.js';
2
+ import { openSpanCountForTests } from './streaming-spans.js';
2
3
  export declare function enqueueStreamingSentinel(sentinel: Element): void;
3
4
  export declare function installStreamingTruncationGuard(): void;
4
5
  export declare function resetStreamingCoordinatorStateForTests(): void;
5
6
  export declare function isStreamingHaltedForTests(): boolean;
6
7
  export declare function streamingRetentionStateForTests(): readonly [number, number];
7
- export { elementHasPendingStateForTests, pendingTagWaiterCountForTests, pendingUndefinedRootCountForTests, };
8
+ export { elementHasPendingStateForTests, openSpanCountForTests, pendingBarrierRootCountForTests, pendingTagWaiterCountForTests, pendingUndefinedRootCountForTests, };