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/dist/client.js CHANGED
@@ -1544,8 +1544,11 @@ var require_picomatch2 = __commonJS({
1544
1544
 
1545
1545
  // src/client/file-loader.js
1546
1546
  var FileLoader = class {
1547
- constructor(httpUrl) {
1547
+ constructor(httpUrl, { iframeTarget = null, iframeOrigin = "*", css = "iframe" } = {}) {
1548
1548
  this.httpUrl = httpUrl;
1549
+ this.iframeTarget = iframeTarget;
1550
+ this.iframeOrigin = iframeOrigin;
1551
+ this.css = css;
1549
1552
  this.loadQueue = /* @__PURE__ */ new Map();
1550
1553
  this.versions = /* @__PURE__ */ new Map();
1551
1554
  }
@@ -1560,8 +1563,28 @@ var FileLoader = class {
1560
1563
  // the old one. This fixes the brief flash of unstyled content that
1561
1564
  // happens when you remove the old sheet before the new one is parsed.
1562
1565
  async loadCSS(path) {
1563
- const existing = document.querySelector(`link[data-file="${path}"]`);
1564
1566
  const url = this.makeUrl(path);
1567
+ const toIframe = this.iframeTarget && this.css !== "parent";
1568
+ const toParent = !this.iframeTarget || this.css !== "iframe";
1569
+ const ops = [];
1570
+ if (toIframe) {
1571
+ ops.push(
1572
+ fetch(url).then((r) => {
1573
+ if (!r.ok) throw new Error(`Failed to fetch CSS: ${path} (${r.status})`);
1574
+ return r.text();
1575
+ }).then((code) => this._inject("css", code, path))
1576
+ );
1577
+ }
1578
+ if (toParent) {
1579
+ ops.push(this._loadCSSInParent(path, url));
1580
+ }
1581
+ const results = await Promise.allSettled(ops);
1582
+ const failed = results.find((r) => r.status === "rejected");
1583
+ if (failed) throw failed.reason;
1584
+ return true;
1585
+ }
1586
+ _loadCSSInParent(path, url) {
1587
+ const existing = document.querySelector(`link[data-file="${path}"]`);
1565
1588
  const link = document.createElement("link");
1566
1589
  link.rel = "stylesheet";
1567
1590
  link.href = url;
@@ -1580,6 +1603,14 @@ var FileLoader = class {
1580
1603
  }
1581
1604
  async loadModule(path) {
1582
1605
  const url = this.makeUrl(path);
1606
+ if (this.iframeTarget) {
1607
+ const code = await fetch(url).then((r) => {
1608
+ if (!r.ok) throw new Error(`Failed to fetch module: ${path} (${r.status})`);
1609
+ return r.text();
1610
+ });
1611
+ await this._inject("module", code, path);
1612
+ return true;
1613
+ }
1583
1614
  await import(url);
1584
1615
  return true;
1585
1616
  }
@@ -1587,6 +1618,14 @@ var FileLoader = class {
1587
1618
  const url = this.makeUrl(path);
1588
1619
  const existing = document.querySelector(`script[data-file="${path}"]`);
1589
1620
  if (existing) existing.remove();
1621
+ if (this.iframeTarget) {
1622
+ const code = await fetch(url).then((r) => {
1623
+ if (!r.ok) throw new Error(`Failed to fetch script: ${path} (${r.status})`);
1624
+ return r.text();
1625
+ });
1626
+ await this._inject("script", code, path);
1627
+ return true;
1628
+ }
1590
1629
  const script = document.createElement("script");
1591
1630
  script.src = url;
1592
1631
  script.setAttribute("data-file", path);
@@ -1632,10 +1671,14 @@ var FileLoader = class {
1632
1671
  for (const { reject } of entry.resolvers) reject(new Error(`File removed: ${path}`));
1633
1672
  this.loadQueue.delete(path);
1634
1673
  }
1635
- const el = document.querySelector(`[data-file="${path}"]`);
1636
- if (el) {
1637
- el.remove();
1638
- await new Promise((r) => setTimeout(r, 0));
1674
+ if (this.iframeTarget) {
1675
+ await this._postAndAwaitAck({ type: "hmr:remove", file: path });
1676
+ } else {
1677
+ const el = document.querySelector(`[data-file="${path}"]`);
1678
+ if (el) {
1679
+ el.remove();
1680
+ await Promise.resolve();
1681
+ }
1639
1682
  }
1640
1683
  this.versions.delete(path);
1641
1684
  }
@@ -1645,6 +1688,28 @@ var FileLoader = class {
1645
1688
  this.versions.set(path, v);
1646
1689
  return `${this.httpUrl}${path}?v=${v}`;
1647
1690
  }
1691
+ // Post a message and resolve once the stub sends back hmr:ack
1692
+ _postAndAwaitAck(message) {
1693
+ return new Promise((resolve) => {
1694
+ const onAck = (e) => {
1695
+ if (e.source !== this.iframeTarget) return;
1696
+ let data;
1697
+ try {
1698
+ data = JSON.parse(e.data);
1699
+ } catch {
1700
+ return;
1701
+ }
1702
+ if (data?.type !== "hmr:ack") return;
1703
+ window.removeEventListener("message", onAck);
1704
+ resolve();
1705
+ };
1706
+ window.addEventListener("message", onAck);
1707
+ this.iframeTarget.postMessage(JSON.stringify(message), this.iframeOrigin);
1708
+ });
1709
+ }
1710
+ _inject(kind, code, file) {
1711
+ return this._postAndAwaitAck({ type: "hmr:inject", kind, code, file });
1712
+ }
1648
1713
  };
1649
1714
 
1650
1715
  // src/shared/constants.js
@@ -1750,6 +1815,10 @@ var HMRClient = class {
1750
1815
  * @param {function(string): void} [options.onFileLoaded] - Called after each file loads or reloads. Receives `(filePath)`.
1751
1816
  * @param {function(string[]): string[]} [options.sortFiles] - Custom sort for the initial file load order. When provided, replaces `defaultSortFiles` entirely and `loadOrder` is ignored.
1752
1817
  * @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.
1818
+ * @param {boolean | Object} [options.iframe] - Forward files to an iframe via `postMessage` (for Private Network Access restricted environments). Pass `true` for defaults.
1819
+ * @param {Window | HTMLIFrameElement} [options.iframe.target] - Target a specific same-origin iframe directly, skipping auto-discovery. Reattachment is not automatic.
1820
+ * @param {string} [options.iframe.origin] - The iframe's origin used to validate incoming handshake responses. Defaults to `'*'`.
1821
+ * @param {'iframe'|'parent'|'both'} [options.iframe.css='iframe'] - Where CSS files are loaded when `iframe` is set.
1753
1822
  */
1754
1823
  constructor(options) {
1755
1824
  const opts = typeof options === "object" && !Array.isArray(options) ? options : {};
@@ -1779,7 +1848,18 @@ var HMRClient = class {
1779
1848
  this._reconnectTimer = null;
1780
1849
  this._messageQueue = [];
1781
1850
  this._processingMessages = false;
1782
- this.fileLoader = new FileLoader(this.httpUrl);
1851
+ const iframeOpts = opts.iframe === true ? {} : opts.iframe;
1852
+ const iframeTarget = iframeOpts?.target ? iframeOpts.target?.contentWindow ?? null : null;
1853
+ const iframeOrigin = iframeOpts?.origin ?? "*";
1854
+ this._iframeTarget = iframeTarget;
1855
+ this._iframeOrigin = iframeOrigin;
1856
+ this._stubManaged = !!iframeOpts && !iframeTarget;
1857
+ this._onReattach = null;
1858
+ this.fileLoader = new FileLoader(this.httpUrl, {
1859
+ iframeTarget,
1860
+ iframeOrigin,
1861
+ css: iframeOpts?.css ?? "iframe"
1862
+ });
1783
1863
  this.overrideMap = /* @__PURE__ */ new Map();
1784
1864
  this._reverseOverrideMap = /* @__PURE__ */ new Map();
1785
1865
  this.logStyles = {
@@ -1926,13 +2006,13 @@ var HMRClient = class {
1926
2006
  }
1927
2007
  }
1928
2008
  const withOverrides = this.buildOverrideMap(toLoad);
1929
- const sorted2 = this.sortFiles(withOverrides);
1930
- this.logInitFileGroup(sorted2, this.overrideMap, this.isColdFile.bind(this));
1931
- for (const file of sorted2) {
2009
+ const sorted = this.sortFiles(withOverrides);
2010
+ this.logInitFileGroup(sorted, this.overrideMap, this.isColdFile.bind(this));
2011
+ for (const file of sorted) {
1932
2012
  await this.fileLoader.loadFile(file);
1933
2013
  if (this.onFileLoaded) this.onFileLoaded(file);
1934
2014
  }
1935
- this.log("success", `HMR client ready (${sorted2.length} files loaded)`);
2015
+ this.log("success", `HMR client ready (${sorted.length} files loaded)`);
1936
2016
  }
1937
2017
  async handleFileChange(file, action, serverCold = false) {
1938
2018
  if (this.shouldSkipFile(file, this.allFiles)) {
@@ -2036,7 +2116,7 @@ var HMRClient = class {
2036
2116
  await this.processInitFiles(data.files);
2037
2117
  } else {
2038
2118
  const modeLabel = this.watchFiles ? "HMR ready" : "Static snapshot ready";
2039
- this.log("success", `${modeLabel} (${sorted.length} files loaded)`);
2119
+ this.log("success", `${modeLabel} (0 files loaded)`);
2040
2120
  }
2041
2121
  return;
2042
2122
  }
@@ -2131,6 +2211,65 @@ var HMRClient = class {
2131
2211
  }
2132
2212
  this._processingMessages = false;
2133
2213
  }
2214
+ // Wait for stub's hmr:ready signal. Stub fires it proactively on run.
2215
+ // Times out after 5s and resolves anyway, a missing stub degrades gracefully.
2216
+ _waitForStub() {
2217
+ return new Promise((resolve) => {
2218
+ const timer = setTimeout(() => {
2219
+ window.removeEventListener("message", onReady);
2220
+ this.log("warning", "Timed out waiting for hmr:ready. Was HMR.stub() called in the iframe?");
2221
+ resolve();
2222
+ }, 5e3);
2223
+ const onReady = (e) => {
2224
+ let data;
2225
+ try {
2226
+ data = JSON.parse(e.data);
2227
+ } catch {
2228
+ return;
2229
+ }
2230
+ if (data?.type !== "hmr:ready") return;
2231
+ const originOk = this._iframeOrigin === "*" || e.origin === this._iframeOrigin;
2232
+ if (!originOk) return;
2233
+ clearTimeout(timer);
2234
+ window.removeEventListener("message", onReady);
2235
+ if (this._stubManaged) {
2236
+ this._iframeTarget = e.source;
2237
+ this.fileLoader.iframeTarget = e.source;
2238
+ }
2239
+ resolve();
2240
+ };
2241
+ window.addEventListener("message", onReady);
2242
+ });
2243
+ }
2244
+ // Persistent listener for hmr:ready that handles iframe reattachment. If the
2245
+ // iframe reloads or is replaced, the stub fires hmr:ready again so we can
2246
+ // update the target and re-inject all currently loaded files.
2247
+ _listenForReattach() {
2248
+ if (this._onReattach) return;
2249
+ this._onReattach = async (e) => {
2250
+ let data;
2251
+ try {
2252
+ data = JSON.parse(e.data);
2253
+ } catch {
2254
+ return;
2255
+ }
2256
+ if (data?.type !== "hmr:ready") return;
2257
+ const originOk = this._iframeOrigin === "*" || e.origin === this._iframeOrigin;
2258
+ if (!originOk) return;
2259
+ if (e.source === this._iframeTarget) return;
2260
+ this._iframeTarget = e.source;
2261
+ this.fileLoader.iframeTarget = e.source;
2262
+ this.log("success", "HMR reattached to new iframe");
2263
+ for (const path of this.sortFiles([...this.fileLoader.versions.keys()])) {
2264
+ try {
2265
+ await this.fileLoader.loadFile(path);
2266
+ } catch (e2) {
2267
+ this.log("error", `Reattach failed to load ${path}: ${e2.message}`);
2268
+ }
2269
+ }
2270
+ };
2271
+ window.addEventListener("message", this._onReattach);
2272
+ }
2134
2273
  /**
2135
2274
  * Connect to the HMR server
2136
2275
  * @returns {Promise<void>}
@@ -2153,14 +2292,20 @@ var HMRClient = class {
2153
2292
  let settled = false;
2154
2293
  try {
2155
2294
  this.socket = new WebSocket(this.wsUrl);
2156
- this.socket.onopen = () => {
2295
+ this.socket.onopen = async () => {
2157
2296
  settled = true;
2158
2297
  this.isConnected = true;
2159
2298
  this.reconnectAttempts = 0;
2160
- this._messageQueue = [];
2161
- this._processingMessages = false;
2162
2299
  this.log("success", "HMR connected");
2163
2300
  this.emit("connect");
2301
+ this._messageQueue = [];
2302
+ this._processingMessages = true;
2303
+ if (this._iframeTarget || this._stubManaged) {
2304
+ await this._waitForStub();
2305
+ if (this._stubManaged) this._listenForReattach();
2306
+ }
2307
+ this._processingMessages = false;
2308
+ if (this._messageQueue.length > 0) this._drainMessageQueue();
2164
2309
  resolve();
2165
2310
  };
2166
2311
  this.socket.onclose = () => {
@@ -2216,15 +2361,93 @@ var HMRClient = class {
2216
2361
  this.isConnected = false;
2217
2362
  clearTimeout(this._reconnectTimer);
2218
2363
  this._reconnectTimer = null;
2364
+ if (this._onReattach) {
2365
+ window.removeEventListener("message", this._onReattach);
2366
+ this._onReattach = null;
2367
+ }
2219
2368
  if (this.socket) {
2220
2369
  this.socket.close();
2221
2370
  this.socket = null;
2222
2371
  }
2223
2372
  }
2224
2373
  };
2374
+
2375
+ // src/client/stub.js
2376
+ function stub() {
2377
+ const byFile = (tag, file) => document.querySelector(`${tag}[data-file="${CSS.escape(file)}"]`);
2378
+ const removeIfExists = (el) => {
2379
+ if (el) el.remove();
2380
+ };
2381
+ const injectScript = async (kind, code, file) => {
2382
+ const url = URL.createObjectURL(new Blob([code + `
2383
+ //# sourceURL=${file}`], { type: "text/javascript" }));
2384
+ try {
2385
+ if (kind === "module") {
2386
+ await import(url);
2387
+ } else {
2388
+ const script = document.createElement("script");
2389
+ script.src = url;
2390
+ await new Promise((resolve, reject) => {
2391
+ script.onload = resolve;
2392
+ script.onerror = () => reject(new Error(`Failed to execute script: ${file}`));
2393
+ document.documentElement.appendChild(script);
2394
+ });
2395
+ }
2396
+ } finally {
2397
+ URL.revokeObjectURL(url);
2398
+ }
2399
+ };
2400
+ const injectStyle = async (code, file) => {
2401
+ const existing = byFile("link", file);
2402
+ const url = URL.createObjectURL(new Blob([code + `
2403
+ /*# sourceURL=${file} */`], { type: "text/css" }));
2404
+ const link = document.createElement("link");
2405
+ link.rel = "stylesheet";
2406
+ link.href = url;
2407
+ link.dataset.file = file;
2408
+ await new Promise((resolve, reject) => {
2409
+ link.onload = () => {
2410
+ URL.revokeObjectURL(url);
2411
+ resolve();
2412
+ };
2413
+ link.onerror = () => {
2414
+ URL.revokeObjectURL(url);
2415
+ reject(new Error(`Failed to load CSS: ${file}`));
2416
+ };
2417
+ document.head.appendChild(link);
2418
+ });
2419
+ removeIfExists(existing);
2420
+ };
2421
+ window.addEventListener("message", async (e) => {
2422
+ let data;
2423
+ try {
2424
+ data = JSON.parse(e.data);
2425
+ } catch {
2426
+ return;
2427
+ }
2428
+ if (!data?.type) return;
2429
+ const ackOrigin = e.origin && e.origin !== "null" ? e.origin : "*";
2430
+ const ack = () => e.source?.postMessage(JSON.stringify({ type: "hmr:ack" }), ackOrigin);
2431
+ if (data.type === "hmr:remove") {
2432
+ removeIfExists(document.querySelector(`[data-file="${CSS.escape(data.file)}"]`));
2433
+ ack();
2434
+ return;
2435
+ }
2436
+ if (data.type !== "hmr:inject") return;
2437
+ const { kind, code, file } = data;
2438
+ try {
2439
+ if (kind === "script" || kind === "module") await injectScript(kind, code, file);
2440
+ else if (kind === "css") await injectStyle(code, file);
2441
+ } catch (err) {
2442
+ console.error(`Failed to inject ${file}:`, err.message ?? err, "\n" + err.stack);
2443
+ }
2444
+ ack();
2445
+ });
2446
+ window.parent.postMessage(JSON.stringify({ type: "hmr:ready" }), "*");
2447
+ }
2225
2448
  export {
2226
- FileLoader,
2227
2449
  HMRClient,
2228
- HMRClient as default
2450
+ HMRClient as default,
2451
+ stub
2229
2452
  };
2230
2453
  //# sourceMappingURL=client.js.map