@fluid-app/widget-runtime 0.1.3 → 0.1.5

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 (50) hide show
  1. package/README.md +36 -0
  2. package/dist/index.cjs +3 -2
  3. package/dist/index.d.cts +3 -2
  4. package/dist/index.d.mts +3 -2
  5. package/dist/index.mjs +3 -3
  6. package/dist/{remote-dom-renderer-CKvnUH8G.d.mts → remote-dom-renderer-BGIZ0FRi.d.mts} +35 -1
  7. package/dist/remote-dom-renderer-BGIZ0FRi.d.mts.map +1 -0
  8. package/dist/{remote-dom-renderer-f7OWlvvv.cjs → remote-dom-renderer-BlZKfMAA.cjs} +340 -172
  9. package/dist/remote-dom-renderer-BlZKfMAA.cjs.map +1 -0
  10. package/dist/{remote-dom-renderer-CeYSItkU.d.cts → remote-dom-renderer-BoB9jd3B.d.cts} +35 -1
  11. package/dist/remote-dom-renderer-BoB9jd3B.d.cts.map +1 -0
  12. package/dist/{remote-dom-renderer-CpdKWANy.mjs → remote-dom-renderer-Dn3pfVsl.mjs} +340 -172
  13. package/dist/remote-dom-renderer-Dn3pfVsl.mjs.map +1 -0
  14. package/dist/remote-dom-ui-contract-B140dMly.d.mts +632 -0
  15. package/dist/remote-dom-ui-contract-B140dMly.d.mts.map +1 -0
  16. package/dist/remote-dom-ui-contract-BLOyWbT2.d.cts +632 -0
  17. package/dist/remote-dom-ui-contract-BLOyWbT2.d.cts.map +1 -0
  18. package/dist/remote-dom-values-B5UZPDRy.cjs +849 -0
  19. package/dist/remote-dom-values-B5UZPDRy.cjs.map +1 -0
  20. package/dist/remote-dom-values-D4AgBNWr.d.cts.map +1 -1
  21. package/dist/remote-dom-values-EwoLXnSn.d.mts.map +1 -1
  22. package/dist/remote-dom-values-wEbrw5iW.mjs +790 -0
  23. package/dist/remote-dom-values-wEbrw5iW.mjs.map +1 -0
  24. package/dist/remote-dom.cjs +2 -2
  25. package/dist/remote-dom.d.cts +1 -1
  26. package/dist/remote-dom.d.mts +1 -1
  27. package/dist/remote-dom.mjs +2 -2
  28. package/dist/{worker-react-renderer-BKzig_Vy.mjs → worker-react-renderer-BV6ExKCS.mjs} +17 -40
  29. package/dist/worker-react-renderer-BV6ExKCS.mjs.map +1 -0
  30. package/dist/{worker-react-renderer-BvHZ2o0B.cjs → worker-react-renderer-CR-TvErh.cjs} +18 -41
  31. package/dist/worker-react-renderer-CR-TvErh.cjs.map +1 -0
  32. package/dist/worker.cjs +18 -3
  33. package/dist/worker.cjs.map +1 -1
  34. package/dist/worker.d.cts +17 -2
  35. package/dist/worker.d.cts.map +1 -1
  36. package/dist/worker.d.mts +17 -1
  37. package/dist/worker.d.mts.map +1 -1
  38. package/dist/worker.mjs +17 -3
  39. package/dist/worker.mjs.map +1 -1
  40. package/package.json +1 -1
  41. package/dist/remote-dom-renderer-CKvnUH8G.d.mts.map +0 -1
  42. package/dist/remote-dom-renderer-CeYSItkU.d.cts.map +0 -1
  43. package/dist/remote-dom-renderer-CpdKWANy.mjs.map +0 -1
  44. package/dist/remote-dom-renderer-f7OWlvvv.cjs.map +0 -1
  45. package/dist/remote-dom-values-BSAxrOE7.mjs +0 -238
  46. package/dist/remote-dom-values-BSAxrOE7.mjs.map +0 -1
  47. package/dist/remote-dom-values-DbgIedSg.cjs +0 -297
  48. package/dist/remote-dom-values-DbgIedSg.cjs.map +0 -1
  49. package/dist/worker-react-renderer-BKzig_Vy.mjs.map +0 -1
  50. package/dist/worker-react-renderer-BvHZ2o0B.cjs.map +0 -1
@@ -1,8 +1,10 @@
1
1
  import { n as release, r as retain, t as Thread } from "./Thread-BiW7rXts.mjs";
2
- import { a as REMOTE_DOM_SEARCH_SORT_EVENTS, c as isRemoteDomSerializableValue, l as isSafeRemoteDomPropertyKey, o as SUPPORTED_REMOTE_DOM_EVENTS, r as REMOTE_DOM_ALLOWED_TAG_NAMES, s as createRemoteDomUrlOriginPolicy } from "./remote-dom-values-BSAxrOE7.mjs";
2
+ import { a as isRemoteDomSerializableValue, c as REMOTE_DOM_SUPPORTED_UI_CONTRACT_VERSIONS, i as createRemoteDomUrlOriginPolicy, l as REMOTE_DOM_UI_COMPONENTS, o as isSafeRemoteDomPropertyKey, r as SUPPORTED_REMOTE_DOM_EVENTS, t as REMOTE_DOM_ALLOWED_TAG_NAMES, u as getRemoteDomUiComponent } from "./remote-dom-values-wEbrw5iW.mjs";
3
3
  import { MUTATION_TYPE_INSERT_CHILD, MUTATION_TYPE_REMOVE_CHILD, MUTATION_TYPE_UPDATE_PROPERTY, MUTATION_TYPE_UPDATE_TEXT, NODE_TYPE_ELEMENT, NODE_TYPE_TEXT, ROOT_ID, UPDATE_PROPERTY_TYPE_ATTRIBUTE, UPDATE_PROPERTY_TYPE_EVENT_LISTENER, UPDATE_PROPERTY_TYPE_PROPERTY } from "@remote-dom/core";
