@myop/sdk 0.4.0-beta.2 → 0.4.0-beta.4

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 (37) hide show
  1. package/dist/bundled-declarations.d.ts +72 -32
  2. package/dist/cjs/{_IframeSDK.a43cfa7d.js → _IframeSDK.3a412cd4.js} +165 -63
  3. package/dist/cjs/_IframeSDK.3a412cd4.js.map +7 -0
  4. package/dist/cjs/_IframeSDK.9338575d.min.js +1 -0
  5. package/dist/cjs/{_WebComponentSDK.0f6ef369.js → _WebComponentSDK.4b6ac803.js} +121 -18
  6. package/dist/cjs/_WebComponentSDK.4b6ac803.js.map +7 -0
  7. package/dist/cjs/_WebComponentSDK.4e78d94a.min.js +1 -0
  8. package/dist/cjs/_hostSDK.34f770f6.min.js +1 -0
  9. package/dist/cjs/{_hostSDK.38fcc5b4.js → _hostSDK.e9b70e44.js} +121 -18
  10. package/dist/cjs/_hostSDK.e9b70e44.js.map +7 -0
  11. package/dist/cjs/myop_sdk.js +11 -11
  12. package/dist/cjs/myop_sdk.js.map +1 -1
  13. package/dist/cjs/myop_sdk.min.js +1 -1
  14. package/dist/cjs-bundled/myop_sdk.bundled.js +167 -64
  15. package/dist/cjs-bundled/myop_sdk.bundled.js.map +3 -3
  16. package/dist/cjs-bundled/myop_sdk.bundled.min.js +1 -1
  17. package/dist/module/Iframe/IframeSDK.d.ts +26 -30
  18. package/dist/module/Iframe/index.js +166 -64
  19. package/dist/module/Iframe/index.js.map +3 -3
  20. package/dist/module/SDK.js +167 -64
  21. package/dist/module/SDK.js.map +3 -3
  22. package/dist/module/embeddedSDK.js +1 -1
  23. package/dist/module/host/components/IMyopComponent.d.ts +3 -0
  24. package/dist/module/host/components/myopExtractedWebComponent.d.ts +24 -1
  25. package/dist/module/host/embeddedSDK.js +1 -1
  26. package/dist/module/host/index.js +122 -19
  27. package/dist/module/host/index.js.map +2 -2
  28. package/dist/module/host/loaders/HTMLComponentLoader.d.ts +19 -1
  29. package/dist/module/webcomponent/index.js +122 -19
  30. package/dist/module/webcomponent/index.js.map +2 -2
  31. package/package.json +1 -1
  32. package/dist/cjs/_IframeSDK.8f08de4b.min.js +0 -1
  33. package/dist/cjs/_IframeSDK.a43cfa7d.js.map +0 -7
  34. package/dist/cjs/_WebComponentSDK.0f6ef369.js.map +0 -7
  35. package/dist/cjs/_WebComponentSDK.649e5e47.min.js +0 -1
  36. package/dist/cjs/_hostSDK.131fedc9.min.js +0 -1
  37. package/dist/cjs/_hostSDK.38fcc5b4.js.map +0 -7
@@ -683,7 +683,7 @@ declare module "@myop/sdk/host/componentCore" {
683
683
  };
684
684
  }
