cindel 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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) => {
1576
+ this._inject("css", code, path);
1577
+ })
1578
+ );
1579
+ }
1580
+ if (toParent) {
1581
+ ops.push(this._loadCSSInParent(path, url));
1582
+ }
1583
+ await Promise.all(ops);
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
+ 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
+ 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,15 @@ 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
+ this._post({ type: "hmr:remove", file: path });
1676
+ await Promise.resolve();
1677
+ } else {
1678
+ const el = document.querySelector(`[data-file="${path}"]`);
1679
+ if (el) {
1680
+ el.remove();
1681
+ await Promise.resolve();
1682
+ }
1639
1683
  }
1640
1684
  this.versions.delete(path);
1641
1685
  }
@@ -1645,6 +1689,14 @@ var FileLoader = class {
1645
1689
  this.versions.set(path, v);
1646
1690
  return `${this.httpUrl}${path}?v=${v}`;
1647
1691
  }
1692
+ // Send a raw postMessage to the iframe target
1693
+ _post(message) {
1694
+ this.iframeTarget.postMessage(message, this.iframeOrigin);
1695
+ }
1696
+ // Forward a file payload to the iframe target
1697
+ _inject(kind, code, file) {
1698
+ this._post({ type: "hmr:inject", kind, code, file });
1699
+ }
1648
1700
  };
1649
1701
 
1650
1702
  // src/shared/constants.js
