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.
- package/.github/dependabot.yml +5 -5
- package/README.md +34 -3
- package/dist/bundle.mjs +69 -18
- package/dist/hmr-client.d.ts +40 -0
- package/dist/hmr-client.js +446 -0
- package/dist/utils.d.mts +14 -2
- package/dist/utils.mjs +45 -178
- package/package.json +3 -1
- package/src/bundle.mts +67 -18
- package/src/hmr-client.ts +577 -0
- package/src/utils.mts +61 -186
- package/tests/bundle.test.mjs +62 -10
- package/tests/hmr-client.test.mjs +245 -0
- package/tests/hmr-server.test.mjs +175 -0
- package/tsconfig.json +1 -1
package/.github/dependabot.yml
CHANGED
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ A (primarily) zero-config bundler for HTML files. The idea is to use HTML as Sin
|
|
|
10
10
|
|
|
11
11
|
- 🦾 TypeScript (reference it as .js or write inline TS)
|
|
12
12
|
- 📦 Automatic Package Installation
|
|
13
|
-
- 💨 HMR
|
|
13
|
+
- 💨 State-preserving HMR with automatic reconnect and full-reload fallback
|
|
14
14
|
- ⚡ [ESBuild](https://github.com/evanw/esbuild)
|
|
15
15
|
- 🦔 [Critical CSS](https://www.npmjs.com/package/beasties)
|
|
16
16
|
- 🚋 Watcher on PostCSS and Tailwind CSS and TS Config
|
|
@@ -45,13 +45,45 @@ $ npm run build
|
|
|
45
45
|
|
|
46
46
|
## CLI
|
|
47
47
|
|
|
48
|
-
`--hmr`: boots up a static server and enables Hot Module Replacement.
|
|
48
|
+
`--hmr`: boots up a static server and enables Hot Module Replacement. See [HMR](#hmr) for what is hot-patched in place versus reloaded.<br>
|
|
49
49
|
`--secure`: creates a secure HTTP2 over HTTPS instance. This requires the files `localhost.pem` and `localhost-key.pem` in the root folder. You can generate them with [mkcert](https://github.com/FiloSottile/mkcert) for instance.<br>
|
|
50
50
|
`--isCritical`: uses critical to extract and inline critical-path CSS to HTML.<br>
|
|
51
51
|
`--handler`: path to your custom handler. Here, you can handle all non-supported files. You can get the filename via `process.argv[2]`.
|
|
52
52
|
|
|
53
|
+
## HMR
|
|
54
|
+
|
|
55
|
+
With `--hmr`, every change is pushed over a single Server-Sent-Events connection and applied without a full reload wherever that is safe:
|
|
56
|
+
|
|
57
|
+
- **HTML** (text, attributes, inline `<style>`) is diffed and patched in place. Only the `<script>`s whose code actually changed are re-executed, so unrelated state is preserved. Inserting or removing surrounding markup keeps dynamically rendered/fetched content intact.
|
|
58
|
+
- **CSS** (linked or inline) and **assets** (images, etc.) are swapped by cache-busting the matching element — no reload.
|
|
59
|
+
- **TS/JS modules** are re-bundled; the page(s) that inline them are re-emitted and hot-patched.
|
|
60
|
+
- **Dead ends** — a change with no owning page (e.g. a web-worker entry) or a deleted file — trigger a single debounced **full page reload**, so you never get stuck on a stale view.
|
|
61
|
+
|
|
62
|
+
Composed apps (pages that fetch and render other pages) are supported: every live page shares one connection and only its own region is patched. After HMR operations the `popstate` event is dispatched to nudge SPA routers.
|
|
63
|
+
|
|
64
|
+
### Client API (optional)
|
|
65
|
+
|
|
66
|
+
Inside any HMR-managed page you can opt into lifecycle hooks — useful for side-effectful top-level code so a hot update does not duplicate its effects:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
if (window.htmlBundleHMR) {
|
|
70
|
+
// run cleanup before this unit's changed scripts re-run
|
|
71
|
+
window.htmlBundleHMR.dispose(() => {
|
|
72
|
+
/* e.g. remove nodes this script appended */
|
|
73
|
+
});
|
|
74
|
+
// run after this page has been patched
|
|
75
|
+
window.htmlBundleHMR.accept(() => {
|
|
76
|
+
/* re-init if needed */
|
|
77
|
+
});
|
|
78
|
+
// a plain object that persists across hot updates
|
|
79
|
+
window.htmlBundleHMR.data.rendered = true;
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
53
83
|
## import
|
|
84
|
+
|
|
54
85
|
It is also possible to start the server by importing the package. This could be useful, if you intend to add routes for local development.
|
|
86
|
+
|
|
55
87
|
```js
|
|
56
88
|
import router from "html-bundle";
|
|
57
89
|
|
|
@@ -60,7 +92,6 @@ router.get("/test", (_req, reply) => {
|
|
|
60
92
|
});
|
|
61
93
|
```
|
|
62
94
|
|
|
63
|
-
|
|
64
95
|
## Optional Config
|
|
65
96
|
|
|
66
97
|
_The CLI flags can also be set by the config. Flags set by the CLI will override the config._
|
package/dist/bundle.mjs
CHANGED
|
@@ -92,6 +92,13 @@ async function build(files, firstRun = true) {
|
|
|
92
92
|
console.log(`💻 Server listening on http${isSecure ? "s" : ""}://${bundleConfig.host === "::" ? "localhost" : bundleConfig.host}:${bundleConfig.port} and is shared in the local network.`);
|
|
93
93
|
console.log(`⌛ Waiting for file changes ...`);
|
|
94
94
|
const chokidarOptions = { awaitWriteFinish: false };
|
|
95
|
+
let rebuildQueue = Promise.resolve();
|
|
96
|
+
const enqueueRebuild = (file) => {
|
|
97
|
+
rebuildQueue = rebuildQueue
|
|
98
|
+
.catch(() => undefined)
|
|
99
|
+
.then(() => rebuild(file));
|
|
100
|
+
return rebuildQueue;
|
|
101
|
+
};
|
|
95
102
|
if (postcssFile) {
|
|
96
103
|
const postCSSWatcher = watch(postcssFile, chokidarOptions);
|
|
97
104
|
const tailwindCSSWatcher = watch(postcssFile.replace("postcss", "tailwind"), chokidarOptions); // Assuming that the file ext is the same
|
|
@@ -111,7 +118,8 @@ async function build(files, firstRun = true) {
|
|
|
111
118
|
return;
|
|
112
119
|
}
|
|
113
120
|
try {
|
|
114
|
-
|
|
121
|
+
files.push(file);
|
|
122
|
+
await enqueueRebuild(file);
|
|
115
123
|
}
|
|
116
124
|
catch { }
|
|
117
125
|
console.log(`⚡ added ${file} to the build`);
|
|
@@ -121,7 +129,7 @@ async function build(files, firstRun = true) {
|
|
|
121
129
|
return;
|
|
122
130
|
}
|
|
123
131
|
file = String.raw `${file}`.replace(/\\/g, "/");
|
|
124
|
-
await
|
|
132
|
+
await enqueueRebuild(file);
|
|
125
133
|
console.log(`⚡ modified ${file} on the build`);
|
|
126
134
|
});
|
|
127
135
|
watcher.on("unlink", async (file) => {
|
|
@@ -129,6 +137,9 @@ async function build(files, firstRun = true) {
|
|
|
129
137
|
return;
|
|
130
138
|
}
|
|
131
139
|
file = String.raw `${file}`.replace(/\\/g, "/");
|
|
140
|
+
const fileIndex = files.indexOf(file);
|
|
141
|
+
if (fileIndex !== -1)
|
|
142
|
+
files.splice(fileIndex, 1);
|
|
132
143
|
inlineFiles.delete(file);
|
|
133
144
|
const buildFile = getBuildPath(file)
|
|
134
145
|
.replace(".ts", ".js")
|
|
@@ -141,46 +152,84 @@ async function build(files, firstRun = true) {
|
|
|
141
152
|
await rm(bfDir);
|
|
142
153
|
}
|
|
143
154
|
catch { }
|
|
155
|
+
serverSentEvents?.({ type: "full-reload", file });
|
|
144
156
|
console.log(`⚡ deleted ${file} from the build`);
|
|
145
157
|
});
|
|
146
158
|
async function rebuild(file) {
|
|
147
159
|
// Rebuild all CSS because a change in any file might need to trigger PostCSS zu rebuild(e.g. Tailwind CSS)
|
|
148
160
|
await rebuildCSS(files.filter((file) => file.endsWith(".css")));
|
|
149
|
-
|
|
161
|
+
const htmlFiles = files.filter((f) => f.endsWith(".html"));
|
|
150
162
|
if (file.endsWith(".html")) {
|
|
163
|
+
const previousHtml = builtHTMLCache.get(file);
|
|
151
164
|
// To refill the inlineFiles needed to build JS
|
|
152
|
-
for (const htmlFile of
|
|
165
|
+
for (const htmlFile of htmlFiles) {
|
|
153
166
|
await writeInlineScripts(htmlFile);
|
|
154
167
|
}
|
|
155
168
|
await minifyCode();
|
|
156
|
-
for (const
|
|
157
|
-
if (INLINE_BUNDLE_FILE.test(
|
|
158
|
-
inlineFiles.delete(
|
|
159
|
-
await rm(
|
|
169
|
+
for (const inline of inlineFiles) {
|
|
170
|
+
if (INLINE_BUNDLE_FILE.test(inline)) {
|
|
171
|
+
inlineFiles.delete(inline);
|
|
172
|
+
await rm(inline);
|
|
160
173
|
}
|
|
161
174
|
}
|
|
162
|
-
html = await minifyHTML(file, getBuildPath(file));
|
|
175
|
+
const html = await minifyHTML(file, getBuildPath(file));
|
|
176
|
+
serverSentEvents?.({ type: "html", file, html, previousHtml });
|
|
163
177
|
}
|
|
164
178
|
else if (/\.(jsx?|tsx?)$/.test(file)) {
|
|
179
|
+
// A module change alters the inlined output of whichever page(s) import
|
|
180
|
+
// it. Rebuild every page, then emit only the pages whose HTML actually
|
|
181
|
+
// changed; the client diff re-runs just the scripts that differ, so
|
|
182
|
+
// unrelated pages keep their state.
|
|
165
183
|
inlineFiles.add(file);
|
|
184
|
+
for (const htmlFile of htmlFiles) {
|
|
185
|
+
await writeInlineScripts(htmlFile);
|
|
186
|
+
}
|
|
166
187
|
await minifyCode();
|
|
188
|
+
for (const inline of inlineFiles) {
|
|
189
|
+
if (INLINE_BUNDLE_FILE.test(inline)) {
|
|
190
|
+
inlineFiles.delete(inline);
|
|
191
|
+
await rm(inline);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
let didEmit = false;
|
|
195
|
+
for (const htmlFile of htmlFiles) {
|
|
196
|
+
const previousHtml = builtHTMLCache.get(htmlFile);
|
|
197
|
+
const html = await minifyHTML(htmlFile, getBuildPath(htmlFile));
|
|
198
|
+
if (html !== previousHtml) {
|
|
199
|
+
didEmit = true;
|
|
200
|
+
serverSentEvents?.({
|
|
201
|
+
type: "html",
|
|
202
|
+
file: htmlFile,
|
|
203
|
+
html,
|
|
204
|
+
previousHtml,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!didEmit) {
|
|
209
|
+
serverSentEvents?.({ type: "full-reload", file });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else if (file.endsWith(".css")) {
|
|
213
|
+
serverSentEvents?.({ type: "css", file });
|
|
167
214
|
}
|
|
168
|
-
else
|
|
215
|
+
else {
|
|
169
216
|
if (handlerFile) {
|
|
170
|
-
|
|
217
|
+
try {
|
|
218
|
+
const { stdout } = await execFilePromise("node", [
|
|
219
|
+
handlerFile,
|
|
220
|
+
file,
|
|
221
|
+
]);
|
|
171
222
|
console.log("📋 Logging Handler: ", String(stdout));
|
|
172
|
-
}
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
console.error(err);
|
|
226
|
+
}
|
|
173
227
|
}
|
|
174
228
|
else {
|
|
175
229
|
await fileCopy(file);
|
|
176
230
|
}
|
|
231
|
+
serverSentEvents?.({ type: "asset", file });
|
|
177
232
|
}
|
|
178
|
-
else if (handlerFile) {
|
|
179
|
-
execFilePromise("node", [handlerFile, file]).then(({ stdout }) => {
|
|
180
|
-
console.log("📋 Logging Handler: ", String(stdout));
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
serverSentEvents?.({ file, html });
|
|
184
233
|
}
|
|
185
234
|
}
|
|
186
235
|
}
|
|
@@ -245,6 +294,7 @@ async function minifyCode() {
|
|
|
245
294
|
}
|
|
246
295
|
}
|
|
247
296
|
const htmlFilesCache = new Map();
|
|
297
|
+
const builtHTMLCache = new Map();
|
|
248
298
|
async function writeInlineScripts(file) {
|
|
249
299
|
let fileText = await readFile(file, { encoding: "utf-8" });
|
|
250
300
|
let DOM;
|
|
@@ -359,6 +409,7 @@ async function minifyHTML(file, buildFile) {
|
|
|
359
409
|
}
|
|
360
410
|
}
|
|
361
411
|
await writeFile(buildFile, fileText);
|
|
412
|
+
builtHTMLCache.set(file, fileText);
|
|
362
413
|
return fileText;
|
|
363
414
|
}
|
|
364
415
|
async function rebuildCSS(files, config) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
type HTMLMessage = {
|
|
2
|
+
type: "html";
|
|
3
|
+
file: string;
|
|
4
|
+
html: string;
|
|
5
|
+
previousHtml?: string;
|
|
6
|
+
};
|
|
7
|
+
type HMRMessage = HTMLMessage | {
|
|
8
|
+
type: "css";
|
|
9
|
+
file: string;
|
|
10
|
+
} | {
|
|
11
|
+
type: "asset";
|
|
12
|
+
file: string;
|
|
13
|
+
} | {
|
|
14
|
+
type: "full-reload";
|
|
15
|
+
file: string;
|
|
16
|
+
};
|
|
17
|
+
type Hub = {
|
|
18
|
+
currentUnit: string | null;
|
|
19
|
+
lastHTML: Map<string, string>;
|
|
20
|
+
register(file: string, id: string, handler: {
|
|
21
|
+
patch: (message: HTMLMessage) => void;
|
|
22
|
+
}): void;
|
|
23
|
+
addAccept(file: string, callback: () => void): void;
|
|
24
|
+
addDispose(file: string, callback: () => void): void;
|
|
25
|
+
dataFor(file: string): Record<string, unknown>;
|
|
26
|
+
dispatch(message: HMRMessage): void;
|
|
27
|
+
};
|
|
28
|
+
type HMRPublicAPI = {
|
|
29
|
+
accept(callback: () => void): void;
|
|
30
|
+
dispose(callback: () => void): void;
|
|
31
|
+
readonly data: Record<string, unknown>;
|
|
32
|
+
};
|
|
33
|
+
declare global {
|
|
34
|
+
interface Window {
|
|
35
|
+
isHMR?: boolean;
|
|
36
|
+
__htmlBundleHMR?: Hub;
|
|
37
|
+
htmlBundleHMR?: HMRPublicAPI;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export {};
|