cindel 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,9 +28,10 @@
28
28
 
29
29
  **Client**
30
30
 
31
+ - No runtime dependencies, works in any modern browser
31
32
  - Exponential backoff with automatic reconnect
32
- - No runtime dependencies, so it works in any modern browser
33
33
  - Event system with `on`, `once`, and `off` for connect, disconnect, reload, add, remove, etc.
34
+ - Iframe injection via `postMessage` for Private Network Access restricted environments
34
35
  - IIFE build compatible with userscript managers (Tampermonkey, Greasemonkey) via `@require`
35
36
 
36
37
  ---
@@ -408,6 +409,10 @@ process.on("SIGINT", () => server.stop().then(() => process.exit(0)));
408
409
  | `onFileLoaded` | `(file) => void` | | Called after each file is loaded or reloaded |
409
410
  | `loadOrder` | `Stage[]` | | Extra stages prepended before the built-in sorting |
410
411
  | `sortFiles` | `(files) => string[]` | | Fully replaces the default sort. When set, `loadOrder` is ignored |
412
+ | `iframe` | `boolean \| Object` | | Forward files to an iframe via `postMessage` |
413
+ | `iframe.target` | `Window \| HTMLIFrameElement` | | Target a specific same-origin iframe, skips auto-discovery |
414
+ | `iframe.origin` | `string` | `'*'` | Validates incoming handshake responses |
415
+ | `iframe.css` | `'iframe' \| 'parent' \| 'both'` | `'iframe'` | Where CSS files are loaded when `iframe` is set |
411
416
 
412
417
  ### Methods
413
418
 