4
- import { remoteId } from "@remote-dom/core/elements";
5
- import { DOMRemoteReceiver } from "@remote-dom/core/receivers";
4
+ import { RemoteFragmentRenderer, RemoteReceiver, RemoteRootRenderer, renderRemoteNode, useRemoteReceived } from "@remote-dom/react/host";
5
+ import { createElement, useEffect, useLayoutEffect, useRef } from "react";
6
+ import { flushSync } from "react-dom";
7
+ import { createRoot } from "react-dom/client";
6
8
  import postcssParse from "postcss/lib/parse";
7
9
  import valueParser from "postcss-value-parser";
8
10
  //#region src/remote-dom-sanitizer.ts
@@ -306,7 +308,6 @@ const MEDIA_SOURCE_TAGS = new Set([
306
308
  "video"
307
309
  ]);
308
310
  const SUPPORTED_REMOTE_DOM_EVENT_SET = new Set(SUPPORTED_REMOTE_DOM_EVENTS);
309
- const REMOTE_DOM_SEARCH_SORT_EVENT_SET = new Set(REMOTE_DOM_SEARCH_SORT_EVENTS);
310
311
  function sanitizeRemoteDomTagName(tagName) {
311
312
  if (typeof tagName !== "string") return void 0;
312
313
  const normalizedTagName = tagName.trim().toLowerCase();
@@ -322,7 +323,7 @@ function sanitizeRemoteDomAttributes(tagName, attributes, options = {}) {
322
323
  }
323
324
  return sanitizedAttributes;
324
325
  }