@@ -1748,7 +1800,12 @@ var HMRClient = class {
1748
1800
  * @param {function(string): boolean} [options.filterCold] - Custom cold file logic. Receives `(filePath)`. Combined with `cold` via OR.
1749
1801
  * @param {function(string, string[]): string|null} [options.getOverrideTarget] - Given a changed file, return the path of the original it replaces, or `null`. Receives `(filePath, allFiles)`. When matched, the original is unloaded before the override loads.
1750
1802
  * @param {function(string): void} [options.onFileLoaded] - Called after each file loads or reloads. Receives `(filePath)`.
1751
- * @param {function(string[]): string[]} [options.sortFiles] - Custom sort for the initial file load order. Default sorts CSS before JS, cold files first.
1803
+ * @param {function(string[]): string[]} [options.sortFiles] - Custom sort for the initial file load order. When provided, replaces `defaultSortFiles` entirely and `loadOrder` is ignored.
1804
+ * @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.
1805
+ * @param {boolean | Object} [options.iframe] - Forward files to an iframe via `postMessage` (for Private Network Access restricted environments). Pass `true` for defaults.
1806
+ * @param {Window | HTMLIFrameElement} [options.iframe.target] - Target a specific same-origin iframe directly, skipping auto-discovery. Reattachment is not automatic.
1807
+ * @param {string} [options.iframe.origin] - The iframe's origin used to validate incoming handshake responses. Defaults to `'*'`.
1808
+ * @param {'iframe'|'parent'|'both'} [options.iframe.css='iframe'] - Where CSS files are loaded when `iframe` is set.
1752
1809
  */
1753
1810
  constructor(options) {
1754
1811
  const opts = typeof options === "object" && !Array.isArray(options) ? options : {};
@@ -1769,6 +1826,7 @@ var HMRClient = class {
1769
1826
  this.allFiles = [];
1770
1827
  this.getOverrideTarget = opts.getOverrideTarget || null;
1771
1828
  this.onFileLoaded = opts.onFileLoaded || null;
1829
+ this.loadOrder = opts.loadOrder || [];
1772
1830
  this.sortFiles = opts.sortFiles || this.defaultSortFiles.bind(this);
1773
1831
  this.socket = null;
1774
1832
  this.reconnectAttempts = 0;
@@ -1777,7 +1835,18 @@ var HMRClient = class {
1777
1835
  this._reconnectTimer = null;
1778
1836
  this._messageQueue = [];
1779
1837
  this._processingMessages = false;
1780
- this.fileLoader = new FileLoader(this.httpUrl);
1838
+ const iframeOpts = opts.iframe === true ? {} : opts.iframe;
1839
+ const iframeTarget = iframeOpts?.target ? iframeOpts.target?.contentWindow ?? null : null;
1840
+ const iframeOrigin = iframeOpts?.origin ?? "*";
1841
+ this._iframeTarget = iframeTarget;
1842
+ this._iframeOrigin = iframeOrigin;
1843
+ this._stubManaged = !!iframeOpts && !iframeTarget;
1844
+ this._onReattach = null;
1845
+ this.fileLoader = new FileLoader(this.httpUrl, {
1846
+ iframeTarget,
1847
+ iframeOrigin,
1848
+ css: iframeOpts?.css ?? "iframe"
1849
+ });
1781
1850
  this.overrideMap = /* @__PURE__ */ new Map();
1782
1851
  this._reverseOverrideMap = /* @__PURE__ */ new Map();
1783
1852
  this.logStyles = {
@@ -1796,16 +1865,18 @@ var HMRClient = class {
1796
1865
  }
1797
1866
  defaultSortFiles(files) {
1798
1867
  const coldSet = new Set(files.filter((f) => this.isColdFile(f)));
1868
+ const builtinStages = [
1869
+ (f) => f.endsWith(".css"),
1870
+ (f) => coldSet.has(f),
1871
+ (a, b) => a.localeCompare(b)
1872
+ ];
1873
+ const stages = [...this.loadOrder, ...builtinStages];
1799
1874
  return [...files].sort((a, b) => {
1800
- const aIsCSS = a.endsWith(".css");
1801
- const bIsCSS = b.endsWith(".css");
1802
- if (aIsCSS && !bIsCSS) return -1;
1803
- if (!aIsCSS && bIsCSS) return 1;
1804
- const coldA = coldSet.has(a);
1805
- const coldB = coldSet.has(b);
1806
- if (coldA && !coldB) return -1;
1807
- if (!coldA && coldB) return 1;
1808
- return a.localeCompare(b);
1875
+ for (const stage of stages) {
1876
+ const result = stage.length === 2 ? stage(a, b) : stage(b) - stage(a);
1877
+ if (result !== 0) return result;
1878
+ }
1879
+ return 0;
1809
1880
  });
1810
1881
  }
1811
1882
  makeFilter(patterns, callback) {
@@ -1922,13 +1993,13 @@ var HMRClient = class {
1922
1993
  }
1923
1994
  }
1924
1995
  const withOverrides = this.buildOverrideMap(toLoad);
1925
- const sorted2 = this.sortFiles(withOverrides);
1926
- this.logInitFileGroup(sorted2, this.overrideMap, this.isColdFile.bind(this));
1927
- for (const file of sorted2) {
1996
+ const sorted = this.sortFiles(withOverrides);
1997
+ this.logInitFileGroup(sorted, this.overrideMap, this.isColdFile.bind(this));
1998
+ for (const file of sorted) {
1928
1999
  await this.fileLoader.loadFile(file);
1929
2000
  if (this.onFileLoaded) this.onFileLoaded(file);
1930
2001
  }
1931
- this.log("success", `HMR client ready (${sorted2.length} files loaded)`);
2002
+ this.log("success", `HMR client ready (${sorted.length} files loaded)`);
1932
2003
  }
1933
2004
  async handleFileChange(file, action, serverCold = false) {
1934
2005
  if (this.shouldSkipFile(file, this.allFiles)) {
@@ -2032,7 +2103,7 @@ var HMRClient = class {
2032
2103
  await this.processInitFiles(data.files);
2033
2104
  } else {
2034
2105
  const modeLabel = this.watchFiles ? "HMR ready" : "Static snapshot ready";
2035
- this.log("success", `${modeLabel} (${sorted.length} files loaded)`);
2106
+ this.log("success", `${modeLabel} (0 files loaded)`);
2036
2107
  }
2037
2108
  return;
2038
2109
  }
@@ -2127,6 +2198,48 @@ var HMRClient = class {
2127
2198
  }
2128
2199
  this._processingMessages = false;
2129
2200
  }
2201
+ // Wait for stub's hmr:ready signal. Stub fires it proactively on run.
2202
+ // Times out after 5s if stub was never injected.
2203
+ _waitForStub() {
2204
+ return new Promise((resolve, reject) => {
2205
+ const timer = setTimeout(() => {
2206
+ window.removeEventListener("message", onReady);
2207
+ reject(new Error("Timed out waiting for hmr:ready. Was HMR.stub() called in the iframe?"));
2208
+ }, 5e3);
2209
+ const onReady = (e) => {
2210
+ if (e.data?.type !== "hmr:ready") return;
2211
+ const originOk = this._iframeOrigin === "*" || e.origin === this._iframeOrigin;
2212
+ if (!originOk) return;
2213
+ clearTimeout(timer);
2214
+ window.removeEventListener("message", onReady);
2215
+ if (this._stubManaged) {
2216
+ this._iframeTarget = e.source;
2217
+ this.fileLoader.iframeTarget = e.source;
2218
+ }
2219
+ resolve();
2220
+ };
2221
+ window.addEventListener("message", onReady);
2222
+ });
2223
+ }
2224
+ // Persistent listener for hmr:ready that handles iframe reattachment. If the
2225
+ // iframe reloads or is replaced, the stub fires hmr:ready again so we can
2226
+ // update the target and re-inject all currently loaded files.
2227
+ _listenForReattach() {
2228
+ if (this._onReattach) return;
2229
+ this._onReattach = async (e) => {
2230
+ if (e.data?.type !== "hmr:ready") return;
2231
+ const originOk = this._iframeOrigin === "*" || e.origin === this._iframeOrigin;
2232
+ if (!originOk) return;
2233
+ if (e.source === this._iframeTarget) return;
2234
+ this._iframeTarget = e.source;
2235
+ this.fileLoader.iframeTarget = e.source;
2236
+ this.log("success", "HMR reattached to new iframe");
2237
+ for (const path of this.fileLoader.versions.keys()) {
2238
+ await this.fileLoader.loadFile(path);
2239
+ }
2240
+ };
2241
+ window.addEventListener("message", this._onReattach);
2242
+ }
2130
2243
  /**
2131
2244
  * Connect to the HMR server
2132
2245
  * @returns {Promise<void>}
@@ -2149,14 +2262,27 @@ var HMRClient = class {
2149
2262
  let settled = false;
2150
2263
  try {
2151
2264
  this.socket = new WebSocket(this.wsUrl);
2152
- this.socket.onopen = () => {
2265
+ this.socket.onopen = async () => {
2153
2266
  settled = true;
2154
2267
  this.isConnected = true;
2155
2268
  this.reconnectAttempts = 0;
2156
- this._messageQueue = [];
2157
- this._processingMessages = false;
2158
2269
  this.log("success", "HMR connected");
2159
2270
  this.emit("connect");
2271
+ this._messageQueue = [];
2272
+ this._processingMessages = true;
2273
+ if (this._iframeTarget || this._stubManaged) {
2274
+ try {
2275
+ await this._waitForStub();
2276
+ if (this._stubManaged) this._listenForReattach();
2277
+ } catch (e) {
2278
+ this.log("error", e.message);
2279
+ this._processingMessages = false;
2280
+ reject(e);
2281
+ return;
2282
+ }
2283
+ }
2284
+ this._processingMessages = false;
2285
+ if (this._messageQueue.length > 0) this._drainMessageQueue();
2160
2286
  resolve();
2161
2287
  };
2162
2288
  this.socket.onclose = () => {
@@ -2212,15 +2338,57 @@ var HMRClient = class {
2212
2338
  this.isConnected = false;
2213
2339
  clearTimeout(this._reconnectTimer);
2214
2340
  this._reconnectTimer = null;
2341
+ if (this._onReattach) {
2342
+ window.removeEventListener("message", this._onReattach);
2343
+ this._onReattach = null;
2344
+ }
2215
2345
  if (this.socket) {
2216
2346
  this.socket.close();
2217
2347
  this.socket = null;
2218
2348
  }
2219
2349
  }
2220
2350
  };
2351
+
2352
+ // src/client/stub.js
2353
+ function stub() {
2354
+ const byFile = (tag, file) => document.querySelector(`${tag}[data-file="${CSS.escape(file)}"]`);
2355
+ const removeIfExists = (el) => {
2356
+ if (el) el.remove();
2357
+ };
2358
+ const injectScript = (kind, code, file) => {
2359
+ removeIfExists(byFile("script", file));
2360
+ const script = document.createElement("script");
2361
+ if (kind === "module") script.type = "module";
2362
+ script.textContent = code;
2363
+ script.dataset.file = file;
2364
+ document.documentElement.appendChild(script);
2365
+ script.remove();
2366
+ };
2367
+ const injectStyle = (code, file) => {
2368
+ const existing = byFile("style", file);
2369
+ const style = document.createElement("style");
2370
+ style.textContent = code;
2371
+ style.dataset.file = file;
2372
+ document.head.appendChild(style);
2373
+ removeIfExists(existing);
2374
+ };
2375
+ window.addEventListener("message", (e) => {
2376
+ const data = e.data;
2377
+ if (!data?.type) return;
2378
+ if (data.type === "hmr:remove") {
2379
+ removeIfExists(document.querySelector(`[data-file="${CSS.escape(data.file)}"]`));
2380
+ return;
2381
+ }
2382
+ if (data.type !== "hmr:inject") return;
2383
+ const { kind, code, file } = data;
2384
+ if (kind === "script" || kind === "module") injectScript(kind, code, file);
2385
+ else if (kind === "css") injectStyle(code, file);
2386
+ });
2387
+ window.parent.postMessage({ type: "hmr:ready" }, "*");
2388
+ }
2221
2389
  export {
2222
- FileLoader,
2223
2390
  HMRClient,
2224
- HMRClient as default
2391
+ HMRClient as default,
2392
+ stub
2225
2393
  };
2226
2394
  //# sourceMappingURL=client.js.map