@@ -552,6 +557,38 @@ new HMRClient({
552
557
 
553
558
  ---
554
559
 
560
+ ### Iframe Injection
561
+
562
+ When your project runs inside an `<iframe>` on a third-party domain, the browser's Private Network Access policy blocks the iframe from reaching `localhost` directly. The `iframe` option works around this by fetching files in the parent page and forwarding them to the iframe via `postMessage`.
563
+
564
+ Call `stub()` once inside the iframe via a userscript or existing inline script:
565
+
566
+ ```js
567
+ HMR.stub();
568
+ ```
569
+
570
+ Then configure the client in the parent:
571
+
572
+ ```js
573
+ new HMR.HMRClient({
574
+ port: 1338,
575
+ iframe: true,
576
+ });
577
+ ```
578
+
579
+ The client listens for the stub's ready signal and attaches automatically. Reattachment is automatic if the iframe reloads or is replaced. In the rare case of multiple same-origin iframes all running stub, pass `iframe.target` to target a specific one, but reattachment is then your responsibility.
580
+
581
+ ```js
582
+ iframe: {
583
+ target: document.querySelector('iframe#html5game'),
584
+ css: 'both', // 'iframe' (default) | 'parent' | 'both'
585
+ }
586
+ ```
587
+
588
+ > `.mjs` files are forwarded as `<script type="module">` blocks, preserving ES module semantics. Bare specifiers and relative imports will not resolve, only self-contained modules are supported.
589
+
590
+ ---
591
+
555
592
  ### Override Detection
556
593
 
557
594
  Override detection lets you maintain a parallel directory of replacement files that shadow originals without modifying them. When an override changes, the client unloads the original before loading the override.
@@ -592,7 +629,7 @@ new HMRClient({
592
629
  | Import path | Environment | Description |
593
630
  | ------------------------------------- | ----------- | -------------------- |
594
631
  | `cindel` or `cindel/server` | Node / Bun | `HMRServer` |
595
- | `cindel/client` | Browser ESM | `HMRClient` |
632
+ | `cindel/client` | Browser ESM | `HMRClient`, `stub` |
596
633
  | `https://cdn.jsdelivr.net/npm/cindel` | Browser CDN | Exposes `window.HMR` |
597
634
 
598
635
  ---
@@ -1,7 +1,28 @@
1
1
  /** Handles loading and hot reloading of JavaScript and CSS files via blob URLs. */
2
2
  export class FileLoader {
3
- constructor(httpUrl: any);
3
+ constructor(httpUrl: any, { iframeTarget, iframeOrigin, css }?: {
4
+ iframeTarget?: any;
5
+ iframeOrigin?: string;
6
+ css?: string;
7
+ });
4
8
  httpUrl: any;
9
+ /**
10
+ * When set, JS and module files are fetched in the parent context and forwarded
11
+ * to this window via postMessage instead of being injected as DOM elements.
12
+ * CSS routing is controlled separately by the `css` option.
13
+ * @type {Window | null}
14
+ */
15
+ iframeTarget: Window | null;
16
+ /** @type {string} */
17
+ iframeOrigin: string;
18
+ /**
19
+ * Controls where CSS files go when `iframeTarget` is set.
20
+ * - `'iframe'` -> forward only, skip parent injection (default)
21
+ * - `'parent'` -> load normally in parent, do not forward
22
+ * - `'both'` -> load in parent via `<link>` and forward to iframe
23
+ * @type {'iframe' | 'parent' | 'both'}
24
+ */
25
+ css: "iframe" | "parent" | "both";
5
26
  /**
6
27
  * Debounce state per file. Stores { timeout, resolvers[] } so that
7
28
  * when a rapid second change clears the first timeout, the first
@@ -20,12 +41,15 @@ export class FileLoader {
20
41
  */
21
42
  versions: Map<string, number>;
22
43
  loadFile(path: any): Promise<any>;
23
- loadCSS(path: any): Promise<any>;
44
+ loadCSS(path: any): Promise<boolean>;
45
+ _loadCSSInParent(path: any, url: any): Promise<any>;
24
46
  loadModule(path: any): Promise<boolean>;
25
47
  loadScript(path: any): Promise<any>;
26
48
  reloadFile(path: any): Promise<any>;
27
49
  _flushReload(path: any): Promise<void>;
28
50
  removeFile(path: any): Promise<void>;
29
51
  makeUrl(path: any): string;
52
+ _postAndAwaitAck(message: any): Promise<any>;
53
+ _inject(kind: any, code: any, file: any): Promise<any>;
30
54
  }
31
55
  //# sourceMappingURL=file-loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"file-loader.d.ts","sourceRoot":"","sources":["../../src/client/file-loader.js"],"names":[],"mappings":"AAAA,mFAAmF;AACnF;IACE,0BAgBC;IAfC,aAAsB;IACtB;;;;;OAKG;IACH,WAFU,GAAG,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,UAAU,CAAA;KAAE,CAAC,CAEvC;IAC1B;;;;;OAKG;IACH,UAFU,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAEJ;IAG3B,kCAOC;IAKD,iCAoBC;IAED,wCAIC;IAED,oCAeC;IAKD,oCAeC;IAED,uCAUC;IAED,qCAiBC;IAGD,2BAIC;CACF"}
1
+ {"version":3,"file":"file-loader.d.ts","sourceRoot":"","sources":["../../src/client/file-loader.js"],"names":[],"mappings":"AAAA,mFAAmF;AACnF;IACE;;;;OAiCC;IAhCC,aAAsB;IACtB;;;;;OAKG;IACH,cAFU,MAAM,GAAG,IAAI,CAES;IAChC,qBAAqB;IACrB,cADW,MAAM,CACe;IAChC;;;;;;OAMG;IACH,KAFU,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAExB;IACd;;;;;OAKG;IACH,WAFU,GAAG,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,UAAU,CAAA;KAAE,CAAC,CAEvC;IAC1B;;;;;OAKG;IACH,UAFU,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAEJ;IAG3B,kCAOC;IAKD,qCAwBC;IAED,oDAmBC;IAED,wCAcC;IAED,oCAwBC;IAKD,oCAeC;IAED,uCAUC;IAED,qCAqBC;IAGD,2BAIC;IAED,6CAaC;IAED,uDAEC;CACF"}
@@ -48,6 +48,10 @@ export class HMRClient {
48
48
  * @param {function(string): void} [options.onFileLoaded] - Called after each file loads or reloads. Receives `(filePath)`.
49
49
  * @param {function(string[]): string[]} [options.sortFiles] - Custom sort for the initial file load order. When provided, replaces `defaultSortFiles` entirely and `loadOrder` is ignored.
50
50
  * @param {Array<Function>} [options.loadOrder] - Stages prepended before the built-in sort (CSS-first, cold-first, alphabetical). One argument: return true to load that file first. Two arguments: works like a normal sort callback.
51
+ * @param {boolean | Object} [options.iframe] - Forward files to an iframe via `postMessage` (for Private Network Access restricted environments). Pass `true` for defaults.
52
+ * @param {Window | HTMLIFrameElement} [options.iframe.target] - Target a specific same-origin iframe directly, skipping auto-discovery. Reattachment is not automatic.
53
+ * @param {string} [options.iframe.origin] - The iframe's origin used to validate incoming handshake responses. Defaults to `'*'`.
54
+ * @param {'iframe'|'parent'|'both'} [options.iframe.css='iframe'] - Where CSS files are loaded when `iframe` is set.
51
55
  */
52
56
  constructor(options: {
53
57
  wsUrl?: string;
@@ -68,6 +72,7 @@ export class HMRClient {
68
72
  onFileLoaded?: (arg0: string) => void;
69
73
  sortFiles?: (arg0: string[]) => string[];
70
74
  loadOrder?: Array<Function>;
75
+ iframe?: boolean | any;
71
76
  });
72
77
  wsUrl: any;
73
78
  httpUrl: any;
@@ -93,6 +98,10 @@ export class HMRClient {
93
98
  _reconnectTimer: NodeJS.Timeout;
94
99
  _messageQueue: any[];
95
100
  _processingMessages: boolean;
101
+ _iframeTarget: any;
102
+ _iframeOrigin: any;
103
+ _stubManaged: boolean;
104
+ _onReattach: (e: any) => Promise<void>;
96
105
  fileLoader: FileLoader;
97
106
  /** @type {Map<string, string>} - Maps override file -> original file */
98
107
  overrideMap: Map<string, string>;
@@ -177,6 +186,8 @@ export class HMRClient {
177
186
  emit(event: any, ...args: any[]): void;
178
187
  _enqueueMessage(data: any): void;
179
188
  _drainMessageQueue(): Promise<void>;
189
+ _waitForStub(): Promise<any>;
190
+ _listenForReattach(): void;
180
191
  /**
181
192
  * Connect to the HMR server
182
193
  * @returns {Promise<void>}
@@ -1 +1 @@
1
- {"version":3,"file":"hmr-client.d.ts","sourceRoot":"","sources":["../../src/client/hmr-client.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH;IACE;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,qBAnBG;QAAyB,KAAK,GAAtB,MAAM;QACW,OAAO,GAAxB,MAAM;QACY,UAAU,GAA5B,OAAO;QACU,IAAI,GAArB,MAAM;QACW,IAAI,GAArB,MAAM;QACY,MAAM,GAAxB,OAAO;QACW,aAAa,GAA/B,OAAO;QACU,cAAc,GAA/B,MAAM;QACW,iBAAiB,GAAlC,MAAM;QACY,eAAe,GAAjC,OAAO;QACY,IAAI,GAAvB,MAAM,EAAE;QACsC,UAAU,GAAxD,CAAS,IAAM,EAAN,MAAM,EAAE,IAAQ,EAAR,MAAM,EAAE,KAAG,OAAO;QAChB,IAAI,GAAvB,MAAM,EAAE;QAC4B,UAAU,GAA9C,CAAS,IAAM,EAAN,MAAM,KAAG,OAAO;QACyB,iBAAiB,GAAnE,CAAS,IAAM,EAAN,MAAM,EAAE,IAAQ,EAAR,MAAM,EAAE,KAAG,MAAM,GAAC,IAAI;QACN,YAAY,GAA7C,CAAS,IAAM,EAAN,MAAM,KAAG,IAAI;QACiB,SAAS,GAAhD,CAAS,IAAQ,EAAR,MAAM,EAAE,KAAG,MAAM,EAAE;QACF,SAAS,GAAnC,KAAK,UAAU;KACzB,EAgEA;IAzDC,WAAkB;IAClB,aAAsB;IACtB,oBAAsB;IAEtB,+BAAyD;IACzD,uBAA+C;IAC/C,uBAAiD;IACjD,0BAAwD;IACxD,yBAAqD;IAGrD,wBAAsC;IACtC,oBAxBkB,MAAM,KAAG,OAAO,CAwBQ;IAG1C,oBAAiF;IACjF,gBAAuE;IAGvE,gBAAkB;IAElB,0BAhCkB,MAAM,QAAE,MAAM,EAAE,KAAG,MAAM,GAAC,IAAI,CAgCO;IACvD,qBAhCkB,MAAM,KAAG,IAAI,CAgCc;IAC7C,sBAAqC;IACrC,eAAmE;IAEnE,kBAAkB;IAClB,0BAA0B;IAC1B,qBAAwB;IACxB,6BAA8B;IAC9B,gCAA2B;IAI3B,qBAAuB;IACvB,6BAAgC;IAEhC,uBAA8C;IAE9C,wEAAwE;IACxE,aADW,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CACF;IAC5B,uFAAuF;IACvF,qBADW,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CACC;IAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAYC;IAGH,oCAkBC;IAED,8CAcC;IAED,mCAIC;IAED,sEAoDC;IAED,kCA0BC;IAED,4CAgDC;IAED,8EAkEC;IAED,2CA4CC;IAED,wCA6CC;IAED;;;;;OAKG;IACH,UAJW,MAAM,GAAC,QAAQ,GAAC,KAAK,GAAC,QAAQ,GAAC,MAAM,GAAC,SAAS,GAAC,YAAY,GAAC,OAAO,sBAElE,SAAS,CAQrB;IAED;;;;;OAKG;IACH,YAJW,MAAM,GAAC,QAAQ,GAAC,KAAK,GAAC,QAAQ,GAAC,MAAM,GAAC,SAAS,GAAC,YAAY,GAAC,OAAO,sBAElE,SAAS,CASrB;IAED;;;;;OAKG;IACH,WAJW,MAAM,GAAC,QAAQ,GAAC,KAAK,GAAC,QAAQ,GAAC,MAAM,GAAC,SAAS,GAAC,YAAY,GAAC,OAAO,sBAElE,SAAS,CAoBrB;IAED,uCAQC;IAMD,iCAGC;IAED,oCAWC;IAED;;;OAGG;IACH,WAFa,OAAO,CAAC,IAAI,CAAC,CAuFzB;IAED;;OAEG;IACH,mBAUC;CACF;2BAvoB0B,kBAAkB"}
1
+ {"version":3,"file":"hmr-client.d.ts","sourceRoot":"","sources":["../../src/client/hmr-client.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH;IACE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,qBAvBG;QAAyB,KAAK,GAAtB,MAAM;QACW,OAAO,GAAxB,MAAM;QACY,UAAU,GAA5B,OAAO;QACU,IAAI,GAArB,MAAM;QACW,IAAI,GAArB,MAAM;QACY,MAAM,GAAxB,OAAO;QACW,aAAa,GAA/B,OAAO;QACU,cAAc,GAA/B,MAAM;QACW,iBAAiB,GAAlC,MAAM;QACY,eAAe,GAAjC,OAAO;QACY,IAAI,GAAvB,MAAM,EAAE;QACsC,UAAU,GAAxD,CAAS,IAAM,EAAN,MAAM,EAAE,IAAQ,EAAR,MAAM,EAAE,KAAG,OAAO;QAChB,IAAI,GAAvB,MAAM,EAAE;QAC4B,UAAU,GAA9C,CAAS,IAAM,EAAN,MAAM,KAAG,OAAO;QACyB,iBAAiB,GAAnE,CAAS,IAAM,EAAN,MAAM,EAAE,IAAQ,EAAR,MAAM,EAAE,KAAG,MAAM,GAAC,IAAI;QACN,YAAY,GAA7C,CAAS,IAAM,EAAN,MAAM,KAAG,IAAI;QACiB,SAAS,GAAhD,CAAS,IAAQ,EAAR,MAAM,EAAE,KAAG,MAAM,EAAE;QACF,SAAS,GAAnC,KAAK,UAAU;QACY,MAAM,GAAjC,OAAO,MAAS;KACxB,EAwFF;IA9EC,WAAkB;IAClB,aAAsB;IACtB,oBAAsB;IAEtB,+BAAyD;IACzD,uBAA+C;IAC/C,uBAAiD;IACjD,0BAAwD;IACxD,yBAAqD;IAGrD,wBAAsC;IACtC,oBA5BkB,MAAM,KAAG,OAAO,CA4BQ;IAG1C,oBAAiF;IACjF,gBAAuE;IAGvE,gBAAkB;IAElB,0BApCkB,MAAM,QAAE,MAAM,EAAE,KAAG,MAAM,GAAC,IAAI,CAoCO;IACvD,qBApCkB,MAAM,KAAG,IAAI,CAoCc;IAC7C,sBAAqC;IACrC,eAAmE;IAEnE,kBAAkB;IAClB,0BAA0B;IAC1B,qBAAwB;IACxB,6BAA8B;IAC9B,gCAA2B;IAI3B,qBAAuB;IACvB,6BAAgC;IAShC,mBAAiC;IACjC,mBAAiC;IAIjC,sBAAiD;IAGjD,uCAAuB;IAEvB,uBAIE;IAEF,wEAAwE;IACxE,aADW,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CACF;IAC5B,uFAAuF;IACvF,qBADW,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CACC;IAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAYC;IAGH,oCAkBC;IAED,8CAcC;IAED,mCAIC;IAED,sEAoDC;IAED,kCA0BC;IAED,4CAgDC;IAED,8EAkEC;IAED,2CA4CC;IAED,wCA6CC;IAED;;;;;OAKG;IACH,UAJW,MAAM,GAAC,QAAQ,GAAC,KAAK,GAAC,QAAQ,GAAC,MAAM,GAAC,SAAS,GAAC,YAAY,GAAC,OAAO,sBAElE,SAAS,CAQrB;IAED;;;;;OAKG;IACH,YAJW,MAAM,GAAC,QAAQ,GAAC,KAAK,GAAC,QAAQ,GAAC,MAAM,GAAC,SAAS,GAAC,YAAY,GAAC,OAAO,sBAElE,SAAS,CASrB;IAED;;;;;OAKG;IACH,WAJW,MAAM,GAAC,QAAQ,GAAC,KAAK,GAAC,QAAQ,GAAC,MAAM,GAAC,SAAS,GAAC,YAAY,GAAC,OAAO,sBAElE,SAAS,CAoBrB;IAED,uCAQC;IAMD,iCAGC;IAED,oCAWC;IAID,6BAyBC;IAKD,2BA0BC;IAED;;;OAGG;IACH,WAFa,OAAO,CAAC,IAAI,CAAC,CAiGzB;IAED;;OAEG;IACH,mBAgBC;CACF;2BA5uB0B,kBAAkB"}
@@ -0,0 +1,2 @@
1
+ export function stub(): void;
2
+ //# sourceMappingURL=stub.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stub.d.ts","sourceRoot":"","sources":["../../src/client/stub.js"],"names":[],"mappings":"AAAA,6BAsEC"}
package/dist/client.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { FileLoader } from "./client/file-loader.js";
1
+ export { stub } from "./client/stub.js";
2
2
  export { HMRClient as default, HMRClient } from "./client/hmr-client.js";
3
3
  //# sourceMappingURL=client.d.ts.map
@@ -1551,15 +1551,18 @@ var HMR = (() => {
1551
1551
  // src/client.js
1552
1552
  var client_exports = {};
1553
1553
  __export(client_exports, {
1554
- FileLoader: () => FileLoader,
1555
1554
  HMRClient: () => HMRClient,
1556
- default: () => HMRClient
1555
+ default: () => HMRClient,
1556
+ stub: () => stub
1557
1557
  });
1558
1558
 
1559
1559
  // src/client/file-loader.js
1560
1560
  var FileLoader = class {
1561
- constructor(httpUrl) {
1561
+ constructor(httpUrl, { iframeTarget = null, iframeOrigin = "*", css = "iframe" } = {}) {
1562
1562
  this.httpUrl = httpUrl;
1563
+ this.iframeTarget = iframeTarget;
1564
+ this.iframeOrigin = iframeOrigin;
1565
+ this.css = css;
1563
1566
  this.loadQueue = /* @__PURE__ */ new Map();
1564
1567
  this.versions = /* @__PURE__ */ new Map();
1565
1568
  }
@@ -1574,8 +1577,28 @@ var HMR = (() => {
1574
1577
  // the old one. This fixes the brief flash of unstyled content that
1575
1578
  // happens when you remove the old sheet before the new one is parsed.
1576
1579
  async loadCSS(path) {
1577
- const existing = document.querySelector(`link[data-file="${path}"]`);
1578
1580
  const url = this.makeUrl(path);
1581
+ const toIframe = this.iframeTarget && this.css !== "parent";
1582
+ const toParent = !this.iframeTarget || this.css !== "iframe";
1583
+ const ops = [];
1584
+ if (toIframe) {
1585
+ ops.push(
1586
+ fetch(url).then((r) => {
1587
+ if (!r.ok) throw new Error(`Failed to fetch CSS: ${path} (${r.status})`);
1588
+ return r.text();
1589
+ }).then((code) => this._inject("css", code, path))
1590
+ );
1591
+ }
1592
+ if (toParent) {
1593
+ ops.push(this._loadCSSInParent(path, url));
1594
+ }
1595
+ const results = await Promise.allSettled(ops);
1596
+ const failed = results.find((r) => r.status === "rejected");
1597
+ if (failed) throw failed.reason;
1598
+ return true;
1599
+ }
1600
+ _loadCSSInParent(path, url) {
1601
+ const existing = document.querySelector(`link[data-file="${path}"]`);
1579
1602
  const link = document.createElement("link");
1580
1603
  link.rel = "stylesheet";
1581
1604
  link.href = url;
@@ -1594,6 +1617,14 @@ var HMR = (() => {
1594
1617
  }
1595
1618
  async loadModule(path) {
1596
1619
  const url = this.makeUrl(path);
1620
+ if (this.iframeTarget) {
1621
+ const code = await fetch(url).then((r) => {
1622
+ if (!r.ok) throw new Error(`Failed to fetch module: ${path} (${r.status})`);
1623
+ return r.text();
1624
+ });
1625
+ await this._inject("module", code, path);
1626
+ return true;
1627
+ }
1597
1628
  await import(url);
1598
1629
  return true;
1599
1630
  }
@@ -1601,6 +1632,14 @@ var HMR = (() => {
1601
1632
  const url = this.makeUrl(path);
1602
1633
  const existing = document.querySelector(`script[data-file="${path}"]`);
1603
1634
  if (existing) existing.remove();
1635
+ if (this.iframeTarget) {
1636
+ const code = await fetch(url).then((r) => {
1637
+ if (!r.ok) throw new Error(`Failed to fetch script: ${path} (${r.status})`);
1638
+ return r.text();
1639
+ });
1640
+ await this._inject("script", code, path);
1641
+ return true;
1642
+ }
1604
1643
  const script = document.createElement("script");
1605
1644
  script.src = url;
1606
1645
  script.setAttribute("data-file", path);
@@ -1646,10 +1685,14 @@ var HMR = (() => {
1646
1685
  for (const { reject } of entry.resolvers) reject(new Error(`File removed: ${path}`));
1647
1686
  this.loadQueue.delete(path);
1648
1687
  }
1649
- const el = document.querySelector(`[data-file="${path}"]`);
1650
- if (el) {
1651
- el.remove();
1652
- await new Promise((r) => setTimeout(r, 0));
1688
+ if (this.iframeTarget) {
1689
+ await this._postAndAwaitAck({ type: "hmr:remove", file: path });
1690
+ } else {
1691
+ const el = document.querySelector(`[data-file="${path}"]`);
1692
+ if (el) {
1693
+ el.remove();
1694
+ await Promise.resolve();
1695
+ }
1653
1696
  }
1654
1697
  this.versions.delete(path);
1655
1698
  }
@@ -1659,6 +1702,28 @@ var HMR = (() => {
1659
1702
  this.versions.set(path, v);
1660
1703
  return `${this.httpUrl}${path}?v=${v}`;
1661
1704
  }
1705
+ // Post a message and resolve once the stub sends back hmr:ack
1706
+ _postAndAwaitAck(message) {
1707
+ return new Promise((resolve) => {
1708
+ const onAck = (e) => {
1709
+ if (e.source !== this.iframeTarget) return;
1710
+ let data;
1711
+ try {
1712
+ data = JSON.parse(e.data);
1713
+ } catch {
1714
+ return;
1715
+ }
1716
+ if (data?.type !== "hmr:ack") return;
1717
+ window.removeEventListener("message", onAck);
1718
+ resolve();
1719
+ };
1720
+ window.addEventListener("message", onAck);
1721
+ this.iframeTarget.postMessage(JSON.stringify(message), this.iframeOrigin);
1722
+ });
1723
+ }
1724
+ _inject(kind, code, file) {
1725
+ return this._postAndAwaitAck({ type: "hmr:inject", kind, code, file });
1726
+ }
1662
1727
  };
1663
1728
 
1664
1729
  // src/shared/constants.js
@@ -1764,6 +1829,10 @@ var HMR = (() => {
1764
1829
  * @param {function(string): void} [options.onFileLoaded] - Called after each file loads or reloads. Receives `(filePath)`.
1765
1830
  * @param {function(string[]): string[]} [options.sortFiles] - Custom sort for the initial file load order. When provided, replaces `defaultSortFiles` entirely and `loadOrder` is ignored.
1766
1831
  * @param {Array<Function>} [options.loadOrder] - Stages prepended before the built-in sort (CSS-first, cold-first, alphabetical). One argument: return true to load that file first. Two arguments: works like a normal sort callback.
1832
+ * @param {boolean | Object} [options.iframe] - Forward files to an iframe via `postMessage` (for Private Network Access restricted environments). Pass `true` for defaults.
1833
+ * @param {Window | HTMLIFrameElement} [options.iframe.target] - Target a specific same-origin iframe directly, skipping auto-discovery. Reattachment is not automatic.
1834
+ * @param {string} [options.iframe.origin] - The iframe's origin used to validate incoming handshake responses. Defaults to `'*'`.
1835
+ * @param {'iframe'|'parent'|'both'} [options.iframe.css='iframe'] - Where CSS files are loaded when `iframe` is set.
1767
1836
  */
1768
1837
  constructor(options) {
1769
1838
  const opts = typeof options === "object" && !Array.isArray(options) ? options : {};
@@ -1793,7 +1862,18 @@ var HMR = (() => {
1793
1862
  this._reconnectTimer = null;
1794
1863
  this._messageQueue = [];
1795
1864
  this._processingMessages = false;
1796
- this.fileLoader = new FileLoader(this.httpUrl);
1865
+ const iframeOpts = opts.iframe === true ? {} : opts.iframe;
1866
+ const iframeTarget = iframeOpts?.target ? iframeOpts.target?.contentWindow ?? null : null;
1867
+ const iframeOrigin = iframeOpts?.origin ?? "*";
1868
+ this._iframeTarget = iframeTarget;
1869
+ this._iframeOrigin = iframeOrigin;
1870
+ this._stubManaged = !!iframeOpts && !iframeTarget;
1871
+ this._onReattach = null;
1872
+ this.fileLoader = new FileLoader(this.httpUrl, {
1873
+ iframeTarget,
1874
+ iframeOrigin,
1875
+ css: iframeOpts?.css ?? "iframe"
1876
+ });
1797
1877
  this.overrideMap = /* @__PURE__ */ new Map();
1798
1878
  this._reverseOverrideMap = /* @__PURE__ */ new Map();
1799
1879
  this.logStyles = {
@@ -1940,13 +2020,13 @@ var HMR = (() => {
1940
2020
  }
1941
2021
  }
1942
2022
  const withOverrides = this.buildOverrideMap(toLoad);
1943
- const sorted2 = this.sortFiles(withOverrides);
1944
- this.logInitFileGroup(sorted2, this.overrideMap, this.isColdFile.bind(this));
1945
- for (const file of sorted2) {
2023
+ const sorted = this.sortFiles(withOverrides);
2024
+ this.logInitFileGroup(sorted, this.overrideMap, this.isColdFile.bind(this));
2025
+ for (const file of sorted) {
1946
2026
  await this.fileLoader.loadFile(file);
1947
2027
  if (this.onFileLoaded) this.onFileLoaded(file);
1948
2028
  }
1949
- this.log("success", `HMR client ready (${sorted2.length} files loaded)`);
2029
+ this.log("success", `HMR client ready (${sorted.length} files loaded)`);
1950
2030
  }
1951
2031
  async handleFileChange(file, action, serverCold = false) {
1952
2032
  if (this.shouldSkipFile(file, this.allFiles)) {
@@ -2050,7 +2130,7 @@ var HMR = (() => {
2050
2130
  await this.processInitFiles(data.files);
2051
2131
  } else {
2052
2132
  const modeLabel = this.watchFiles ? "HMR ready" : "Static snapshot ready";
2053
- this.log("success", `${modeLabel} (${sorted.length} files loaded)`);
2133
+ this.log("success", `${modeLabel} (0 files loaded)`);
2054
2134
  }
2055
2135
  return;
2056
2136
  }
@@ -2145,6 +2225,65 @@ var HMR = (() => {
2145
2225
  }
2146
2226
  this._processingMessages = false;
2147
2227
  }
2228
+ // Wait for stub's hmr:ready signal. Stub fires it proactively on run.
2229
+ // Times out after 5s and resolves anyway, a missing stub degrades gracefully.
2230
+ _waitForStub() {
2231
+ return new Promise((resolve) => {
2232
+ const timer = setTimeout(() => {
2233
+ window.removeEventListener("message", onReady);
2234
+ this.log("warning", "Timed out waiting for hmr:ready. Was HMR.stub() called in the iframe?");
2235
+ resolve();
2236
+ }, 5e3);
2237
+ const onReady = (e) => {
2238
+ let data;
2239
+ try {
2240
+ data = JSON.parse(e.data);
2241
+ } catch {
2242
+ return;
2243
+ }
2244
+ if (data?.type !== "hmr:ready") return;
2245
+ const originOk = this._iframeOrigin === "*" || e.origin === this._iframeOrigin;
2246
+ if (!originOk) return;
2247
+ clearTimeout(timer);
2248
+ window.removeEventListener("message", onReady);
2249
+ if (this._stubManaged) {
2250
+ this._iframeTarget = e.source;
2251
+ this.fileLoader.iframeTarget = e.source;
2252
+ }
2253
+ resolve();
2254
+ };
2255
+ window.addEventListener("message", onReady);
2256
+ });
2257
+ }
2258
+ // Persistent listener for hmr:ready that handles iframe reattachment. If the
2259
+ // iframe reloads or is replaced, the stub fires hmr:ready again so we can
2260
+ // update the target and re-inject all currently loaded files.
2261
+ _listenForReattach() {
2262
+ if (this._onReattach) return;
2263
+ this._onReattach = async (e) => {
2264
+ let data;
2265
+ try {
2266
+ data = JSON.parse(e.data);
2267
+ } catch {
2268
+ return;
2269
+ }
2270
+ if (data?.type !== "hmr:ready") return;
2271
+ const originOk = this._iframeOrigin === "*" || e.origin === this._iframeOrigin;
2272
+ if (!originOk) return;
2273
+ if (e.source === this._iframeTarget) return;
2274
+ this._iframeTarget = e.source;
2275
+ this.fileLoader.iframeTarget = e.source;
2276
+ this.log("success", "HMR reattached to new iframe");
2277
+ for (const path of this.sortFiles([...this.fileLoader.versions.keys()])) {
2278
+ try {
2279
+ await this.fileLoader.loadFile(path);
2280
+ } catch (e2) {
2281
+ this.log("error", `Reattach failed to load ${path}: ${e2.message}`);
2282
+ }
2283
+ }
2284
+ };
2285
+ window.addEventListener("message", this._onReattach);
2286
+ }
2148
2287
  /**
2149
2288
  * Connect to the HMR server
2150
2289
  * @returns {Promise<void>}
@@ -2167,14 +2306,20 @@ var HMR = (() => {
2167
2306
  let settled = false;
2168
2307
  try {
2169
2308
  this.socket = new WebSocket(this.wsUrl);
2170
- this.socket.onopen = () => {
2309
+ this.socket.onopen = async () => {
2171
2310
  settled = true;
2172
2311
  this.isConnected = true;
2173
2312
  this.reconnectAttempts = 0;
2174
- this._messageQueue = [];
2175
- this._processingMessages = false;
2176
2313
  this.log("success", "HMR connected");
2177
2314
  this.emit("connect");
2315
+ this._messageQueue = [];
2316
+ this._processingMessages = true;
2317
+ if (this._iframeTarget || this._stubManaged) {
2318
+ await this._waitForStub();
2319
+ if (this._stubManaged) this._listenForReattach();
2320
+ }
2321
+ this._processingMessages = false;
2322
+ if (this._messageQueue.length > 0) this._drainMessageQueue();
2178
2323
  resolve();
2179
2324
  };
2180
2325
  this.socket.onclose = () => {
@@ -2230,12 +2375,90 @@ var HMR = (() => {
2230
2375
  this.isConnected = false;
2231
2376
  clearTimeout(this._reconnectTimer);
2232
2377
  this._reconnectTimer = null;
2378
+ if (this._onReattach) {
2379
+ window.removeEventListener("message", this._onReattach);
2380
+ this._onReattach = null;
2381
+ }
2233
2382
  if (this.socket) {
2234
2383
  this.socket.close();
2235
2384
  this.socket = null;
2236
2385
  }
2237
2386
  }
2238
2387
  };
2388
+
2389
+ // src/client/stub.js
2390
+ function stub() {
2391
+ const byFile = (tag, file) => document.querySelector(`${tag}[data-file="${CSS.escape(file)}"]`);
2392
+ const removeIfExists = (el) => {
2393
+ if (el) el.remove();
2394
+ };
2395
+ const injectScript = async (kind, code, file) => {
2396
+ const url = URL.createObjectURL(new Blob([code + `
2397
+ //# sourceURL=${file}`], { type: "text/javascript" }));
2398
+ try {
2399
+ if (kind === "module") {
2400
+ await import(url);
2401
+ } else {
2402
+ const script = document.createElement("script");
2403
+ script.src = url;
2404
+ await new Promise((resolve, reject) => {
2405
+ script.onload = resolve;
2406
+ script.onerror = () => reject(new Error(`Failed to execute script: ${file}`));
2407
+ document.documentElement.appendChild(script);
2408
+ });
2409
+ }
2410
+ } finally {
2411
+ URL.revokeObjectURL(url);
2412
+ }
2413
+ };
2414
+ const injectStyle = async (code, file) => {
2415
+ const existing = byFile("link", file);
2416
+ const url = URL.createObjectURL(new Blob([code + `
2417
+ /*# sourceURL=${file} */`], { type: "text/css" }));
2418
+ const link = document.createElement("link");
2419
+ link.rel = "stylesheet";
2420
+ link.href = url;
2421
+ link.dataset.file = file;
2422
+ await new Promise((resolve, reject) => {
2423
+ link.onload = () => {
2424
+ URL.revokeObjectURL(url);
2425
+ resolve();
2426
+ };
2427
+ link.onerror = () => {
2428
+ URL.revokeObjectURL(url);
2429
+ reject(new Error(`Failed to load CSS: ${file}`));
2430
+ };
2431
+ document.head.appendChild(link);
2432
+ });
2433
+ removeIfExists(existing);
2434
+ };
2435
+ window.addEventListener("message", async (e) => {
2436
+ let data;
2437
+ try {
2438
+ data = JSON.parse(e.data);
2439
+ } catch {
2440
+ return;
2441
+ }
2442
+ if (!data?.type) return;
2443
+ const ackOrigin = e.origin && e.origin !== "null" ? e.origin : "*";
2444
+ const ack = () => e.source?.postMessage(JSON.stringify({ type: "hmr:ack" }), ackOrigin);
2445
+ if (data.type === "hmr:remove") {
2446
+ removeIfExists(document.querySelector(`[data-file="${CSS.escape(data.file)}"]`));
2447
+ ack();
2448
+ return;
2449
+ }
2450
+ if (data.type !== "hmr:inject") return;
2451
+ const { kind, code, file } = data;
2452
+ try {
2453
+ if (kind === "script" || kind === "module") await injectScript(kind, code, file);
2454
+ else if (kind === "css") await injectStyle(code, file);
2455
+ } catch (err) {
2456
+ console.error(`Failed to inject ${file}:`, err.message ?? err, "\n" + err.stack);
2457
+ }
2458
+ ack();
2459
+ });
2460
+ window.parent.postMessage(JSON.stringify({ type: "hmr:ready" }), "*");
2461
+ }
2239
2462
  return __toCommonJS(client_exports);
2240
2463
  })();
2241
2464
  //# sourceMappingURL=client.iife.js.map