325
- function sanitizeRemoteDomProperty(tagName, rawName, rawValue) {
326
+ function sanitizeRemoteDomProperty(tagName, rawName, rawValue, options = {}) {
326
327
  if (typeof rawName !== "string") return void 0;
327
328
  const normalizedTagName = tagName.trim().toLowerCase();
328
329
  const name = rawName.trim().toLowerCase();
@@ -337,45 +338,54 @@ function sanitizeRemoteDomProperty(tagName, rawName, rawValue) {
337
338
  value: rawValue
338
339
  };
339
340
  }
340
- if (normalizedTagName === "fluid-ui-search-sort") {
341
- if (name === "searchvalue" && typeof rawValue === "string") return {
342
- name: "searchValue",
343
- value: rawValue
344
- };
345
- if (name === "placeholder") return typeof rawValue === "string" || rawValue === void 0 ? {
346
- name: "placeholder",
347
- value: rawValue
348
- } : void 0;
349
- if (name === "sortoptions") {
350
- const value = sanitizeSearchSortOptions(rawValue);
351
- return value === void 0 && rawValue !== void 0 ? void 0 : {
352
- name: "sortOptions",
341
+ const definition = getRemoteDomUiComponent(normalizedTagName);
342
+ if (definition) {
343
+ const propertyEntry = Object.entries(definition.properties).find(([propertyName]) => propertyName.toLowerCase() === name);
344
+ if (!propertyEntry) return void 0;
345
+ const [propertyName, codec] = propertyEntry;
346
+ if (codec === "navigation-url" && rawValue !== void 0) {
347
+ const value = sanitizeRemoteDomUrl(rawValue, {
348
+ allowHashOnly: true,
349
+ allowMailTo: false,
350
+ allowRelative: true,
351
+ allowTel: false,
352
+ allowedHttpOriginGroups: ["navigation"],
353
+ ...options.allowedAnchorUrls ? { allowedHttpUrls: options.allowedAnchorUrls } : {},
354
+ originPolicy: options.urlOriginPolicy,
355
+ relativeUrlBase: "navigation"
356
+ });
357
+ return value === void 0 ? void 0 : {
358
+ name: propertyName,
353
359
  value
354
360
  };
355
361
  }
356
- if (name === "sortvalue") return typeof rawValue === "string" || rawValue === void 0 ? {
357
- name: "sortValue",
358
- value: rawValue
359
- } : void 0;
360
- }
361
- if (normalizedTagName === "fluid-spacer-widget") {
362
- if (name === "customheight") return typeof rawValue === "string" || rawValue === void 0 ? {
363
- name: "customHeight",
362
+ if (codec === "url" && rawValue !== void 0) {
363
+ const value = sanitizeRemoteDomUrl(rawValue, {
364
+ allowHashOnly: false,
365
+ allowMailTo: false,
366
+ allowRelative: true,
367
+ allowTel: false,
368
+ allowedHttpOriginGroups: ["artifact", "media"],
369
+ originPolicy: options.urlOriginPolicy,
370
+ relativeUrlBase: "artifact"
371
+ });
372
+ return value === void 0 ? void 0 : {
373
+ name: propertyName,
374
+ value
375
+ };
376
+ }
377
+ return isRemoteDomUiPropertyValue(rawValue, codec) ? {
378
+ name: propertyName,
364
379
  value: rawValue
365
380
  } : void 0;
366
- if (name === "previewmode" && typeof rawValue === "boolean") return {
367
- name: "previewMode",
368
- value: rawValue
369
- };
370
381
  }
371
382
  }
372
383
  function isRemoteDomPropertyName(tagName, rawName) {
373
384
  const normalizedTagName = tagName.trim().toLowerCase();
374
385
  const name = rawName.trim().toLowerCase();
375
386
  if (normalizedTagName === "input") return name === "checked" || name === "value";
376
- if (normalizedTagName === "fluid-ui-search-sort") return name === "searchvalue" || name === "placeholder" || name === "sortoptions" || name === "sortvalue";
377
- if (normalizedTagName === "fluid-spacer-widget") return name === "customheight" || name === "previewmode";
378
- return false;
387
+ const definition = getRemoteDomUiComponent(normalizedTagName);
388
+ return Boolean(definition && Object.keys(definition.properties).some((propertyName) => propertyName.toLowerCase() === name));
379
389
  }
380
390
  function sanitizeRemoteDomAttribute(tagName, rawName, rawValue, options = {}) {
381
391
  if (typeof rawName !== "string") return void 0;
@@ -465,7 +475,8 @@ function sanitizeRemoteDomCssUrlReferences(value, options = {}) {
465
475
  }
466
476
  function isSupportedRemoteDomEventName(eventName, tagName) {
467
477
  if (SUPPORTED_REMOTE_DOM_EVENT_SET.has(eventName)) return true;
468
- return tagName?.trim().toLowerCase() === "fluid-ui-search-sort" && REMOTE_DOM_SEARCH_SORT_EVENT_SET.has(eventName);
478
+ const definition = tagName ? getRemoteDomUiComponent(tagName.trim().toLowerCase()) : void 0;
479
+ return Boolean(definition?.events.some((event) => event.eventName === eventName));
469
480
  }
470
481
  function serializeRemoteDomEvent(event, targetId, currentTargetId, currentTargetTagName) {
471
482
  if (!isSupportedRemoteDomEventName(event.type, currentTargetTagName)) return;
@@ -484,20 +495,22 @@ function serializeRemoteDomEvent(event, targetId, currentTargetId, currentTarget
484
495
  ...readCustomEventDetail(event)
485
496
  };
486
497
  }
487
- function sanitizeSearchSortOptions(value) {
488
- if (!Array.isArray(value)) return void 0;
489
- const options = [];
490
- for (const option of value) {
491
- if (!isObjectRecord$1(option) || typeof option.label !== "string" || typeof option.value !== "string") return;
492
- options.push({
493
- label: option.label,
494
- value: option.value
495
- });
498
+ function isRemoteDomUiPropertyValue(value, codec) {
499
+ if (value === void 0) return true;
500
+ switch (codec) {
501
+ case "boolean": return typeof value === "boolean";
502
+ case "finite-number-array": return Array.isArray(value) && value.every((item) => typeof item === "number" && Number.isFinite(item));
503
+ case "finite-number": return typeof value === "number" && Number.isFinite(value);
504
+ case "navigation-url": return false;
505
+ case "option-array": return Array.isArray(value) && value.every((item) => isObjectRecord$1(item) && typeof item.label === "string" && typeof item.value === "string" && Object.keys(item).every((key) => key === "label" || key === "value"));
506
+ case "string": return typeof value === "string";
507
+ case "string-array": return Array.isArray(value) && value.every((item) => typeof item === "string");
508
+ case "string-or-string-array": return typeof value === "string" || Array.isArray(value) && value.every((item) => typeof item === "string");
509
+ case "url": return false;
496
510
  }
497
- return options;
498
511
  }
499
512
  function readCustomEventDetail(event) {
500
- if (!(event instanceof CustomEvent) || !REMOTE_DOM_SEARCH_SORT_EVENT_SET.has(event.type)) return {};
513
+ if (!(event instanceof CustomEvent)) return {};
501
514
  if (!isRemoteDomSerializableValue(event.detail)) return {};
502
515
  try {
503
516
  const detailJson = JSON.stringify(event.detail);
@@ -561,6 +574,7 @@ function hasControlCharacter(value) {
561
574
  }
562
575
  function isAllowedAttributeName(tagName, name) {
563
576
  if (name.length === 0 || name.startsWith("on") || name === "style" || name === "srcdoc" || name.includes(":") || !/^[a-z][a-z0-9_.-]*$/u.test(name)) return false;
577
+ if (name === "data-fluid-remote-dom-node-id") return false;
564
578
  if (name.startsWith("data-")) return true;
565
579
  if (GLOBAL_ALLOWED_ATTRIBUTES.has(name)) return true;
566
580
  return TAG_ALLOWED_ATTRIBUTES[tagName]?.has(name) === true;
@@ -995,14 +1009,26 @@ function normalizeUpdatePropertyRecordForHost(options, state, mutation, pendingE
995
1009
  eventName: property,
996
1010
  listener
997
1011
  });
998
- return { ok: true };
1012
+ return {
1013
+ ok: true,
1014
+ record: [
1015
+ MUTATION_TYPE_UPDATE_PROPERTY,
1016
+ id,
1017
+ property,
1018
+ listener,
1019
+ UPDATE_PROPERTY_TYPE_EVENT_LISTENER
1020
+ ]
1021
+ };
999
1022
  }
1000
1023
  if (value !== void 0 && !isRemoteDomSerializableValue(value)) return {
1001
1024
  ok: false,
1002
1025
  message: `Remote DOM mutation property "${property}" was not serializable.`
1003
1026
  };
1004
1027
  if (updateType === UPDATE_PROPERTY_TYPE_PROPERTY) {
1005
- const sanitizedProperty = sanitizeRemoteDomProperty(node.localName, property, value);
1028
+ const sanitizedProperty = sanitizeRemoteDomProperty(node.localName, property, value, {
1029
+ ...options.allowedAnchorUrls ? { allowedAnchorUrls: options.allowedAnchorUrls } : {},
1030
+ urlOriginPolicy: options.urlOriginPolicy
1031
+ });
1006
1032
  if (sanitizedProperty) return {
1007
1033
  ok: true,
1008
1034
  record: [
@@ -1082,7 +1108,7 @@ function normalizeRemoteNodeForHost(options, state, value, seen, pendingEventLis
1082
1108
  ok: false,
1083
1109
  message: `Remote DOM element "${value.id}" used an unsupported tag name.`
1084
1110
  };
1085
- queueRemoteNodeEventListeners(value.id, tagName, value.eventListeners, pendingEventListenerUpdates);
1111
+ const eventListeners = sanitizeRemoteNodeEventListeners(value.id, tagName, value.eventListeners, pendingEventListenerUpdates);
1086
1112
  if (!Array.isArray(value.children)) return {
1087
1113
  ok: false,
1088
1114
  message: `Remote DOM element "${value.id}" did not contain a valid children array.`
@@ -1101,7 +1127,10 @@ function normalizeRemoteNodeForHost(options, state, value, seen, pendingEventLis
1101
1127
  const mutableAttributeProperties = { ...properties };
1102
1128
  attributeProperties = mutableAttributeProperties;
1103
1129
  for (const [rawName, rawValue] of Object.entries(value.properties)) {
1104
- const property = sanitizeRemoteDomProperty(tagName, rawName, rawValue);
1130
+ const property = sanitizeRemoteDomProperty(tagName, rawName, rawValue, {
1131
+ ...options.allowedAnchorUrls ? { allowedAnchorUrls: options.allowedAnchorUrls } : {},
1132
+ urlOriginPolicy: options.urlOriginPolicy
1133
+ });
1105
1134
  if (property) {
1106
1135
  remoteProperties[property.name] = property.value;
1107
1136
  delete mutableAttributeProperties[rawName];
@@ -1128,13 +1157,15 @@ function normalizeRemoteNodeForHost(options, state, value, seen, pendingEventLis
1128
1157
  element: tagName,
1129
1158
  attributes: remoteAttributes,
1130
1159
  properties: remoteProperties,
1160
+ eventListeners,
1131
1161
  children
1132
1162
  },
1133
1163
  nodeCount
1134
1164
  };
1135
1165
  }
1136
- function queueRemoteNodeEventListeners(id, tagName, eventListeners, pendingEventListenerUpdates) {
1137
- if (!isObjectRecord(eventListeners)) return;
1166
+ function sanitizeRemoteNodeEventListeners(id, tagName, eventListeners, pendingEventListenerUpdates) {
1167
+ const sanitized = {};
1168
+ if (!isObjectRecord(eventListeners)) return sanitized;
1138
1169
  for (const [eventName, listener] of Object.entries(eventListeners)) {
1139
1170
  if (!isSafeRemoteDomPropertyKey(eventName) || !isSupportedRemoteDomEventName(eventName, tagName)) continue;
1140
1171
  pendingEventListenerUpdates.push({
@@ -1142,7 +1173,9 @@ function queueRemoteNodeEventListeners(id, tagName, eventListeners, pendingEvent
1142
1173
  eventName,
1143
1174
  listener: typeof listener === "function" ? listener : null
1144
1175
  });
1176
+ if (typeof listener === "function") sanitized[eventName] = listener;
1145
1177
  }
1178
+ return sanitized;
1146
1179
  }
1147
1180
  function validateRemoteNodeId(id, seen) {
1148
1181
  if (id === ROOT_ID) return {
@@ -1680,8 +1713,207 @@ function mirrorClasses({ host, sourceElement, classNames, managedClasses, origin
1680
1713
  }
1681
1714
  //#endregion
1682
1715
  //#region src/remote-dom-renderer.ts
1683
- const DEFAULT_MAX_MUTATION_BATCH_SIZE = 100;
1716
+ const DEFAULT_MAX_MUTATION_BATCH_SIZE = 1e3;
1684
1717
  const DEFAULT_MAX_DOM_NODE_COUNT = 1e3;
1718
+ const BOOLEAN_ATTRIBUTE_REACT_PROPERTIES = new Map([
1719
+ ["allowfullscreen", "allowFullScreen"],
1720
+ ["async", "async"],
1721
+ ["autofocus", "autoFocus"],
1722
+ ["autoplay", "autoPlay"],
1723
+ ["checked", "checked"],
1724
+ ["controls", "controls"],
1725
+ ["default", "default"],
1726
+ ["defer", "defer"],
1727
+ ["disabled", "disabled"],
1728
+ ["disablepictureinpicture", "disablePictureInPicture"],
1729
+ ["disableremoteplayback", "disableRemotePlayback"],
1730
+ ["formnovalidate", "formNoValidate"],
1731
+ ["hidden", "hidden"],
1732
+ ["inert", "inert"],
1733
+ ["itemscope", "itemScope"],
1734
+ ["loop", "loop"],
1735
+ ["multiple", "multiple"],
1736
+ ["muted", "muted"],
1737
+ ["nomodule", "noModule"],
1738
+ ["novalidate", "noValidate"],
1739
+ ["open", "open"],
1740
+ ["playsinline", "playsInline"],
1741
+ ["readonly", "readOnly"],
1742
+ ["required", "required"],
1743
+ ["reversed", "reversed"],
1744
+ ["selected", "selected"]
1745
+ ]);
1746
+ function createRemoteComponentRenderers(options, isDisposed) {
1747
+ const renderers = /* @__PURE__ */ new Map();
1748
+ const fluidTags = new Set(REMOTE_DOM_UI_COMPONENTS.map(({ tagName }) => tagName));
1749
+ for (const tagName of REMOTE_DOM_ALLOWED_TAG_NAMES) {
1750
+ if (fluidTags.has(tagName)) continue;
1751
+ renderers.set(tagName, createNativeRemoteComponentRenderer(tagName, options, isDisposed));
1752
+ }
1753
+ for (const definition of REMOTE_DOM_UI_COMPONENTS) {
1754
+ if (definition.contractVersion !== "1") continue;
1755
+ renderers.set(definition.tagName, createNativeRemoteComponentRenderer(definition.tagName, options, isDisposed));
1756
+ }
1757
+ renderers.set("remote-fragment", RemoteFragmentRenderer);
1758
+ for (const [tagName, renderer] of options.componentRenderers ?? []) {
1759
+ if (!fluidTags.has(tagName)) continue;
1760
+ renderers.set(tagName, renderer);
1761
+ }
1762
+ return renderers;
1763
+ }
1764
+ function createNativeRemoteComponentRenderer(tagName, options, isDisposed) {
1765
+ function NativeRemoteComponent({ element, receiver, components }) {
1766
+ const attachedElement = useRemoteReceived(element, receiver);
1767
+ const elementRef = useRef(null);
1768
+ useEffect(() => {
1769
+ const hostElement = elementRef.current;
1770
+ if (!hostElement || !attachedElement) return;
1771
+ const cleanup = [];
1772
+ for (const [eventName, listener] of Object.entries(attachedElement.eventListeners)) {
1773
+ if (typeof listener !== "function") continue;
1774
+ const handleEvent = (event) => {
1775
+ if (hasCloserRemoteEventListener(receiver, event, attachedElement.id, eventName)) return;
1776
+ deliverRemoteDomEvent({
1777
+ event,
1778
+ eventName,
1779
+ currentTargetId: attachedElement.id,
1780
+ currentTargetTagName: tagName,
1781
+ listener,
1782
+ options,
1783
+ isDisposed
1784
+ });
1785
+ };
1786
+ hostElement.addEventListener(eventName, handleEvent);
1787
+ cleanup.push(() => hostElement.removeEventListener(eventName, handleEvent));
1788
+ }
1789
+ return () => {
1790
+ for (const dispose of cleanup) dispose();
1791
+ };
1792
+ }, [
1793
+ attachedElement,
1794
+ attachedElement?.version,
1795
+ receiver
1796
+ ]);
1797
+ useLayoutEffect(() => {
1798
+ const hostElement = elementRef.current;
1799
+ if (!(hostElement instanceof HTMLInputElement) || !attachedElement) return;
1800
+ const value = attachedElement.properties.value;
1801
+ if (typeof value === "string" && hostElement.value !== value) hostElement.value = value;
1802
+ const checked = attachedElement.properties.checked;
1803
+ if (typeof checked === "boolean" && hostElement.checked !== checked) hostElement.checked = checked;
1804
+ }, [attachedElement, attachedElement?.version]);
1805
+ if (!attachedElement) return null;
1806
+ const domProps = {};
1807
+ for (const [name, value] of Object.entries({
1808
+ ...attachedElement.attributes,
1809
+ ...attachedElement.properties
1810
+ })) {
1811
+ if (tagName === "input" && (name === "checked" || name === "value")) continue;
1812
+ const booleanProperty = BOOLEAN_ATTRIBUTE_REACT_PROPERTIES.get(name);
1813
+ domProps[booleanProperty ?? toReactDomPropertyName(name)] = booleanProperty && typeof value === "string" ? true : value;
1814
+ }
1815
+ domProps["data-fluid-remote-dom-node-id"] = attachedElement.id;
1816
+ domProps.ref = elementRef;
1817
+ const renderOptions = {
1818
+ receiver,
1819
+ components
1820
+ };
1821
+ return createElement(tagName, domProps, ...attachedElement.children.map((child) => renderRemoteNode(child, renderOptions)));
1822
+ }
1823
+ NativeRemoteComponent.displayName = `RemoteDomNative(${tagName})`;
1824
+ return NativeRemoteComponent;
1825
+ }
1826
+ function RemoteRootEventBoundary({ receiver, container, options, isDisposed, children }) {
1827
+ const root = useRemoteReceived(receiver.root, receiver);
1828
+ useEffect(() => {
1829
+ if (!root) return;
1830
+ const cleanup = [];
1831
+ for (const [eventName, listener] of Object.entries(root.eventListeners)) {
1832
+ if (typeof listener !== "function") continue;
1833
+ const handleEvent = (event) => {
1834
+ if (hasCloserRemoteEventListener(receiver, event, ROOT_ID, eventName)) return;
1835
+ deliverRemoteDomEvent({
1836
+ event,
1837
+ eventName,
1838
+ currentTargetId: ROOT_ID,
1839
+ listener,
1840
+ options,
1841
+ isDisposed
1842
+ });
1843
+ };
1844
+ container.addEventListener(eventName, handleEvent, { capture: true });
1845
+ cleanup.push(() => container.removeEventListener(eventName, handleEvent, { capture: true }));
1846
+ }
1847
+ return () => {
1848
+ for (const dispose of cleanup) dispose();
1849
+ };
1850
+ }, [
1851
+ container,
1852
+ isDisposed,
1853
+ options,
1854
+ receiver,
1855
+ root,
1856
+ root?.version
1857
+ ]);
1858
+ return children;
1859
+ }
1860
+ function deliverRemoteDomEvent({ event, eventName, currentTargetId, currentTargetTagName, listener, options, isDisposed }) {
1861
+ if (eventName === "submit" && event.cancelable) event.preventDefault();
1862
+ const targetId = resolveRenderedRemoteNodeId(event) ?? currentTargetId;
1863
+ const payload = serializeRemoteDomEvent(event, targetId, currentTargetId, currentTargetTagName);
1864
+ if (!payload) return;
1865
+ try {
1866
+ const result = listener({
1867
+ targetId,
1868
+ detail: toSerializableEventPayload(payload)
1869
+ });
1870
+ if (isPromiseLike(result)) result.catch((error) => {
1871
+ if (!isDisposed()) reportError(options, "Remote DOM event delivery failed", error);
1872
+ });
1873
+ } catch (error) {
1874
+ if (!isDisposed()) reportError(options, "Remote DOM event delivery failed", error);
1875
+ }
1876
+ }
1877
+ function hasCloserRemoteEventListener(receiver, event, currentTargetId, eventName) {
1878
+ for (const pathTarget of event.composedPath()) {
1879
+ if (!(pathTarget instanceof Element)) continue;
1880
+ const id = pathTarget.getAttribute("data-fluid-remote-dom-node-id");
1881
+ if (!id) continue;
1882
+ if (id === currentTargetId) return false;
1883
+ const remoteNode = receiver.get({ id });
1884
+ if (remoteNode && "eventListeners" in remoteNode && typeof remoteNode.eventListeners[eventName] === "function") return true;
1885
+ }
1886
+ return false;
1887
+ }
1888
+ function resolveRenderedRemoteNodeId(event) {
1889
+ for (const pathTarget of event.composedPath()) {
1890
+ if (!(pathTarget instanceof Element)) continue;
1891
+ const id = pathTarget.getAttribute("data-fluid-remote-dom-node-id");
1892
+ if (id) return id;
1893
+ }
1894
+ }
1895
+ function toReactDomPropertyName(name) {
1896
+ switch (name) {
1897
+ case "class": return "className";
1898
+ case "for": return "htmlFor";
1899
+ case "readonly": return "readOnly";
1900
+ case "tabindex": return "tabIndex";
1901
+ default: return name;
1902
+ }
1903
+ }
1904
+ function getSupportedUiContractVersions(componentRenderers) {
1905
+ return REMOTE_DOM_UI_COMPONENTS.every(({ contractVersion, tagName }) => contractVersion !== "2" || componentRenderers.has(tagName)) ? REMOTE_DOM_SUPPORTED_UI_CONTRACT_VERSIONS : REMOTE_DOM_SUPPORTED_UI_CONTRACT_VERSIONS.filter((version) => version !== "2");
1906
+ }
1907
+ /**
1908
+ * Mounts one Remote DOM widget worker into an isolated shadow root.
1909
+ *
1910
+ * The low-level runtime supports native Remote DOM elements by default. A host
1911
+ * that supports worker-facing Fluid UI components must also provide the full
1912
+ * reviewed {@link MountRemoteDomWidgetOptions.componentRenderers} map, a
1913
+ * containment-aware {@link MountRemoteDomWidgetOptions.wrapRemoteRoot}, and
1914
+ * the matching Fluid component CSS. The runtime advertises Fluid UI contract
1915
+ * v2 only when every v2 renderer is present.
1916
+ */
1685
1917
  function mountRemoteDomWidget(options) {
1686
1918
  const initialProps = options.props ?? null;
1687
1919
  assertSerializable(initialProps, "Initial widget props");
@@ -1728,35 +1960,25 @@ function mountRemoteDomWidget(options) {
1728
1960
  throw error;
1729
1961
  }
1730
1962
  let state;
1731
- const pendingListenerUpdates = /* @__PURE__ */ new Map();
1732
- const connection = createSanitizingRemoteConnection({
1733
- connection: new DOMRemoteReceiver({
1734
- root: shadowRoot,
1735
- call: () => {
1736
- throw new RemoteDomConnectionCallDeniedError("remote element method");
1737
- }
1738
- }).connection,
1739
- ...options.allowedAnchorUrls ? { allowedAnchorUrls: options.allowedAnchorUrls } : {},
1740
- urlOriginPolicy,
1741
- maxMutationBatchSize: options.maxMutationBatchSize ?? DEFAULT_MAX_MUTATION_BATCH_SIZE,
1742
- maxDomNodeCount: options.maxDomNodeCount ?? DEFAULT_MAX_DOM_NODE_COUNT,
1743
- onEventListenerUpdate: (targetId, eventName, listener) => {
1744
- pendingListenerUpdates.set(subscriptionKey(targetId, eventName), {
1745
- targetId,
1746
- eventName,
1747
- listener
1748
- });
1749
- },
1750
- onNodeLimitExceeded: () => {
1751
- reportError(options, "Remote DOM widget exceeded its DOM node limit");
1752
- },
1753
- onInvalidRecord: (message) => reportError(options, message),
1754
- onMutateError: (error) => reportError(options, "Remote DOM mutation could not be applied", error)
1963
+ const receiver = new RemoteReceiver({
1964
+ retain,
1965
+ release
1755
1966
  });
1967
+ const renderContainer = document.createElement("div");
1968
+ renderContainer.setAttribute("data-fluid-remote-dom-render-root", "");
1969
+ const portalContainer = document.createElement("div");
1970
+ portalContainer.setAttribute("data-fluid-remote-dom-portal-container", "");
1971
+ shadowRoot.append(renderContainer, portalContainer);
1756
1972
  const submitListener = createSubmitPreventionListener();
1757
1973
  const navigationListener = (event) => {
1758
1974
  handleNavigationIntent(state, event);
1759
1975
  };
1976
+ const reactRoot = createRoot(renderContainer, { onUncaughtError(error) {
1977
+ reportError(options, "Remote DOM host rendering failed", error);
1978
+ queueMicrotask(() => {
1979
+ if (!state.disposed) disposeAfterFailure(state, false);
1980
+ });
1981
+ } });
1760
1982
  state = {
1761
1983
  options,
1762
1984
  shadowRoot,
@@ -1765,14 +1987,40 @@ function mountRemoteDomWidget(options) {
1765
1987
  styles,
1766
1988
  urlOriginPolicy,
1767
1989
  navigationOrigin,
1768
- nodesById: new Map([[ROOT_ID, shadowRoot]]),
1769
- subscriptions: /* @__PURE__ */ new Map(),
1770
- pendingListenerUpdates,
1990
+ receiver,
1991
+ reactRoot,
1992
+ renderContainer,
1993
+ portalContainer,
1771
1994
  submitListener,
1772
1995
  navigationListener,
1773
1996
  disposed: false,
1774
1997
  cleanupStarted: false
1775
1998
  };
1999
+ const componentRenderers = createRemoteComponentRenderers(options, () => state.disposed);
2000
+ const remoteRoot = createElement(RemoteRootEventBoundary, {
2001
+ receiver,
2002
+ container: renderContainer,
2003
+ options,
2004
+ isDisposed: () => state?.disposed ?? false
2005
+ }, createElement(RemoteRootRenderer, {
2006
+ receiver,
2007
+ components: componentRenderers
2008
+ }));
2009
+ flushSync(() => {
2010
+ reactRoot.render(options.wrapRemoteRoot ? options.wrapRemoteRoot(remoteRoot, portalContainer) : remoteRoot);
2011
+ });
2012
+ const connection = createSanitizingRemoteConnection({
2013
+ connection: receiver.connection,
2014
+ ...options.allowedAnchorUrls ? { allowedAnchorUrls: options.allowedAnchorUrls } : {},
2015
+ urlOriginPolicy,
2016
+ maxMutationBatchSize: options.maxMutationBatchSize ?? DEFAULT_MAX_MUTATION_BATCH_SIZE,
2017
+ maxDomNodeCount: options.maxDomNodeCount ?? DEFAULT_MAX_DOM_NODE_COUNT,
2018
+ onNodeLimitExceeded: () => {
2019
+ reportError(options, "Remote DOM widget exceeded its DOM node limit");
2020
+ },
2021
+ onInvalidRecord: (message) => reportError(options, message),
2022
+ onMutateError: (error) => reportError(options, "Remote DOM mutation could not be applied", error)
2023
+ });
1776
2024
  shadowRoot.addEventListener("submit", submitListener, { capture: true });
1777
2025
  shadowRoot.addEventListener("click", navigationListener, { capture: true });
1778
2026
  worker.onerror = (event) => {
@@ -1784,10 +2032,8 @@ function mountRemoteDomWidget(options) {
1784
2032
  const hostApi = {
1785
2033
  mutate(records) {
1786
2034
  if (state.disposed) return;
1787
- connection.mutate(records);
1788
- rebuildNodeIndex(state);
2035
+ flushSync(() => connection.mutate(records));
1789
2036
  enforceAllowedAnchorSafety(state);
1790
- flushPendingListenerUpdates(state);
1791
2037
  },
1792
2038
  callCapability(capability, method, payload) {
1793
2039
  return callCapability(state, capability, method, payload);
@@ -1799,6 +2045,7 @@ function mountRemoteDomWidget(options) {
1799
2045
  const ready = Promise.all([styles.ready, thread.imports.mount({
1800
2046
  widgetType: options.artifact.widgetType,
1801
2047
  props: initialProps,
2048
+ supportedUiContractVersions: getSupportedUiContractVersions(componentRenderers),
1802
2049
  ...options.artifact.networkAccessGranted === true ? {
1803
2050
  networkAccessGranted: true,
1804
2051
  hostOrigin: options.hostOrigin
@@ -1848,82 +2095,6 @@ function assertSerializableCapabilityResult(result) {
1848
2095
  if (!isRemoteDomSerializableValue(result)) throw new Error("Remote DOM capability result was not serializable");
1849
2096
  return result;
1850
2097
  }
1851
- function rebuildNodeIndex(state) {
1852
- state.nodesById.clear();
1853
- state.nodesById.set(ROOT_ID, state.shadowRoot);
1854
- const visit = (node) => {
1855
- state.nodesById.set(remoteId(node), node);
1856
- for (const child of node.childNodes) visit(child);
1857
- };
1858
- for (const child of state.shadowRoot.childNodes) if (!isWidgetStyleNode(child)) visit(child);
1859
- for (const subscription of Array.from(state.subscriptions.values())) if (!state.nodesById.has(subscription.targetId)) unsubscribe(state, subscription.targetId, subscription.eventName);
1860
- }
1861
- function flushPendingListenerUpdates(state) {
1862
- for (const update of state.pendingListenerUpdates.values()) if (update.listener === null) unsubscribe(state, update.targetId, update.eventName);
1863
- else subscribe(state, update.targetId, update.eventName, update.listener);
1864
- state.pendingListenerUpdates.clear();
1865
- }
1866
- function subscribe(state, targetId, eventName, proxy) {
1867
- const target = state.nodesById.get(targetId);
1868
- const tagName = target instanceof Element ? target.localName : void 0;
1869
- if (!target || !isSupportedRemoteDomEventName(eventName, tagName)) return;
1870
- unsubscribe(state, targetId, eventName);
1871
- const capture = targetId === ROOT_ID;
1872
- const listener = (event) => {
1873
- if (!capture && hasCloserEventSubscription(state, event, target, eventName)) return;
1874
- if (eventName === "submit" && event.cancelable) event.preventDefault();
1875
- const actualTargetId = resolveEventTargetId(state, event);
1876
- if (!actualTargetId) return;
1877
- const eventPayload = serializeRemoteDomEvent(event, actualTargetId, targetId, tagName);
1878
- if (!eventPayload) return;
1879
- try {
1880
- const result = proxy({
1881
- targetId: actualTargetId,
1882
- detail: toSerializableEventPayload(eventPayload)
1883
- });
1884
- if (isPromiseLike(result)) result.catch((error) => {
1885
- reportEventDeliveryFailure(state, error);
1886
- });
1887
- } catch (error) {
1888
- reportEventDeliveryFailure(state, error);
1889
- }
1890
- };
1891
- target.addEventListener(eventName, listener, { capture });
1892
- retain(proxy);
1893
- state.subscriptions.set(subscriptionKey(targetId, eventName), {
1894
- targetId,
1895
- eventName,
1896
- target,
1897
- listener,
1898
- capture,
1899
- proxy
1900
- });
1901
- }
1902
- function hasCloserEventSubscription(state, event, subscriptionTarget, eventName) {
1903
- for (const pathTarget of event.composedPath()) {
1904
- if (pathTarget === subscriptionTarget) return false;
1905
- if (!(pathTarget instanceof Node)) continue;
1906
- const pathTargetId = remoteId(pathTarget);
1907
- if (state.nodesById.get(pathTargetId) === pathTarget && state.subscriptions.has(subscriptionKey(pathTargetId, eventName))) return true;
1908
- }
1909
- return false;
1910
- }
1911
- function unsubscribe(state, targetId, eventName) {
1912
- const key = subscriptionKey(targetId, eventName);
1913
- const subscription = state.subscriptions.get(key);
1914
- if (!subscription) return;
1915
- subscription.target.removeEventListener(eventName, subscription.listener, { capture: subscription.capture });
1916
- release(subscription.proxy);
1917
- state.subscriptions.delete(key);
1918
- }
1919
- function resolveEventTargetId(state, event) {
1920
- for (const target of event.composedPath()) {
1921
- if (target === state.shadowRoot) return ROOT_ID;
1922
- if (!(target instanceof Node)) continue;
1923
- const id = remoteId(target);
1924
- if (state.nodesById.get(id) === target) return id;
1925
- }
1926
- }
1927
2098
  function handleNavigationIntent(state, event) {
1928
2099
  if (!(event instanceof MouseEvent)) return;
1929
2100
  const anchor = (event.target instanceof Element ? event.target : event.target instanceof Text ? event.target.parentElement : null)?.closest("a[href]");
@@ -1978,10 +2149,7 @@ function disposeMount(state, notifyWorker = true) {
1978
2149
  state.shadowRoot.removeEventListener("click", state.navigationListener, { capture: true });
1979
2150
  }
1980
2151
  ];
1981
- for (const subscription of Array.from(state.subscriptions.values())) cleanupSteps.push(() => {
1982
- unsubscribe(state, subscription.targetId, subscription.eventName);
1983
- });
1984
- cleanupSteps.push(() => state.pendingListenerUpdates.clear(), () => state.styles.dispose(), () => clearRenderedRemoteDomNodes(state.shadowRoot), () => state.nodesById.clear());
2152
+ cleanupSteps.push(() => flushSync(() => state.reactRoot.unmount()), () => clearRemoteReceiver(state.receiver), () => state.styles.dispose(), () => clearRenderedRemoteDomNodes(state.shadowRoot));
1985
2153
  const cleanupErrors = collectCleanupErrors(cleanupSteps);
1986
2154
  if (notifyWorker) Promise.resolve(state.thread.imports.dispose()).catch(() => void 0);
1987
2155
  queueMicrotask(() => {
@@ -1996,10 +2164,6 @@ function disposeAfterFailure(state, notifyWorker = true) {
1996
2164
  reportError(state.options, "Remote DOM widget cleanup failed after an earlier error", cleanupError);
1997
2165
  }
1998
2166
  }
1999
- function reportEventDeliveryFailure(state, error) {
2000
- if (state.disposed) return;
2001
- reportError(state.options, "Remote DOM event delivery failed", error);
2002
- }
2003
2167
  function createShadowRoot(host) {
2004
2168
  if (host.shadowRoot) {
2005
2169
  host.shadowRoot.replaceChildren();
@@ -2065,8 +2229,8 @@ function resolveInternalNavigationUrl(href, navigationOrigin) {
2065
2229
  return;
2066
2230
  }
2067
2231
  }
2068
- function findNodeId(state, node) {
2069
- for (const [id, candidate] of state.nodesById) if (candidate === node) return id;
2232
+ function findNodeId(_state, node) {
2233
+ return (node instanceof Element ? node : node.parentElement)?.closest("[data-fluid-remote-dom-node-id]")?.getAttribute("data-fluid-remote-dom-node-id") ?? void 0;
2070
2234
  }
2071
2235
  function isWidgetStyleNode(node) {
2072
2236
  return node instanceof HTMLStyleElement && node.hasAttribute("data-fluid-widget-style");
@@ -2074,6 +2238,13 @@ function isWidgetStyleNode(node) {
2074
2238
  function clearRenderedRemoteDomNodes(shadowRoot) {
2075
2239
  for (const child of Array.from(shadowRoot.childNodes)) if (!isWidgetStyleNode(child)) child.remove();
2076
2240
  }
2241
+ function clearRemoteReceiver(receiver) {
2242
+ while (receiver.root.children.length > 0) receiver.connection.mutate([[
2243
+ MUTATION_TYPE_REMOVE_CHILD,
2244
+ ROOT_ID,
2245
+ 0
2246
+ ]]);
2247
+ }
2077
2248
  function toSerializableEventPayload(payload) {
2078
2249
  const result = {
2079
2250
  bubbles: payload.bubbles,
@@ -2178,13 +2349,10 @@ function reportCleanupErrors(options, message, errors) {
2178
2349
  if (errors.length === 0) return;
2179
2350
  reportError(options, message, errors.length === 1 ? errors[0] : new AggregateError(errors, "Multiple cleanup operations failed"));
2180
2351
  }
2181
- function subscriptionKey(targetId, eventName) {
2182
- return `${targetId}:${eventName}`;
2183
- }
2184
2352
  function isPromiseLike(value) {
2185
2353
  return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
2186
2354
  }
2187
2355
  //#endregion
2188
2356
  export { mountRemoteDomWidget as t };
2189
2357
 
2190
- //# sourceMappingURL=remote-dom-renderer-CpdKWANy.mjs.map
2358
+ //# sourceMappingURL=remote-dom-renderer-Dn3pfVsl.mjs.map