html-bundle 6.3.1 → 6.4.0

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.
@@ -0,0 +1,446 @@
1
+ // html-bundle HMR client runtime.
2
+ //
3
+ // This is a browser TypeScript module. The DOM lib is enabled in tsconfig.json,
4
+ // so the main `tsc` build type-checks it alongside the Node code and compiles it
5
+ // to dist/hmr-client.js next to this module's output. At inject time
6
+ // `buildHMRClient()` in utils.mts substitutes the `__HMR_*` tokens, then the
7
+ // result is injected as an inline `<script type="module">` into every built HTML
8
+ // page and bundled by esbuild so the `hydro-js` import resolves. Keeping it as a
9
+ // real file (instead of a generated template string) means backslashes/backticks
10
+ // are literal, avoiding the template-literal escape corruption a generated string
11
+ // is prone to.
12
+ import { render as hydroRender, html, setShouldSetReactivity } from "hydro-js";
13
+ // hydro-js's `render` is typed against its own `html()` output; this client
14
+ // feeds it arbitrary DOM nodes and uses `false` for `where` (a detached render),
15
+ // so widen the signature. The assertion is type-only — esbuild strips it and the
16
+ // runtime binding stays hydro-js's `render`.
17
+ const render = hydroRender;
18
+ const FILE = "__HMR_FILE__";
19
+ const ID = "__HMR_ID__";
20
+ const SRC = "__HMR_SRC__";
21
+ const REGION = '[data-hmr="__HMR_ID__"]';
22
+ const CLIENT = 'script[data-hmr-client="__HMR_ID__"]';
23
+ window.isHMR = true;
24
+ // One hub per browsing context, created by whichever page loads first. Every
25
+ // composed sub-page (index.html + fetched fragments) registers into it and stays
26
+ // live, so a single shared EventSource dispatches every file's changes to the
27
+ // right region — instead of each page fighting over one connection.
28
+ const hub = window.__htmlBundleHMR || (window.__htmlBundleHMR = createHub(SRC));
29
+ hub.currentUnit = FILE;
30
+ if (!window.htmlBundleHMR) {
31
+ // Public opt-in API for user code running inside HMR-managed pages.
32
+ // window.htmlBundleHMR.dispose(cb) — cleanup before this unit re-runs
33
+ // window.htmlBundleHMR.accept(cb) — hook after this unit is patched
34
+ // window.htmlBundleHMR.data — object persisted across hot updates
35
+ window.htmlBundleHMR = {
36
+ accept(callback) {
37
+ hub.addAccept(hub.currentUnit, callback);
38
+ },
39
+ dispose(callback) {
40
+ hub.addDispose(hub.currentUnit, callback);
41
+ },
42
+ get data() {
43
+ return hub.dataFor(hub.currentUnit);
44
+ },
45
+ };
46
+ }
47
+ hub.register(FILE, ID, { patch });
48
+ // --- per-page patching (closes over this page's own hydro-js instance) --------
49
+ function patch(message) {
50
+ const previousScroll = window.scrollY;
51
+ if (isFullDocument(message.html)) {
52
+ patchDocument(message.previousHtml, message.html);
53
+ if (FILE === SRC + "/index.html") {
54
+ dispatchEvent(new Event("popstate"));
55
+ }
56
+ }
57
+ else {
58
+ patchFragment(message.html);
59
+ }
60
+ window.scrollTo(0, previousScroll);
61
+ }
62
+ function patchFragment(htmlText) {
63
+ const incoming = parseHTML(htmlText);
64
+ // hydro-js returns a DocumentFragment for multi-root markup but the element
65
+ // itself for a single root — normalise to a flat list of top-level nodes.
66
+ const nextNodes = incoming.nodeType === Node.DOCUMENT_FRAGMENT_NODE ||
67
+ incoming.nodeType === Node.DOCUMENT_NODE
68
+ ? Array.from(incoming.childNodes)
69
+ : [incoming];
70
+ const regions = Array.from(document.querySelectorAll(REGION));
71
+ // Replace each existing region in place, drop the surplus, append the rest.
72
+ regions.forEach((where, index) => {
73
+ if (index < nextNodes.length) {
74
+ render(nextNodes[index], where, false);
75
+ }
76
+ else {
77
+ where.remove();
78
+ }
79
+ });
80
+ for (let rest = regions.length; rest < nextNodes.length; rest++) {
81
+ if (regions.length) {
82
+ const template = document.createElement("template");
83
+ nextNodes[regions.length - 1].after(template);
84
+ render(nextNodes[rest], template, false);
85
+ template.remove();
86
+ }
87
+ else {
88
+ render(nextNodes[rest], false, false);
89
+ }
90
+ }
91
+ }
92
+ function isFullDocument(htmlText) {
93
+ const trimmed = htmlText.trimStart().toLowerCase();
94
+ return trimmed.startsWith("<!doctype html") || trimmed.startsWith("<html");
95
+ }
96
+ function parseHTML(htmlText) {
97
+ try {
98
+ return html `${htmlText}`;
99
+ }
100
+ catch {
101
+ setShouldSetReactivity(false);
102
+ const parsed = html `${htmlText}`;
103
+ setShouldSetReactivity(true);
104
+ return parsed;
105
+ }
106
+ }
107
+ function patchDocument(previousHtml, nextHtml) {
108
+ const previousText = previousHtml ||
109
+ hub.lastHTML.get(FILE) ||
110
+ document.documentElement.outerHTML;
111
+ const previousDocument = getDocumentParts(parseHTML(previousText));
112
+ const nextDocument = getDocumentParts(parseHTML(nextHtml));
113
+ if (!previousDocument.html || !nextDocument.html) {
114
+ render(parseHTML(nextHtml), document.documentElement, false);
115
+ hub.lastHTML.set(FILE, nextHtml);
116
+ return;
117
+ }
118
+ patchAttributes(document.documentElement, nextDocument.html);
119
+ if (previousDocument.head && nextDocument.head && document.head) {
120
+ patchChildren(previousDocument.head, nextDocument.head, document.head);
121
+ patchAttributes(document.head, nextDocument.head);
122
+ }
123
+ if (previousDocument.body && nextDocument.body && document.body) {
124
+ patchChildren(previousDocument.body, nextDocument.body, document.body);
125
+ patchAttributes(document.body, nextDocument.body);
126
+ }
127
+ hub.lastHTML.set(FILE, nextHtml);
128
+ }
129
+ function getDocumentParts(parsed) {
130
+ const htmlNode = (parsed instanceof HTMLHtmlElement
131
+ ? parsed
132
+ : parsed.querySelector?.("html") || parsed);
133
+ return {
134
+ html: htmlNode,
135
+ head: htmlNode.querySelector?.("head"),
136
+ body: htmlNode.querySelector?.("body"),
137
+ };
138
+ }
139
+ function patchChildren(previousParent, nextParent, liveParent) {
140
+ const previousNodes = comparableNodes(previousParent);
141
+ const nextNodes = comparableNodes(nextParent);
142
+ let previousIndex = 0;
143
+ let nextIndex = 0;
144
+ let liveIndex = 0;
145
+ while (nextIndex < nextNodes.length) {
146
+ const previousNode = previousNodes[previousIndex];
147
+ const nextNode = nextNodes[nextIndex];
148
+ const liveNode = nextLiveNode(liveParent, liveIndex);
149
+ if (!liveNode) {
150
+ liveParent.append(cloneForRender(nextNode));
151
+ nextIndex++;
152
+ liveIndex++;
153
+ continue;
154
+ }
155
+ if (!previousNode) {
156
+ render(cloneForRender(nextNode), liveNode, false);
157
+ nextIndex++;
158
+ liveIndex++;
159
+ continue;
160
+ }
161
+ const previousMatch = findStaticMatch(previousNodes, nextNode, previousIndex + 1);
162
+ const nextMatch = findStaticMatch(nextNodes, previousNode, nextIndex + 1);
163
+ if (previousMatch !== -1 && nextMatch === -1) {
164
+ liveNode.remove();
165
+ previousIndex++;
166
+ continue;
167
+ }
168
+ if (nextMatch !== -1) {
169
+ liveNode.before(cloneForRender(nextNode));
170
+ nextIndex++;
171
+ liveIndex++;
172
+ continue;
173
+ }
174
+ if (sameNodeIdentity(previousNode, nextNode)) {
175
+ patchNode(previousNode, nextNode, liveNode);
176
+ previousIndex++;
177
+ nextIndex++;
178
+ liveIndex++;
179
+ continue;
180
+ }
181
+ render(cloneForRender(nextNode), liveNode, false);
182
+ previousIndex++;
183
+ nextIndex++;
184
+ liveIndex++;
185
+ }
186
+ while (previousIndex < previousNodes.length &&
187
+ nextLiveNode(liveParent, liveIndex)) {
188
+ nextLiveNode(liveParent, liveIndex).remove();
189
+ previousIndex++;
190
+ }
191
+ }
192
+ function patchNode(previousNode, nextNode, liveNode) {
193
+ if (sameStaticNode(previousNode, nextNode))
194
+ return;
195
+ if (previousNode.nodeType !== nextNode.nodeType ||
196
+ liveNode.nodeType !== nextNode.nodeType) {
197
+ render(cloneForRender(nextNode), liveNode, false);
198
+ return;
199
+ }
200
+ if (nextNode.nodeType === Node.TEXT_NODE) {
201
+ liveNode.nodeValue = nextNode.nodeValue;
202
+ return;
203
+ }
204
+ if (nextNode.nodeType !== Node.ELEMENT_NODE) {
205
+ render(cloneForRender(nextNode), liveNode, false);
206
+ return;
207
+ }
208
+ if (nextNode.localName === "script") {
209
+ patchScript(previousNode, nextNode, liveNode);
210
+ return;
211
+ }
212
+ patchAttributes(liveNode, nextNode);
213
+ patchChildren(previousNode, nextNode, liveNode);
214
+ }
215
+ // Re-running only the scripts whose content actually changed is the "hot swap":
216
+ // unchanged scripts keep their state, changed ones re-execute with fresh code.
217
+ function patchScript(previousScript, nextScript, liveScript) {
218
+ if (previousScript.isEqualNode(nextScript))
219
+ return;
220
+ const clone = cloneScript(nextScript);
221
+ const source = clone.getAttribute("src");
222
+ if (source)
223
+ clone.setAttribute("src", bust(source));
224
+ render(clone, liveScript, false);
225
+ }
226
+ function cloneForRender(node) {
227
+ if (node.nodeType === Node.ELEMENT_NODE &&
228
+ node.localName === "script") {
229
+ return cloneScript(node);
230
+ }
231
+ return node.cloneNode(true);
232
+ }
233
+ // Cloning via document.createElement forces the browser to (re-)execute the
234
+ // script; a plain cloneNode of a parsed <script> would not run.
235
+ function cloneScript(script) {
236
+ const clone = document.createElement("script");
237
+ for (const attr of Array.from(script.attributes)) {
238
+ clone.setAttribute(attr.name, attr.value);
239
+ }
240
+ clone.textContent = script.textContent;
241
+ return clone;
242
+ }
243
+ function patchAttributes(liveElement, nextElement) {
244
+ for (const attr of Array.from(liveElement.attributes)) {
245
+ if (!nextElement.hasAttribute(attr.name))
246
+ liveElement.removeAttribute(attr.name);
247
+ }
248
+ for (const attr of Array.from(nextElement.attributes)) {
249
+ if (liveElement.getAttribute(attr.name) !== attr.value) {
250
+ liveElement.setAttribute(attr.name, attr.value);
251
+ }
252
+ }
253
+ }
254
+ function sameStaticNode(previousNode, nextNode) {
255
+ return cloneComparable(previousNode).isEqualNode(cloneComparable(nextNode));
256
+ }
257
+ function sameNodeIdentity(previousNode, nextNode) {
258
+ return (previousNode.nodeType === nextNode.nodeType &&
259
+ previousNode.nodeName === nextNode.nodeName);
260
+ }
261
+ function findStaticMatch(nodes, needle, start) {
262
+ for (let index = start; index < nodes.length; index++) {
263
+ if (sameStaticNode(nodes[index], needle))
264
+ return index;
265
+ }
266
+ return -1;
267
+ }
268
+ // Ignore the injected HMR client script when diffing so it never counts as a
269
+ // real change.
270
+ function cloneComparable(node) {
271
+ const clone = node.cloneNode(true);
272
+ clone.querySelectorAll?.(CLIENT).forEach((script) => script.remove());
273
+ if (clone.matches?.(CLIENT))
274
+ clone.remove();
275
+ return clone;
276
+ }
277
+ function comparableNodes(parent) {
278
+ return Array.from(parent.childNodes).filter((node) => {
279
+ return !(node.nodeType === Node.ELEMENT_NODE && node.matches?.(CLIENT));
280
+ });
281
+ }
282
+ function nextLiveNode(parent, index) {
283
+ return comparableNodes(parent)[index];
284
+ }
285
+ function bust(url) {
286
+ return url.split("?")[0] + "?v=" + Date.now();
287
+ }
288
+ // --- shared hub (created once by the first page to load) ----------------------
289
+ function createHub(src) {
290
+ const registry = new Map();
291
+ const accepts = new Map();
292
+ const disposers = new Map();
293
+ const store = new Map();
294
+ const throttle = new Map();
295
+ let source;
296
+ let reconnectTimer;
297
+ let reloadTimer;
298
+ let shouldReconnect = true;
299
+ const hub = {
300
+ currentUnit: null,
301
+ lastHTML: new Map(),
302
+ register(file, id, handler) {
303
+ registry.set(file, Object.assign({ id }, handler));
304
+ connect();
305
+ },
306
+ addAccept(file, callback) {
307
+ push(accepts, file, callback);
308
+ },
309
+ addDispose(file, callback) {
310
+ push(disposers, file, callback);
311
+ },
312
+ dataFor(file) {
313
+ if (!store.has(file))
314
+ store.set(file, {});
315
+ return store.get(file);
316
+ },
317
+ dispatch(message) {
318
+ if (message.type === "html") {
319
+ const entry = registry.get(message.file);
320
+ if (!entry)
321
+ return;
322
+ hub.currentUnit = message.file;
323
+ run(disposers, message.file);
324
+ try {
325
+ entry.patch(message);
326
+ }
327
+ catch (error) {
328
+ console.error("[html-bundle HMR] patch failed", error);
329
+ }
330
+ run(accepts, message.file);
331
+ }
332
+ else if (message.type === "css") {
333
+ once("css", bustStylesheets);
334
+ }
335
+ else if (message.type === "asset") {
336
+ once("asset:" + message.file, () => bustAsset(message.file));
337
+ }
338
+ else if (message.type === "full-reload") {
339
+ reloadPage();
340
+ }
341
+ },
342
+ };
343
+ function push(map, key, value) {
344
+ if (!map.has(key))
345
+ map.set(key, []);
346
+ map.get(key).push(value);
347
+ }
348
+ function run(map, key) {
349
+ const callbacks = map.get(key);
350
+ if (!callbacks || !callbacks.length)
351
+ return;
352
+ map.set(key, []);
353
+ for (const callback of callbacks) {
354
+ try {
355
+ callback();
356
+ }
357
+ catch (error) {
358
+ console.error("[html-bundle HMR] callback failed", error);
359
+ }
360
+ }
361
+ }
362
+ // Coalesce bursts of identical events (e.g. a save that touches many files).
363
+ function once(key, action) {
364
+ const now = performance.now();
365
+ if (!throttle.has(key) || now - throttle.get(key) > 100) {
366
+ throttle.set(key, now);
367
+ action();
368
+ }
369
+ }
370
+ function reloadPage() {
371
+ // Own timer (not reconnectTimer) so an SSE reconnect can't cancel a pending
372
+ // reload and vice versa.
373
+ clearTimeout(reloadTimer);
374
+ reloadTimer = setTimeout(() => window.location.reload(), 20);
375
+ }
376
+ function bustStylesheets() {
377
+ document
378
+ .querySelectorAll('link[rel="stylesheet"][href]')
379
+ .forEach((link) => {
380
+ const href = link.getAttribute("href").split("?")[0];
381
+ link.setAttribute("href", href + "?v=" + Date.now());
382
+ });
383
+ }
384
+ function bustAsset(file) {
385
+ if (file.endsWith(".css")) {
386
+ bustStylesheets();
387
+ return;
388
+ }
389
+ const prefix = src + "/";
390
+ const relative = file.indexOf(prefix) === 0 ? file.slice(prefix.length) : file;
391
+ const version = "?v=" + Date.now();
392
+ bustAttribute("img[src]", "src", relative, version);
393
+ bustAttribute("script[src]", "src", relative, version);
394
+ bustAttribute("link[href]", "href", relative, version);
395
+ bustContains("source[srcset]", "srcset", relative, version);
396
+ bustAttribute("img[data-src]", "data-src", relative, version);
397
+ }
398
+ function bustAttribute(selector, attribute, relative, version) {
399
+ document.querySelectorAll(selector).forEach((node) => {
400
+ const value = node.getAttribute(attribute);
401
+ if (value && value.split("?")[0] === relative) {
402
+ node.setAttribute(attribute, value.split("?")[0] + version);
403
+ }
404
+ });
405
+ }
406
+ function bustContains(selector, attribute, relative, version) {
407
+ document.querySelectorAll(selector).forEach((node) => {
408
+ const value = node.getAttribute(attribute);
409
+ if (value && value.split("?")[0].indexOf(relative) !== -1) {
410
+ node.setAttribute(attribute, value.split("?")[0] + version);
411
+ }
412
+ });
413
+ }
414
+ function connect() {
415
+ if (source)
416
+ return;
417
+ shouldReconnect = true;
418
+ source = new EventSource("/hmr");
419
+ source.addEventListener("message", (event) => {
420
+ let message;
421
+ try {
422
+ message = JSON.parse(event.data);
423
+ }
424
+ catch {
425
+ return;
426
+ }
427
+ hub.dispatch(message);
428
+ });
429
+ source.addEventListener("error", () => {
430
+ source.close();
431
+ source = undefined;
432
+ if (shouldReconnect) {
433
+ clearTimeout(reconnectTimer);
434
+ reconnectTimer = setTimeout(connect, 1000);
435
+ }
436
+ });
437
+ window.addEventListener("pagehide", () => {
438
+ shouldReconnect = false;
439
+ clearTimeout(reconnectTimer);
440
+ if (source)
441
+ source.close();
442
+ source = undefined;
443
+ }, { once: true });
444
+ }
445
+ return hub;
446
+ }
package/dist/utils.d.mts CHANGED
@@ -9,10 +9,22 @@ export declare const bundleConfig: Config;
9
9
  export declare function fileCopy(file: string): Promise<void>;
10
10
  export declare function createDir(file: string): Promise<string | undefined>;
11
11
  export declare function getBuildPath(file: string): string;
12
- export declare let serverSentEvents: undefined | (({ file, html }: {
12
+ export type HMREvent = {
13
+ type: "html";
13
14
  file: string;
14
15
  html?: string;
15
- }) => void);
16
+ previousHtml?: string;
17
+ } | {
18
+ type: "css";
19
+ file: string;
20
+ } | {
21
+ type: "asset";
22
+ file: string;
23
+ } | {
24
+ type: "full-reload";
25
+ file: string;
26
+ };
27
+ export declare let serverSentEvents: undefined | ((event: HMREvent) => void);
16
28
  export declare function createDefaultServer(isSecure: boolean): Promise<[Router, Server | HTTPSServer]>;
17
29
  export declare function getPostCSSConfig(): Promise<postcssrc.Result | {
18
30
  plugins: (typeof cssnano)[];