685
685
  declare module "@myop/sdk/host/components/myopExtractedWebComponent" {
686
- import { BaseMyopComponent } from "@myop/sdk/host/components/IMyopComponent";
686
+ import { AutoSizeOptions, BaseMyopComponent } from "@myop/sdk/host/components/IMyopComponent";
687
687
  import { BaseMyopMessage } from "@myop/sdk/messages";
688
688
  import { CleanupHandler, IComponentDefinitionConfig, ISkinConfig } from "@myop/sdk/common";
689
689
  import { loaderOptions } from "@myop/sdk/host/hostSDK";
@@ -703,6 +703,29 @@ declare module "@myop/sdk/host/components/myopExtractedWebComponent" {
703
703
  private _ctaQueue;
704
704
  private _ctaListener;
705
705
  constructor(componentDefinition: IComponentDefinitionConfig, skin: ISkinConfig, container: HTMLElement, customElement: HTMLElement, tagName: string, options?: loaderOptions);
706
+ private _autoSizeOptions?;
707
+ /**
708
+ * Data-out for size — the extracted counterpart of the iframe implementation.
709
+ *
710
+ * THIS EXISTED ONLY ON MyopIframeComponent, and that was invisible for as long as
711
+ * minification was sending every v2 component down the iframe path. Restoring v2
712
+ * detection moves them to this class, and `@myop/react` drives both through one
713
+ * wrapper: in content mode it hides its loader from the FIRST onSizeChange, so a
714
+ * component whose class has no observeAutoSize leaves a React host on a loader
715
+ * forever. Pic-Time's sizeMeta is width:100% with no height, which is exactly content
716
+ * mode. Caught by v2-data-contract D8 (API parity).
717
+ *
718
+ * It REPORTS rather than imposes. An iframe has no intrinsic size and must be
719
+ * measured inside and then set from outside; a custom element sizes itself from its
720
+ * own content, so the honest thing is to measure it and tell the host, and let the
721
+ * host decide. React then uses width:100% and the measured height.
722
+ *
723
+ * The one thing it does impose is `display: block` when the element computes to
724
+ * `inline`, which is the default for a custom element and measures as zero height no
725
+ * matter what it contains. The iframe path forces width:100% for the same reason —
726
+ * without it the first measurement is 0 and the host sizes the component to nothing.
727
+ */
728
+ observeAutoSize: (options?: AutoSizeOptions) => CleanupHandler;
706
729
  cleanupInspect: CleanupHandler;
707
730
  inspect(): CleanupHandler;
708
731
  hide(): void;
@@ -1262,6 +1285,9 @@ declare module "@myop/sdk/host/components/IMyopComponent" {
1262
1285
  initiated: () => Promise<any>;
1263
1286
  private sendCleanupMessage;
1264
1287
  send(message: BaseMyopMessage): CleanupHandler;
1288
+ /** Runs the 'onDispose' handlers once. hostSDK registers the one that unregisters
1289
+ * this component, so it has to run on EVERY dispose path, initiated or not. */
1290
+ private runDisposeHandlers;
1265
1291
  dispose(): void;
1266
1292
  props: P;
1267
1293
  refs: R;
@@ -1483,41 +1509,37 @@ declare module "@myop/sdk/Iframe/IframeSDK" {
1483
1509
  messageToHost: (message: MyopBindReplayMessage, replayTo?: MyopBindMessage) => void;
1484
1510
  supportedHandlers: BaseMessageHandler[];
1485
1511
  /**
1486
- * Announce this frame until the host answers, not once.
1487
- *
1488
- * The host only learns a component exists AFTER `await componentLoader.load(...)`
1489
- * resolves (hostSDK.ts:433-437 — the line above it already calls this a race). A frame
1490
- * that boots inside that await announces itself to a host whose components2init is
1491
- * still empty: the host iterates nothing, sends no InitMessage, and a single-shot
1492
- * announce leaves the frame waiting forever. The visible symptom is initiated()
1493
- * rejecting with timeout_<ms>, with exactly one message on the wire -- InitRequest,
1494
- * never InitResponse.
1512
+ * The window this frame answers to. Defaults to the parent, which is correct for a
1513
+ * component mounted directly by the page's own host.
1495
1514
  *
1496
- * Minification made it common rather than rare: a smaller payload boots the frame
1497
- * sooner, landing it deeper inside the window. Reported by a customer 2026-10-17.
1515
+ * IT IS NOT ALWAYS THE PARENT. A host can mount a component into a container that
1516
+ * lives inside ANOTHER component's document -- Pic-Time's reel templates are mounted
1517
+ * by the top-level host into `#reelSlot` inside the reel dialog's frame. The real host
1518
+ * is then a GRANDparent, and `window.parent` is a frame that knows nothing about this
1519
+ * component: its nested hostSDK has no entry for it and its IframeSDK drops messages
1520
+ * from a child. Replies sent to the parent are simply lost.
1498
1521
  *
1499
- * Re-announcing is safe and idempotent. The host replies to InitRequest by sending
1500
- * InitMessage to every component still awaiting init; a frame that has already
1501
- * registered that id ignores the repeat (BaseMyopSDK: `if (this.components[id]) return`).
1502
- * So a late answer costs one extra round trip and nothing else.
1503
- *
1504
- * Backs off 50ms -> 1s and stops the moment any component registers, so the common
1505
- * case sends exactly one message.
1522
+ * So we adopt whoever actually talks to us, the first time an ancestor does.
1523
+ */
1524
+ private hostWindow;
1525
+ /**
1526
+ * Commands (including ExecuteScript) must come from a window this frame is actually
1527
+ * nested inside -- never from an opener or a sibling that merely holds a reference.
1506
1528
  *
1507
- * THE CAP IS A CONSTANT, and it should ideally be the host's own `options.timeout`
1508
- * so the frame never gives up before the host does. That was tried on 2026-10-17 and
1509
- * REVERTED: the budget has to reach the frame through the init script that
1510
- * HTMLComponentLoader generates, and `${...}` interpolated into that template does
1511
- * not survive the build -- the transpiled `.concat()` leaks into the emitted script
1512
- * as literal text, the frame parses no number, and it silently falls back to the
1513
- * default. It looked correct in source and in the bundle, and only the e2e caught it.
1529
+ * THIS USED TO COMPARE AGAINST `window.parent` ALONE, and that was a regression I
1530
+ * shipped in 0.4.0-beta: it silently dropped the host's InitMessage for any component
1531
+ * mounted cross-document, because the host is a grandparent there. The frame then
1532
+ * announced for its full 10s budget into a parent that never answers and rejected with
1533
+ * timeout_10000 having registered nothing. Reproduced live on Pic-Time's QA page
1534
+ * 2026-10-17: 15 InitRequests, 0 InitResponses, `IframeSDK.components` empty.
1535
+ * 0.3.36 had no guard at all, which is why it worked.
1514
1536
  *
1515
- * 10s is chosen to exceed BaseMyopComponent's 5s default, so the frame outlives the
1516
- * common host deadline. This only matters in the pathological case: with the host now
1517
- * sending InitMessage on registration (hostSDK.ts), the retry is a backstop that in
1518
- * practice never fires.
1537
+ * Walking the ancestor chain keeps the security property -- an opener or a sibling is
1538
+ * not an ancestor -- while allowing a host any number of frames up. The walk is
1539
+ * bounded, and reading `.parent` across origins is allowed (only the reference is
1540
+ * compared, never touched).
1519
1541
  */
1520
- private announceUntilAnswered;
1542
+ private isFromHostChain;
1521
1543
  init: () => void;
1522
1544
  }
1523
1545
  }
@@ -2109,7 +2131,25 @@ declare module "@myop/sdk/host/loaders/HTMLComponentLoader" {
2109
2131
  export class HTMLComponentLoader extends BaseMyopLoader {
2110
2132
  type: "HTMLLoader";
2111
2133
  /**
2112
- * Detect if HTML is a Myop Web Component (starts with marker comment)
2134
+ * Detect if HTML is a Myop Web Component.
2135
+ *
2136
+ * THE MARKER COMMENT ALONE IS NOT A SAFE SIGNAL, BECAUSE MINIFIERS DELETE COMMENTS.
2137
+ *
2138
+ * This used to be `startsWith(MYOP_WEB_COMPONENT_MARKER)` and nothing else. The
2139
+ * delivery lane minifies with `removeComments: true` (myop-server derive.ts MIN_CFG),
2140
+ * so the stored `min` artifact of every v2 web component loses its leading
2141
+ * `<!-- myop-web-component -->`. Detection then returned false, the component fell
2142
+ * back to plain iframe rendering, and the whole authored document was shown --
2143
+ * including the v2 authoring wrapper, so Pic-Time's production dashboard displayed the
2144
+ * preview chrome ("Web Component v2", a tag-name badge reading `loading...`, SIZE /
2145
+ * OVERFLOW / BG controls) instead of their component. Reported 2026-10-18; the served
2146
+ * HTML measured 328,071 bytes against 951,130 at origin, with zero `<!--` left in it.
2147
+ *
2148
+ * The script id is the signal that survives, and it is the one `extractWebComponentScript`
2149
+ * already depends on: if that element is present, extraction works, so detection
2150
+ * should agree. Checked with a regex rather than DOMParser -- this runs for every
2151
+ * HTML component, and parsing a 300 KB document to answer a yes/no question is not
2152
+ * worth it.
2113
2153
  */
2114
2154
  private isMyopWebComponent;
2115
2155
  /**
@@ -1008,6 +1008,7 @@ var BaseMyopComponent = class {
1008
1008
  setTimeout(() => {
1009
1009
  if (!this.isInitiated && this._whenInitiatedReject && !this._markedForDisposed) {
1010
1010
  this._whenInitiatedReject("timeout_".concat(timeout, " ").concat(this.id));
1011
+ this.dispose();
1011
1012
  }
1012
1013
  }, timeout);
1013
1014
  this.initiated().then(() => {
@@ -1091,19 +1092,27 @@ var BaseMyopComponent = class {
1091
1092
  };
1092
1093
  }
1093
1094
  }
1095
+ /** Runs the 'onDispose' handlers once. hostSDK registers the one that unregisters
1096
+ * this component, so it has to run on EVERY dispose path, initiated or not. */
1097
+ runDisposeHandlers() {
1098
+ const onDisposeHandlers = this.messageHandlers["onDispose"];
1099
+ if (onDisposeHandlers) {
1100
+ onDisposeHandlers.forEach((disposeHandler) => {
1101
+ disposeHandler(null);
1102
+ });
1103
+ }
1104
+ }
1094
1105
  dispose() {
1095
1106
  this.markedForDisposed = true;
1096
1107
  if (!this.isInitiated) {
1108
+ this.runDisposeHandlers();
1109
+ this.messageHandlers = {};
1110
+ this.id += "_disposed";
1097
1111
  return;
1098
1112
  }
1099
1113
  console.log("disposing component", this.id);
1100
1114
  this.send(new DisposeMessage());
1101
- const onDisposeHandlers = this.messageHandlers["onDispose"];
1102
- if (onDisposeHandlers) {
1103
- onDisposeHandlers.forEach((disposeHandler) => {
1104
- disposeHandler(null);
1105
- });
1106
- }
1115
+ this.runDisposeHandlers();
1107
1116
  this.messageHandlers = {};
1108
1117
  this.id += "_disposed";
1109
1118
  this.isInitiated = false;
@@ -1179,6 +1188,67 @@ var MyopExtractedWebComponent = class extends BaseMyopComponent {
1179
1188
  this._ctaQueue.push({ action: detail.action, payload: detail.payload });
1180
1189
  }
1181
1190
  });
1191
+ __publicField(this, "_autoSizeOptions");
1192
+ /**
1193
+ * Data-out for size — the extracted counterpart of the iframe implementation.
1194
+ *
1195
+ * THIS EXISTED ONLY ON MyopIframeComponent, and that was invisible for as long as
1196
+ * minification was sending every v2 component down the iframe path. Restoring v2
1197
+ * detection moves them to this class, and `@myop/react` drives both through one
1198
+ * wrapper: in content mode it hides its loader from the FIRST onSizeChange, so a
1199
+ * component whose class has no observeAutoSize leaves a React host on a loader
1200
+ * forever. Pic-Time's sizeMeta is width:100% with no height, which is exactly content
1201
+ * mode. Caught by v2-data-contract D8 (API parity).
1202
+ *
1203
+ * It REPORTS rather than imposes. An iframe has no intrinsic size and must be
1204
+ * measured inside and then set from outside; a custom element sizes itself from its
1205
+ * own content, so the honest thing is to measure it and tell the host, and let the
1206
+ * host decide. React then uses width:100% and the measured height.
1207
+ *
1208
+ * The one thing it does impose is `display: block` when the element computes to
1209
+ * `inline`, which is the default for a custom element and measures as zero height no
1210
+ * matter what it contains. The iframe path forces width:100% for the same reason —
1211
+ * without it the first measurement is 0 and the host sizes the component to nothing.
1212
+ */
1213
+ __publicField(this, "observeAutoSize", (options) => {
1214
+ this._autoSizeOptions = options;
1215
+ const el = this.customElement;
1216
+ try {
1217
+ if (getComputedStyle(el).display === "inline") el.style.display = "block";
1218
+ } catch (e) {
1219
+ }
1220
+ const parse = (v) => {
1221
+ if (v === void 0) return void 0;
1222
+ if (typeof v === "number") return v;
1223
+ const n = parseFloat(v);
1224
+ return isNaN(n) ? void 0 : n;
1225
+ };
1226
+ const clamp = (v, min, max) => min !== void 0 && v < min ? min : max !== void 0 && v > max ? max : v;
1227
+ const report = () => {
1228
+ var _a;
1229
+ const opts = this._autoSizeOptions;
1230
+ const rect = el.getBoundingClientRect();
1231
+ const autoHeight = (opts == null ? void 0 : opts.explicitHeight) === void 0;
1232
+ (_a = opts == null ? void 0 : opts.onSizeChange) == null ? void 0 : _a.call(opts, {
1233
+ width: rect.width,
1234
+ height: autoHeight ? clamp(rect.height, parse(opts == null ? void 0 : opts.minHeight), parse(opts == null ? void 0 : opts.maxHeight)) : rect.height,
1235
+ autoSizingWidth: false,
1236
+ // a web component fills its container's width
1237
+ autoSizingHeight: autoHeight
1238
+ });
1239
+ };
1240
+ let ro;
1241
+ try {
1242
+ ro = new ResizeObserver(() => report());
1243
+ ro.observe(el);
1244
+ } catch (e) {
1245
+ }
1246
+ report();
1247
+ return () => {
1248
+ ro == null ? void 0 : ro.disconnect();
1249
+ this._autoSizeOptions = void 0;
1250
+ };
1251
+ });
1182
1252
  __publicField(this, "cleanupInspect", () => {
1183
1253
  });
1184
1254
  /**
@@ -1506,8 +1576,13 @@ var createRefComponent = async (ref, componentConfig, components, options) => {
1506
1576
  return new Promise(async (resolve, reject) => {
1507
1577
  const refComponent = new MyopRefComponent(componentConfig.type, component, options);
1508
1578
  components.push(refComponent);
1579
+ const unregister = () => {
1580
+ const at = components.indexOf(refComponent);
1581
+ if (at !== -1) components.splice(at, 1);
1582
+ };
1509
1583
  await component.initiated();
1510
1584
  const timeout = setTimeout(() => {
1585
+ unregister();
1511
1586
  reject("timeout");
1512
1587
  }, 5e3);
1513
1588
  component.send(new CreateRefComponentMessage(
@@ -1520,11 +1595,13 @@ var createRefComponent = async (ref, componentConfig, components, options) => {
1520
1595
  async (message) => {
1521
1596
  clearTimeout(timeout);
1522
1597
  if (message.failed) {
1598
+ unregister();
1523
1599
  reject("CreateRefComponentMessage failed");
1524
1600
  return;
1525
1601
  }
1526
1602
  const implementationComponent = components.find((c) => c.id == refComponent.id);
1527
- if (implementationComponent !== refComponent) {
1603
+ if (implementationComponent && implementationComponent !== refComponent) {
1604
+ unregister();
1528
1605
  component.refs[ref.refConfig.name] = implementationComponent;
1529
1606
  resolve(implementationComponent);
1530
1607
  return;
@@ -1888,7 +1965,7 @@ var MyopIframeComponent = class extends BaseMyopComponent {
1888
1965
  return cleanupHandler;
1889
1966
  });
1890
1967
  __publicField(this, "dispose", () => {
1891
- var _a;
1968
+ var _a, _b;
1892
1969
  (_a = this._bridgeSizeCleanup) == null ? void 0 : _a.call(this);
1893
1970
  if (this._autoSizeCleanup) {
1894
1971
  this._autoSizeCleanup();
@@ -1901,7 +1978,7 @@ var MyopIframeComponent = class extends BaseMyopComponent {
1901
1978
  if (this.cleanupInspect)
1902
1979
  this.cleanupInspect();
1903
1980
  super.dispose();
1904
- this.IframeElement.parentNode.removeChild(this.IframeElement);
1981
+ (_b = this.IframeElement.parentNode) == null ? void 0 : _b.removeChild(this.IframeElement);
1905
1982
  });
1906
1983
  this.element = this.IframeElement;
1907
1984
  this._lastData = options == null ? void 0 : options.data;
@@ -2173,7 +2250,7 @@ var WebcomponentLoader = class extends BaseMyopLoader {
2173
2250
  };
2174
2251
 
2175
2252
  // version:myop-sdk-version
2176
- var myop_sdk_version_default = "0.4.0-beta.2";
2253
+ var myop_sdk_version_default = "0.4.0-beta.4";
2177
2254
 
2178
2255
  // src/helpers/deliveryStore.ts
2179
2256
  var DB_NAME = "myop-delivery";
@@ -3335,14 +3412,14 @@ var HTMLComponentLoader = class extends BaseMyopLoader {
3335
3412
  if (!isolate) {
3336
3413
  doc.writeln("".concat(sdkScript).concat(initScript));
3337
3414
  }
3338
- if (!isolate && (options == null ? void 0 : options.data) !== void 0) {
3415
+ if (!isolate && (options == null ? void 0 : options.data) !== void 0 && (options == null ? void 0 : options.data) !== null) {
3339
3416
  const ctaQueueScript = "<script>\n window.__myop_cta_queue = [];\n window.myop_cta_handler = function(action, payload) {\n window.__myop_cta_queue.push({action: action, payload: payload});\n };\n <\/script>";
3340
3417
  const escapedData = JSON.stringify(options.data).replace(/<\/script/gi, "\\u003c/script");
3341
3418
  const dataScript = '<script id="__myop_init_data_script">\n (function() {\n var data = '.concat(escapedData, ";\n var scriptEl = document.getElementById('__myop_init_data_script');\n\n var callInitInterface = function(fn) {\n var startTime = performance.now();\n // Mark as pre-injected data call for extension tracking\n window.__myop_init_preinjected = true;\n try {\n fn(data);\n } finally {\n window.__myop_init_preinjected = false;\n }\n var duration = performance.now() - startTime;\n\n // Notify extension via postMessage (init* = pre-injected)\n try {\n window.parent.postMessage({\n type: 'MYOP_PREINJECTED_INIT',\n payload: {\n data: data,\n duration: duration,\n timestamp: Date.now()\n }\n }, '*');\n } catch (e) {}\n\n // Cleanup: remove script tag and stored data\n if (scriptEl && scriptEl.parentNode) {\n scriptEl.parentNode.removeChild(scriptEl);\n }\n delete window.__myop_init_data;\n };\n\n if (typeof window.myop_init_interface === 'function') {\n // Function already exists - call immediately\n callInitInterface(window.myop_init_interface);\n } else {\n // Function not yet defined - store data and watch for definition\n window.__myop_init_data = data;\n\n var _original;\n Object.defineProperty(window, 'myop_init_interface', {\n configurable: true,\n enumerable: true,\n set: function(fn) {\n _original = fn;\n if (window.__myop_init_data !== undefined) {\n var storedData = window.__myop_init_data;\n delete window.__myop_init_data;\n callInitInterface(fn);\n }\n },\n get: function() {\n return _original;\n }\n });\n }\n })();\n <\/script>");
3342
3419
  doc.writeln(ctaQueueScript + dataScript);
3343
3420
  }
3344
3421
  if (isolate) {
3345
- const escapedData = (options == null ? void 0 : options.data) !== void 0 ? JSON.stringify(options.data).replace(/<\/script/gi, "\\u003c/script") : "undefined";
3422
+ const escapedData = (options == null ? void 0 : options.data) !== void 0 && (options == null ? void 0 : options.data) !== null ? JSON.stringify(options.data).replace(/<\/script/gi, "\\u003c/script") : "undefined";
3346
3423
  doc.writeln(this.buildIsolationBridge(escapedData));
3347
3424
  }
3348
3425
  doc.close();
@@ -3397,10 +3474,29 @@ var HTMLComponentLoader = class extends BaseMyopLoader {
3397
3474
  });
3398
3475
  }
3399
3476
  /**
3400
- * Detect if HTML is a Myop Web Component (starts with marker comment)
3477
+ * Detect if HTML is a Myop Web Component.
3478
+ *
3479
+ * THE MARKER COMMENT ALONE IS NOT A SAFE SIGNAL, BECAUSE MINIFIERS DELETE COMMENTS.
3480
+ *
3481
+ * This used to be `startsWith(MYOP_WEB_COMPONENT_MARKER)` and nothing else. The
3482
+ * delivery lane minifies with `removeComments: true` (myop-server derive.ts MIN_CFG),
3483
+ * so the stored `min` artifact of every v2 web component loses its leading
3484
+ * `<!-- myop-web-component -->`. Detection then returned false, the component fell
3485
+ * back to plain iframe rendering, and the whole authored document was shown --
3486
+ * including the v2 authoring wrapper, so Pic-Time's production dashboard displayed the
3487
+ * preview chrome ("Web Component v2", a tag-name badge reading `loading...`, SIZE /
3488
+ * OVERFLOW / BG controls) instead of their component. Reported 2026-10-18; the served
3489
+ * HTML measured 328,071 bytes against 951,130 at origin, with zero `<!--` left in it.
3490
+ *
3491
+ * The script id is the signal that survives, and it is the one `extractWebComponentScript`
3492
+ * already depends on: if that element is present, extraction works, so detection
3493
+ * should agree. Checked with a regex rather than DOMParser -- this runs for every
3494
+ * HTML component, and parsing a 300 KB document to answer a yes/no question is not
3495
+ * worth it.
3401
3496
  */
3402
3497
  isMyopWebComponent(html) {
3403
- return html.trimStart().startsWith(MYOP_WEB_COMPONENT_MARKER);
3498
+ if (html.trimStart().startsWith(MYOP_WEB_COMPONENT_MARKER)) return true;
3499
+ return new RegExp("<script[^>]*\\sid=[\"']?".concat(MYOP_WEB_COMPONENT_SCRIPT_ID, "[\"'\\s>]"), "i").test(html);
3404
3500
  }
3405
3501
  /**
3406
3502
  * Resolve how this component should render, applying the host render policy.
@@ -3495,7 +3591,6 @@ var HTMLComponentLoader = class extends BaseMyopLoader {
3495
3591
  if (!container.querySelector('[id^="myop-comp-"]')) {
3496
3592
  container.innerHTML = "";
3497
3593
  }
3498
- this.appendChild(container, element, options);
3499
3594
  const component = new MyopExtractedWebComponent(
3500
3595
  componentDefinition,
3501
3596
  skin,
@@ -3504,7 +3599,8 @@ var HTMLComponentLoader = class extends BaseMyopLoader {
3504
3599
  tagName,
3505
3600
  options
3506
3601
  );
3507
- if ((options == null ? void 0 : options.data) !== void 0) {
3602
+ this.appendChild(container, element, options);
3603
+ if ((options == null ? void 0 : options.data) !== void 0 && (options == null ? void 0 : options.data) !== null) {
3508
3604
  component.initWithData(options.data);
3509
3605
  }
3510
3606
  console.log("[MyopWebComponent] Loaded <".concat(tagName, "> into container"));
@@ -3629,7 +3725,12 @@ var HostSDK = class {
3629
3725
  }
3630
3726
  const myopMessage = event.data;
3631
3727
  if (myopMessage.messageType === MyopMessages.InitRequest) {
3632
- this.components2init.forEach((component2) => {
3728
+ const fromAskingFrame = this.components2init.filter((component2) => {
3729
+ const element = component2.element;
3730
+ return (element == null ? void 0 : element.nodeName) === "IFRAME" && element.contentWindow === event.source;
3731
+ });
3732
+ const targets = fromAskingFrame.length ? fromAskingFrame : this.components2init;
3733
+ targets.forEach((component2) => {
3633
3734
  component2.send(new InitMessage(component2.id));
3634
3735
  });
3635
3736
  return;
@@ -3846,7 +3947,6 @@ var HostSDK = class {
3846
3947
  let component = await componentLoader.load(componentConfig.type, selectedSkin, container, options);
3847
3948
  if (!component.isInitiated) {
3848
3949
  this.components2init.push(component);
3849
- component.send(new InitMessage(component.id));
3850
3950
  }
3851
3951
  component.bind("onDispose", () => {
3852
3952
  this.components = this.components.filter((c) => c !== component);
@@ -3854,6 +3954,9 @@ var HostSDK = class {
3854
3954
  return true;
3855
3955
  });
3856
3956
  this.components.push(component);
3957
+ if (!component.isInitiated) {
3958
+ component.send(new InitMessage(component.id));
3959
+ }
3857
3960
  if (!options || options.connectProps !== false) {
3858
3961
  component = connectProps(component, componentConfig.type.props, selectedSkin);
3859
3962
  }
@@ -4107,7 +4210,7 @@ var IframeSDK = class extends BaseMyopSDK {
4107
4210
  super();
4108
4211
  __publicField(this, "version", myop_sdk_version_default);
4109
4212
  __publicField(this, "messageToHost", (message, replayTo) => {
4110
- var _a;
4213
+ var _a, _b;
4111
4214
  if (!message.replayToHandler && (replayTo == null ? void 0 : replayTo.handlerUniqueId))
4112
4215
  message.replayToHandler = replayTo.handlerUniqueId;
4113
4216
  const componentIds = Object.keys(this.components);
@@ -4121,7 +4224,7 @@ var IframeSDK = class extends BaseMyopSDK {
4121
4224
  if (!message.source)
4122
4225
  message.source = componentIds[0];
4123
4226
  message.direction = MessageDirection.up;
4124
- (_a = window.parent) == null ? void 0 : _a.postMessage(__spreadValues({}, message), "*");
4227
+ (_b = (_a = this.hostWindow) != null ? _a : window.parent) == null ? void 0 : _b.postMessage(__spreadValues({}, message), "*");
4125
4228
  });
4126
4229
  __publicField(this, "supportedHandlers", [
4127
4230
  new ChangeTextHandler(this.messageToHost),
@@ -4139,59 +4242,58 @@ var IframeSDK = class extends BaseMyopSDK {
4139
4242
  new CreateRefComponentMessageHandler2(this.messageToHost)
4140
4243
  ]);
4141
4244
  /**
4142
- * Announce this frame until the host answers, not once.
4143
- *
4144
- * The host only learns a component exists AFTER `await componentLoader.load(...)`
4145
- * resolves (hostSDK.ts:433-437 — the line above it already calls this a race). A frame
4146
- * that boots inside that await announces itself to a host whose components2init is
4147
- * still empty: the host iterates nothing, sends no InitMessage, and a single-shot
4148
- * announce leaves the frame waiting forever. The visible symptom is initiated()
4149
- * rejecting with timeout_<ms>, with exactly one message on the wire -- InitRequest,
4150
- * never InitResponse.
4151
- *
4152
- * Minification made it common rather than rare: a smaller payload boots the frame
4153
- * sooner, landing it deeper inside the window. Reported by a customer 2026-10-17.
4245
+ * The window this frame answers to. Defaults to the parent, which is correct for a
4246
+ * component mounted directly by the page's own host.
4154
4247
  *
4155
- * Re-announcing is safe and idempotent. The host replies to InitRequest by sending
4156
- * InitMessage to every component still awaiting init; a frame that has already
4157
- * registered that id ignores the repeat (BaseMyopSDK: `if (this.components[id]) return`).
4158
- * So a late answer costs one extra round trip and nothing else.
4248
+ * IT IS NOT ALWAYS THE PARENT. A host can mount a component into a container that
4249
+ * lives inside ANOTHER component's document -- Pic-Time's reel templates are mounted
4250
+ * by the top-level host into `#reelSlot` inside the reel dialog's frame. The real host
4251
+ * is then a GRANDparent, and `window.parent` is a frame that knows nothing about this
4252
+ * component: its nested hostSDK has no entry for it and its IframeSDK drops messages
4253
+ * from a child. Replies sent to the parent are simply lost.
4159
4254
  *
4160
- * Backs off 50ms -> 1s and stops the moment any component registers, so the common
4161
- * case sends exactly one message.
4255
+ * So we adopt whoever actually talks to us, the first time an ancestor does.
4256
+ */
4257
+ __publicField(this, "hostWindow", window.parent);
4258
+ /**
4259
+ * Commands (including ExecuteScript) must come from a window this frame is actually
4260
+ * nested inside -- never from an opener or a sibling that merely holds a reference.
4162
4261
  *
4163
- * THE CAP IS A CONSTANT, and it should ideally be the host's own `options.timeout`
4164
- * so the frame never gives up before the host does. That was tried on 2026-10-17 and
4165
- * REVERTED: the budget has to reach the frame through the init script that
4166
- * HTMLComponentLoader generates, and `${...}` interpolated into that template does
4167
- * not survive the build -- the transpiled `.concat()` leaks into the emitted script
4168
- * as literal text, the frame parses no number, and it silently falls back to the
4169
- * default. It looked correct in source and in the bundle, and only the e2e caught it.
4262
+ * THIS USED TO COMPARE AGAINST `window.parent` ALONE, and that was a regression I
4263
+ * shipped in 0.4.0-beta: it silently dropped the host's InitMessage for any component
4264
+ * mounted cross-document, because the host is a grandparent there. The frame then
4265
+ * announced for its full 10s budget into a parent that never answers and rejected with
4266
+ * timeout_10000 having registered nothing. Reproduced live on Pic-Time's QA page
4267
+ * 2026-10-17: 15 InitRequests, 0 InitResponses, `IframeSDK.components` empty.
4268
+ * 0.3.36 had no guard at all, which is why it worked.
4170
4269
  *
4171
- * 10s is chosen to exceed BaseMyopComponent's 5s default, so the frame outlives the
4172
- * common host deadline. This only matters in the pathological case: with the host now
4173
- * sending InitMessage on registration (hostSDK.ts), the retry is a backstop that in
4174
- * practice never fires.
4270
+ * Walking the ancestor chain keeps the security property -- an opener or a sibling is
4271
+ * not an ancestor -- while allowing a host any number of frames up. The walk is
4272
+ * bounded, and reading `.parent` across origins is allowed (only the reference is
4273
+ * compared, never touched).
4175
4274
  */
4176
- __publicField(this, "announceUntilAnswered", (budgetMs = 1e4) => {
4177
- let wait2 = 50;
4178
- let waited = 0;
4179
- const tick = () => {
4180
- if (Object.keys(this.components).length > 0) return;
4181
- if (waited > budgetMs) return;
4182
- this.messageToHost(new InitRequest());
4183
- waited += wait2;
4184
- wait2 = Math.min(wait2 * 2, 1e3);
4185
- setTimeout(tick, wait2);
4186
- };
4187
- tick();
4275
+ __publicField(this, "isFromHostChain", (source) => {
4276
+ if (!source) return true;
4277
+ if (source === window) return true;
4278
+ let current = window;
4279
+ for (let depth = 0; depth < 32; depth++) {
4280
+ const parent = current.parent;
4281
+ if (!parent || parent === current) return false;
4282
+ if (source === parent) return true;
4283
+ current = parent;
4284
+ }
4285
+ return false;
4188
4286
  });
4189
4287
  __publicField(this, "init", () => {
4190
4288
  window.onmessage = (event) => {
4191
- if (event.source && event.source !== window.parent && event.source !== window) return;
4289
+ var _a;
4290
+ if (!this.isFromHostChain(event.source)) return;
4291
+ if (event.source && event.source !== window && ((_a = event.data) == null ? void 0 : _a.messageType) === MyopMessages.InitMessage) {
4292
+ this.hostWindow = event.source;
4293
+ }
4192
4294
  this.handleIncomeMessages(event);
4193
4295
  };
4194
- this.announceUntilAnswered();
4296
+ this.messageToHost(new InitRequest());
4195
4297
  });
4196
4298
  if (!window.myop) {
4197
4299
  window.myop = {};