html-bundle 6.4.0 → 6.4.1

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.
@@ -20,7 +20,27 @@ const ID = "__HMR_ID__";
20
20
  const SRC = "__HMR_SRC__";
21
21
  const REGION = '[data-hmr="__HMR_ID__"]';
22
22
  const CLIENT = 'script[data-hmr-client="__HMR_ID__"]';
23
- window.isHMR = true;
23
+ // The client is injected as the first <head> module so its hub and public API
24
+ // exist before the page's own scripts run. But `window.isHMR` must NOT be
25
+ // observable while those scripts execute for the first time: user code commonly
26
+ // branches on it to run one-time setup exactly once (e.g.
27
+ // `if (!window.isHMR) createRouter()`), relying on the flag being false on the
28
+ // pristine load and true on every hot re-run afterwards. Assigning it eagerly
29
+ // here runs before the page scripts and breaks that contract, leaving one-time
30
+ // init skipped (e.g. an SPA router never mounts, so its outlet stays empty).
31
+ // Flag it only after the initial module scripts have executed:
32
+ // - initial load: readyState is "interactive"; set it on DOMContentLoaded,
33
+ // which fires after the page's own deferred/module scripts.
34
+ // - hot re-render / composed page registered post-load: readyState is
35
+ // "complete"; set it immediately, since we are already past the first load.
36
+ if (document.readyState === "complete") {
37
+ window.isHMR = true;
38
+ }
39
+ else {
40
+ document.addEventListener("DOMContentLoaded", () => (window.isHMR = true), {
41
+ once: true,
42
+ });
43
+ }
24
44
  // One hub per browsing context, created by whichever page loads first. Every
25
45
  // composed sub-page (index.html + fetched fragments) registers into it and stays
26
46
  // live, so a single shared EventSource dispatches every file's changes to the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "html-bundle",
3
- "version": "6.4.0",
3
+ "version": "6.4.1",
4
4
  "description": "A very simple bundler for HTML SFC",
5
5
  "bin": "./dist/bundle.mjs",
6
6
  "main": "./dist/bundle.mjs",
package/src/hmr-client.ts CHANGED
@@ -72,7 +72,26 @@ const SRC = "__HMR_SRC__";
72
72
  const REGION = '[data-hmr="__HMR_ID__"]';
73
73
  const CLIENT = 'script[data-hmr-client="__HMR_ID__"]';
74
74
 
75
- window.isHMR = true;
75
+ // The client is injected as the first <head> module so its hub and public API
76
+ // exist before the page's own scripts run. But `window.isHMR` must NOT be
77
+ // observable while those scripts execute for the first time: user code commonly
78
+ // branches on it to run one-time setup exactly once (e.g.
79
+ // `if (!window.isHMR) createRouter()`), relying on the flag being false on the
80
+ // pristine load and true on every hot re-run afterwards. Assigning it eagerly
81
+ // here runs before the page scripts and breaks that contract, leaving one-time
82
+ // init skipped (e.g. an SPA router never mounts, so its outlet stays empty).
83
+ // Flag it only after the initial module scripts have executed:
84
+ // - initial load: readyState is "interactive"; set it on DOMContentLoaded,
85
+ // which fires after the page's own deferred/module scripts.
86
+ // - hot re-render / composed page registered post-load: readyState is
87
+ // "complete"; set it immediately, since we are already past the first load.
88
+ if (document.readyState === "complete") {
89
+ window.isHMR = true;
90
+ } else {
91
+ document.addEventListener("DOMContentLoaded", () => (window.isHMR = true), {
92
+ once: true,
93
+ });
94
+ }
76
95
 
77
96
  // One hub per browsing context, created by whichever page loads first. Every
78
97
  // composed sub-page (index.html + fetched fragments) registers into it and stays
@@ -93,6 +93,32 @@ test("shared hub patches a full-document title change in place", async () => {
93
93
  assert.equal(document.querySelector("title").textContent, "After");
94
94
  });
95
95
 
96
+ test("window.isHMR is hidden from the page's own initial scripts, then set for hot re-runs", async () => {
97
+ // Regression: the client is injected as the first <head> module so its hub and
98
+ // public API exist before the page's own scripts. It must NOT flip
99
+ // window.isHMR before those scripts run, or one-time guards such as
100
+ // `if (!window.isHMR) createRouter()` get skipped on the pristine load — the
101
+ // symptom being an SPA whose outlet never mounts (an almost-empty page).
102
+ const { window, document, loadPage } = await setup();
103
+ assert.equal(document.readyState, "interactive"); // pristine load, still parsing
104
+
105
+ loadPage("src/index.html", "idx1");
106
+ assert.notEqual(
107
+ window.isHMR,
108
+ true,
109
+ "isHMR must stay falsy while the page's own scripts run",
110
+ );
111
+
112
+ // DOMContentLoaded fires after those scripts; HMR mode is now observable so a
113
+ // subsequent hot re-execution can take the isHMR branch and skip re-init.
114
+ document.dispatchEvent(new window.Event("DOMContentLoaded"));
115
+ assert.equal(
116
+ window.isHMR,
117
+ true,
118
+ "isHMR is set once the initial load settles",
119
+ );
120
+ });
121
+
96
122
  test("full-document insertion before a composed mount preserves fetched content", async () => {
97
123
  const { document, loadPage, reloadCount } = await setup();
98
124
  const before =