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 +39 -2
- package/dist/client/file-loader.d.ts +26 -2
- package/dist/client/file-loader.d.ts.map +1 -1
- package/dist/client/hmr-client.d.ts +11 -0
- package/dist/client/hmr-client.d.ts.map +1 -1
- package/dist/client/stub.d.ts +2 -0
- package/dist/client/stub.d.ts.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.iife.js +240 -17
- package/dist/client.iife.js.map +4 -4
- package/dist/client.iife.min.js +4 -1
- package/dist/client.iife.min.js.map +4 -4
- package/dist/client.js +240 -17
- package/dist/client.js.map +4 -4
- package/package.json +1 -1
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<
|
|
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
|
|
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
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"stub.d.ts","sourceRoot":"","sources":["../../src/client/stub.js"],"names":[],"mappings":"AAAA,6BAsEC"}
|
package/dist/client.d.ts
CHANGED
package/dist/client.iife.js
CHANGED
|
@@ -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
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
-
|
|
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
|
|
1944
|
-
this.logInitFileGroup(
|
|
1945
|
-
for (const file of
|
|
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 (${
|
|
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} (
|
|
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
|