html-bundle 6.0.0-beta → 6.0.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/README.md CHANGED
@@ -1,19 +1,21 @@
1
1
  # html-bundle
2
2
 
3
+ <p align="center">
4
+ <img src="./logo.jpg" style="width:500px;" />
5
+ </p>
6
+
3
7
  A (primarily) zero-config bundler for HTML files. The idea is to use HTML as Single File Components, because HTML can already include `<style>` and `<script>` elements.
4
8
 
5
9
  ## Features
6
10
 
7
11
  - 🦾 TypeScript (reference it as .js or write inline TS)
8
12
  - 📦 Automatic Package Installation
9
- - 💨 HMR
13
+ - 💨 HMR and automatic reconnect
10
14
  - ⚡ [ESBuild](https://github.com/evanw/esbuild)
11
15
  - 🦔 [Critical CSS](https://github.com/evanw/esbuild)
12
- - 💅 PostCSS and Tailwind CSS Support
16
+ - 🚋 Watcher on PostCSS and Tailwind CSS and TS Config
13
17
  - 🛡️ Almost no need to restart
14
18
 
15
- ![Demo](./example.gif)
16
-
17
19
  ## Installation and Usage
18
20
 
19
21
  ```properties
@@ -39,7 +41,7 @@ $ npm run build
39
41
 
40
42
  ## CLI
41
43
 
42
- `--hmr`: boots up a static server and enables Hot Module Replacement. **This generates a development build.**<br>
44
+ `--hmr`: boots up a static server and enables Hot Module Replacement. **This generates a development build and works best when not triggered from the main index.html**<br>
43
45
  `--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>
44
46
  `--critical`: uses critical to extract and inline critical-path CSS to HTML.<br>
45
47
  `--handler`: path to your custom handler. Here, you can handle all non-supported files. You can get the filename via `process.argv[2]`.
@@ -52,6 +54,7 @@ Generate the config in the root and call it "bundle.config.js"
52
54
  **src:** input path. Default to "src"<br>
53
55
  **build:** output path. Defaults to "build"<br>
54
56
  **port:** For the HMR Server. Defaults to 5000<br>
57
+ **deletePrev:** Whether to delelte the build folder. Defaults to true<br>
55
58
  **esbuild:** Your additional config<br>
56
59
  **html-minifier-terser:** Your additional config<br>
57
60
  **critical:** Your additional config<br>
@@ -76,13 +79,14 @@ Set `"jsxFactory": "h"` in `tsconfig.json` for JSX.
76
79
  <title>Example</title>
77
80
  <meta name="Description" content="Example for html-bundle" />
78
81
  <script type="module">
79
- import { render, h } from "hydro-js";
82
+ import { render, h, reactive } from "hydro-js";
80
83
 
81
- function Example() {
82
- return <main id="app">Testing html-bundle</main>;
84
+ function Example({ name }) {
85
+ return <main id="app">Hi {name}</main>;
83
86
  }
84
87
 
85
- render(<Example />, "#app");
88
+ const name = reactive("Tester");
89
+ render(<Example name={name} />, "#app");
86
90
  </script>
87
91
  <style>
88
92
  body {
@@ -100,7 +104,7 @@ Set `"jsxFactory": "h"` in `tsconfig.json` for JSX.
100
104
 
101
105
  ![Output](output.JPG)
102
106
 
103
- ## Example Vue.js
107
+ ## Example Vue.js@next
104
108
 
105
109
  Set `"jsxFactory": "h"` in `tsconfig.json`.
106
110
 
@@ -136,7 +140,7 @@ Set `"jsxFactory": "h"` in `tsconfig.json`.
136
140
 
137
141
  ## Example React
138
142
 
139
- Set `"jsxFactory": "React.createElement"` in `tsconfig.json`.
143
+ Set `"jsxFactory": "h"` in `tsconfig.json`.
140
144
 
141
145
  ```html
142
146
  <!DOCTYPE html>
@@ -149,6 +153,7 @@ Set `"jsxFactory": "React.createElement"` in `tsconfig.json`.
149
153
  <script type="module">
150
154
  import React, { useState } from "react";
151
155
  import { render } from "react-dom";
156
+ const h = React.createElement;
152
157
 
153
158
  function Example() {
154
159
  const [count, setCount] = useState(0);
@@ -161,10 +166,14 @@ Set `"jsxFactory": "React.createElement"` in `tsconfig.json`.
161
166
  );
162
167
  }
163
168
 
164
- render(<Example />, document.getElementById("root"));
169
+ render(<Example />, document.getElementById("app"));
165
170
  </script>
166
171
  <body>
167
- <div id="root"></div>
172
+ <div id="app"></div>
168
173
  </body>
169
174
  </html>
170
175
  ```
176
+
177
+ ### Demo
178
+
179
+ ![Demo](./example.gif)
package/dist/bundle.mjs CHANGED
@@ -12,7 +12,7 @@ import { watch } from "chokidar";
12
12
  import { serialize, parse, parseFragment } from "parse5";
13
13
  import { getTagName, findElements } from "@web/parse5-utils";
14
14
  import awaitSpawn from "await-spawn";
15
- import { fileCopy, createDefaultServer, getPostCSSConfig, getBuildPath, createDir, bundleConfig, serverSentEvents, addHMRCode, } from "./utils.js";
15
+ import { fileCopy, createDefaultServer, getPostCSSConfig, getBuildPath, createDir, bundleConfig, serverSentEvents, addHMRCode, } from "./utils.mjs";
16
16
  const isHMR = process.argv.includes("--hmr") || bundleConfig.hmr;
17
17
  const isCritical = process.argv.includes("--critical") || bundleConfig.critical;
18
18
  const isSecure = process.argv.includes("--secure") || bundleConfig.secure; // uses CSP for critical too
@@ -20,23 +20,25 @@ const handlerFile = process.argv.includes("--handler")
20
20
  ? process.argv[process.argv.indexOf("--handler") + 1]
21
21
  : bundleConfig.handler;
22
22
  process.env.NODE_ENV = isHMR ? "development" : "production"; // just in case other tools are using it
23
- const timer = performance.now();
23
+ let timer = performance.now();
24
24
  let { plugins, options, file: postcssFile } = await getPostCSSConfig();
25
25
  let CSSprocessor = postcss(plugins);
26
26
  let fastify;
27
27
  const inlineFiles = new Set();
28
28
  const TEMPLATE_LITERAL_MINIFIER = /\n\s+/g;
29
29
  const INLINE_BUNDLE_FILE = /-bundle-\d+.tsx$/;
30
- const SUPPORTED_FILES = /\.(html|css|m?jsx?|m?tsx?)$/;
30
+ const SUPPORTED_FILES = /\.(html|css|jsx?|tsx?)$/;
31
31
  const execFilePromise = promisify(execFile);
32
- await rm(bundleConfig.build, { force: true, recursive: true });
32
+ if (bundleConfig.deletePrev) {
33
+ await rm(bundleConfig.build, { force: true, recursive: true });
34
+ }
33
35
  glob(`${bundleConfig.src}/**/*.*`, build);
34
- async function build(err, files) {
36
+ async function build(err, files, firstRun = true) {
35
37
  if (err) {
36
38
  console.error(err);
37
39
  process.exit(1);
38
40
  }
39
- if (isHMR) {
41
+ if (isHMR && firstRun) {
40
42
  fastify = await createDefaultServer(isSecure);
41
43
  fastify.listen(bundleConfig.port);
42
44
  console.log(`💻 Sever listening on port ${bundleConfig.port}.`);
@@ -46,7 +48,8 @@ async function build(err, files) {
46
48
  if (!SUPPORTED_FILES.test(file)) {
47
49
  if (handlerFile) {
48
50
  const { stdout } = await execFilePromise("node", [handlerFile, file]);
49
- console.log("📋 Logging Handler: ", String(stdout));
51
+ if (String(stdout))
52
+ console.log("📋 Logging Handler: ", String(stdout));
50
53
  }
51
54
  else {
52
55
  await fileCopy(file);
@@ -77,14 +80,19 @@ async function build(err, files) {
77
80
  }
78
81
  }
79
82
  console.log(`🚀 Build finished in ${(performance.now() - timer).toFixed(2)}ms ✨`);
80
- if (isHMR) {
83
+ if (isHMR && firstRun) {
81
84
  console.log(`⌛ Waiting for file changes ...`);
82
85
  if (postcssFile) {
83
86
  const postCSSWatcher = watch(postcssFile);
84
87
  const tailwindCSSWatcher = watch(postcssFile.replace("postcss", "tailwind")); // Assuming that the file ext is the same
88
+ const tsConfigWatcher = watch(postcssFile.split("\\").slice(0, -1).join("\\") + "\\tsconfig.json");
85
89
  const cssFiles = files.filter((file) => file.endsWith(".css"));
86
90
  postCSSWatcher.on("change", async () => await rebuildCSS(cssFiles, "postcss"));
87
91
  tailwindCSSWatcher.on("change", async () => await rebuildCSS(cssFiles, "tailwind"));
92
+ tsConfigWatcher.on("change", async () => {
93
+ timer = performance.now();
94
+ await build(null, files, false);
95
+ });
88
96
  }
89
97
  const watcher = watch(bundleConfig.src);
90
98
  let addCount = 0; // The add watcher will add all the files initially - do not rebuild them
@@ -126,7 +134,7 @@ async function build(err, files) {
126
134
  let html;
127
135
  if (file.endsWith(".html")) {
128
136
  // To refill the inlineFiles needed to build JS
129
- for (const htmlFile of files.filter((file) => file.endsWith("html"))) {
137
+ for (const htmlFile of files.filter((file) => file.endsWith(".html"))) {
130
138
  await writeInlineScripts(htmlFile);
131
139
  }
132
140
  await minifyCode();
@@ -138,14 +146,16 @@ async function build(err, files) {
138
146
  }
139
147
  html = await minifyHTML(file, getBuildPath(file));
140
148
  }
141
- else if (/(m?jsx?|m?tsx?)$/.test(file)) {
149
+ else if (/\.(jsx?|tsx?)$/.test(file)) {
150
+ inlineFiles.add(file);
142
151
  await minifyCode();
143
152
  }
144
153
  else {
145
154
  const { stdout } = await execFilePromise("node", [handlerFile, file]);
146
- console.log("📋 Logging Handler: ", String(stdout));
155
+ if (String(stdout))
156
+ console.log("📋 Logging Handler: ", String(stdout));
147
157
  }
148
- serverSentEvents({ file, html });
158
+ serverSentEvents?.({ file, html });
149
159
  }
150
160
  }
151
161
  }
@@ -173,7 +183,7 @@ async function minifyCode() {
173
183
  sourcemap: isHMR,
174
184
  splitting: true,
175
185
  define: {
176
- "process.env.NODE_ENV": process.env.NODE_ENV,
186
+ "process.env.NODE_ENV": `"${process.env.NODE_ENV}"`,
177
187
  },
178
188
  loader: { ".js": "jsx", ".ts": "tsx" },
179
189
  bundle: true,
@@ -283,11 +293,16 @@ async function minifyHTML(file, buildFile) {
283
293
  }
284
294
  fileText = serialize(DOM);
285
295
  // Minify HTML
286
- fileText = await minify(fileText, {
287
- collapseWhitespace: true,
288
- removeComments: true,
289
- ...bundleConfig["html-minifier-terser"],
290
- });
296
+ try {
297
+ fileText = await minify(fileText, {
298
+ collapseWhitespace: true,
299
+ removeComments: true,
300
+ ...bundleConfig["html-minifier-terser"],
301
+ });
302
+ }
303
+ catch (e) {
304
+ console.error(e);
305
+ }
291
306
  if (!isCritical) {
292
307
  await writeFile(buildFile, fileText);
293
308
  return fileText;
@@ -1,4 +1,4 @@
1
- import { access, copyFile, mkdir, readFile } from "fs/promises";
1
+ import { copyFile, mkdir, readFile } from "fs/promises";
2
2
  import path from "path";
3
3
  import Fastify from "fastify";
4
4
  import fastifyStatic from "fastify-static";
@@ -40,20 +40,25 @@ export async function createDefaultServer(isSecure) {
40
40
  fastify.register(fastifyStatic, {
41
41
  root: path.join(process.cwd(), bundleConfig.build),
42
42
  });
43
- fastify.get("/events", (_req, reply) => {
43
+ fastify.get("/hmr", (_req, reply) => {
44
44
  reply.raw.setHeader("Content-Type", "text/event-stream");
45
45
  reply.raw.setHeader("Cache-Control", "no-cache");
46
46
  !isSecure && reply.raw.setHeader("Connection", "keep-alive");
47
47
  CONNECTIONS.push(reply.raw);
48
- serverSentEvents = (data) => CONNECTIONS.forEach((rep) => {
49
- rep.write(`data: ${JSON.stringify(data)}\n\n`);
50
- });
48
+ serverSentEvents = (data) => {
49
+ if (/\.(jsx?|tsx?)$/.test(data.file)) {
50
+ data.file = data.file.replace(".ts", ".js").replace(".jsx", ".js");
51
+ }
52
+ CONNECTIONS.forEach((rep) => {
53
+ rep.write(`data: ${JSON.stringify(data)}\n\n`);
54
+ });
55
+ };
51
56
  });
52
57
  return fastify;
53
58
  }
54
- export function getPostCSSConfig() {
59
+ export async function getPostCSSConfig() {
55
60
  try {
56
- return postcssrc({});
61
+ return await postcssrc({});
57
62
  }
58
63
  catch {
59
64
  return { plugins: [cssnano], options: {}, file: "" };
@@ -67,15 +72,14 @@ async function getBundleConfig() {
67
72
  esbuild: {},
68
73
  "html-minifier-terser": {},
69
74
  critical: {},
75
+ deletePrev: true,
70
76
  };
71
77
  try {
72
- const cfgPath = path.resolve(process.cwd(), "bundle.config.cjs");
73
- await access(cfgPath, 0);
78
+ const cfgPath = path.resolve(process.cwd(), "bundle.config.js");
74
79
  const config = await import(`file://${cfgPath}`);
75
80
  return { ...base, ...config.default };
76
81
  }
77
82
  catch {
78
- console.log("catch");
79
83
  return base;
80
84
  }
81
85
  }
@@ -84,7 +88,7 @@ export function addHMRCode(html, file, ast) {
84
88
  if (!htmlIdMap.has(file)) {
85
89
  htmlIdMap.set(file, randomText());
86
90
  }
87
- const script = createScript({ type: "module" }, getHMRCode(file, htmlIdMap.get(file), bundleConfig.src, bundleConfig.build));
91
+ const script = createScript({ type: "module" }, getHMRCode(file, htmlIdMap.get(file), bundleConfig.src));
88
92
  let DOM;
89
93
  if (html.includes("<!DOCTYPE html>") || html.includes("<html")) {
90
94
  DOM = ast || parse(html);
@@ -102,22 +106,35 @@ export function addHMRCode(html, file, ast) {
102
106
  function randomText() {
103
107
  return Math.random().toString(32).slice(2);
104
108
  }
105
- function getHMRCode(file, id, src, build) {
106
- return `import { render, html, $, $$, setInsertDiffing } from "hydro-js";
109
+ function getHMRCode(file, id, src) {
110
+ return `import { render, html, $, $$, setShouldSetReactivity } from "hydro-js";
111
+ window.isHMR = true;
107
112
  if (!window.eventsource${id}) {
108
- setInsertDiffing(true);
109
- window.eventsource${id} = new EventSource("/events");
113
+ window.eventsource${id} = new EventSource("/hmr");
114
+ window.eventsource${id}.addEventListener('error', (e) => {
115
+ setTimeout(() => {
116
+ window.eventsource${id} = new EventSource("/hmr");
117
+ }, 1000);
118
+ });
110
119
  window.eventsource${id}.addEventListener("message", ({ data }) => {
111
120
  const dataObj = JSON.parse(data);
112
121
  const file = "${file}";
113
122
 
114
123
  if (file === dataObj.file && "html" in dataObj) {
124
+ let newHTML;
125
+ try {
126
+ newHTML = html\`\${dataObj.html}\`
127
+ } catch {
128
+ setShouldSetReactivity(false);
129
+ newHTML = html\`\${dataObj.html}\`
130
+ setShouldSetReactivity(true);
131
+ }
132
+
115
133
  if (dataObj.html.startsWith('<!DOCTYPE html>') || dataObj.html.startsWith('<html')) {
116
134
  document.head.remove(); // Don't try to diff the head – just re-run the scripts
117
- render(html\`\${dataObj.html}\`, document.documentElement);
135
+ render(newHTML, document.documentElement, false);
118
136
  } else {
119
137
  const hmrID = "${id}";
120
- const newHTML = html\`\${dataObj.html}\`;
121
138
  const hmrElems = Array.from(newHTML.childNodes);
122
139
  const hmrWheres = Array.from($$(\`[data-hmr="\${hmrID}"]\`))
123
140
  // render new elements for old elements. Then, remove rest old elements and add add new elements after the last old one
@@ -140,10 +157,12 @@ function getHMRCode(file, id, src, build) {
140
157
  }
141
158
  }
142
159
 
143
- setTimeout(() => dispatchEvent(new Event("popstate")));
144
- } else if (dataObj.file.endsWith("css")) {
160
+ if (dataObj.file === \`${src}/index.html\`) {
161
+ dispatchEvent(new Event("popstate"));
162
+ }
163
+ } else if (dataObj.file.endsWith(".css")) {
145
164
  updateElem("link");
146
- } else if (dataObj.file.endsWith("js")) {
165
+ } else if (dataObj.file.endsWith(".js")) {
147
166
  updateElem("script")
148
167
  }
149
168
 
@@ -154,14 +173,22 @@ function getHMRCode(file, id, src, build) {
154
173
  const elem = $(\`[data-hmr="\${hmrId}"] \${type}[\${attr}^="\${noSrcFile}"]\`); // could be $(\`\${type}[data-hmr="\${hmrId}"][\${attr}^="\${noSrcFile}"]\`) ?
155
174
 
156
175
  if (elem) {
157
- const clone = document.createElement(type);
158
- for (const key of elem.getAttributeNames()) {
159
- clone.setAttribute(key, elem.getAttribute(key));
176
+ updateOne(type, attr, elem)
177
+ } else {
178
+ for(const e of $$(\`[data-hmr="\${hmrId}"] \${type}\`)) {
179
+ updateOne(type, attr, e);
160
180
  }
161
- clone.setAttribute(attr, elem.getAttribute(attr) + "?v=" + String(Math.random().toFixed(4)).slice(2));
162
- render(clone, elem, false);
163
181
  }
164
182
  }
183
+
184
+ function updateOne(type, attr, elem) {
185
+ const clone = document.createElement(type);
186
+ for (const key of elem.getAttributeNames()) {
187
+ clone.setAttribute(key, elem.getAttribute(key));
188
+ }
189
+ clone.setAttribute(attr, elem.getAttribute(attr) + "?v=" + String(Math.random().toFixed(4)).slice(2));
190
+ render(clone, elem, false);
191
+ }
165
192
  });
166
193
  }
167
194
  `;
package/logo.jpg ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "html-bundle",
3
- "version": "6.0.0-beta",
3
+ "version": "6.0.0",
4
4
  "description": "A very simple bundler for HTML SFC",
5
5
  "bin": "./dist/bundle.mjs",
6
6
  "scripts": {
package/src/bundle.mts CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  bundleConfig,
25
25
  serverSentEvents,
26
26
  addHMRCode,
27
- } from "./utils.js";
27
+ } from "./utils.mjs";
28
28
 
29
29
  const isHMR = process.argv.includes("--hmr") || bundleConfig.hmr;
30
30
  const isCritical = process.argv.includes("--critical") || bundleConfig.critical;
@@ -34,27 +34,29 @@ const handlerFile = process.argv.includes("--handler")
34
34
  : bundleConfig.handler;
35
35
 
36
36
  process.env.NODE_ENV = isHMR ? "development" : "production"; // just in case other tools are using it
37
- const timer = performance.now();
37
+ let timer = performance.now();
38
38
  let { plugins, options, file: postcssFile } = await getPostCSSConfig();
39
39
  let CSSprocessor = postcss(plugins as AcceptedPlugin[]);
40
40
  let fastify;
41
41
  const inlineFiles = new Set<string>();
42
42
  const TEMPLATE_LITERAL_MINIFIER = /\n\s+/g;
43
43
  const INLINE_BUNDLE_FILE = /-bundle-\d+.tsx$/;
44
- const SUPPORTED_FILES = /\.(html|css|m?jsx?|m?tsx?)$/;
44
+ const SUPPORTED_FILES = /\.(html|css|jsx?|tsx?)$/;
45
45
  const execFilePromise = promisify(execFile);
46
46
 
47
- await rm(bundleConfig.build, { force: true, recursive: true });
47
+ if (bundleConfig.deletePrev) {
48
+ await rm(bundleConfig.build, { force: true, recursive: true });
49
+ }
48
50
 
49
51
  glob(`${bundleConfig.src}/**/*.*`, build);
50
52
 
51
- async function build(err: any, files: string[]) {
53
+ async function build(err: any, files: string[], firstRun = true) {
52
54
  if (err) {
53
55
  console.error(err);
54
56
  process.exit(1);
55
57
  }
56
58
 
57
- if (isHMR) {
59
+ if (isHMR && firstRun) {
58
60
  fastify = await createDefaultServer(isSecure);
59
61
  fastify.listen(bundleConfig.port);
60
62
  console.log(`💻 Sever listening on port ${bundleConfig.port}.`);
@@ -66,7 +68,7 @@ async function build(err: any, files: string[]) {
66
68
  if (!SUPPORTED_FILES.test(file)) {
67
69
  if (handlerFile) {
68
70
  const { stdout } = await execFilePromise("node", [handlerFile, file]);
69
- console.log("📋 Logging Handler: ", String(stdout));
71
+ if (String(stdout)) console.log("📋 Logging Handler: ", String(stdout));
70
72
  } else {
71
73
  await fileCopy(file);
72
74
  }
@@ -97,7 +99,7 @@ async function build(err: any, files: string[]) {
97
99
  `🚀 Build finished in ${(performance.now() - timer).toFixed(2)}ms ✨`
98
100
  );
99
101
 
100
- if (isHMR) {
102
+ if (isHMR && firstRun) {
101
103
  console.log(`⌛ Waiting for file changes ...`);
102
104
 
103
105
  if (postcssFile) {
@@ -105,6 +107,9 @@ async function build(err: any, files: string[]) {
105
107
  const tailwindCSSWatcher = watch(
106
108
  postcssFile.replace("postcss", "tailwind")
107
109
  ); // Assuming that the file ext is the same
110
+ const tsConfigWatcher = watch(
111
+ postcssFile.split("\\").slice(0, -1).join("\\") + "\\tsconfig.json"
112
+ );
108
113
 
109
114
  const cssFiles = files.filter((file) => file.endsWith(".css"));
110
115
  postCSSWatcher.on(
@@ -115,6 +120,10 @@ async function build(err: any, files: string[]) {
115
120
  "change",
116
121
  async () => await rebuildCSS(cssFiles, "tailwind")
117
122
  );
123
+ tsConfigWatcher.on("change", async () => {
124
+ timer = performance.now();
125
+ await build(null, files, false);
126
+ });
118
127
  }
119
128
 
120
129
  const watcher = watch(bundleConfig.src);
@@ -165,7 +174,7 @@ async function build(err: any, files: string[]) {
165
174
  let html;
166
175
  if (file.endsWith(".html")) {
167
176
  // To refill the inlineFiles needed to build JS
168
- for (const htmlFile of files.filter((file) => file.endsWith("html"))) {
177
+ for (const htmlFile of files.filter((file) => file.endsWith(".html"))) {
169
178
  await writeInlineScripts(htmlFile);
170
179
  }
171
180
  await minifyCode();
@@ -176,14 +185,15 @@ async function build(err: any, files: string[]) {
176
185
  }
177
186
  }
178
187
  html = await minifyHTML(file, getBuildPath(file));
179
- } else if (/(m?jsx?|m?tsx?)$/.test(file)) {
188
+ } else if (/\.(jsx?|tsx?)$/.test(file)) {
189
+ inlineFiles.add(file);
180
190
  await minifyCode();
181
191
  } else {
182
192
  const { stdout } = await execFilePromise("node", [handlerFile, file]);
183
- console.log("📋 Logging Handler: ", String(stdout));
193
+ if (String(stdout)) console.log("📋 Logging Handler: ", String(stdout));
184
194
  }
185
195
 
186
- serverSentEvents!({ file, html });
196
+ serverSentEvents?.({ file, html });
187
197
  }
188
198
  }
189
199
  }
@@ -212,7 +222,7 @@ async function minifyCode(): Promise<unknown> {
212
222
  sourcemap: isHMR,
213
223
  splitting: true,
214
224
  define: {
215
- "process.env.NODE_ENV": process.env.NODE_ENV,
225
+ "process.env.NODE_ENV": `"${process.env.NODE_ENV}"`,
216
226
  },
217
227
  loader: { ".js": "jsx", ".ts": "tsx" },
218
228
  bundle: true,
@@ -336,11 +346,15 @@ async function minifyHTML(file: string, buildFile: string) {
336
346
  fileText = serialize(DOM);
337
347
 
338
348
  // Minify HTML
339
- fileText = await minify(fileText, {
340
- collapseWhitespace: true,
341
- removeComments: true,
342
- ...bundleConfig["html-minifier-terser"],
343
- });
349
+ try {
350
+ fileText = await minify(fileText, {
351
+ collapseWhitespace: true,
352
+ removeComments: true,
353
+ ...bundleConfig["html-minifier-terser"],
354
+ });
355
+ } catch (e) {
356
+ console.error(e);
357
+ }
344
358
 
345
359
  if (!isCritical) {
346
360
  await writeFile(buildFile, fileText);
@@ -1,5 +1,5 @@
1
1
  import type { FastifyServerOptions } from "fastify";
2
- import { access, copyFile, mkdir, readFile } from "fs/promises";
2
+ import { copyFile, mkdir, readFile } from "fs/promises";
3
3
  import path from "path";
4
4
  import Fastify from "fastify";
5
5
  import fastifyStatic from "fastify-static";
@@ -58,24 +58,28 @@ export async function createDefaultServer(isSecure: boolean) {
58
58
  fastify.register(fastifyStatic, {
59
59
  root: path.join(process.cwd(), bundleConfig.build),
60
60
  });
61
- fastify.get("/events", (_req, reply) => {
61
+ fastify.get("/hmr", (_req, reply) => {
62
62
  reply.raw.setHeader("Content-Type", "text/event-stream");
63
63
  reply.raw.setHeader("Cache-Control", "no-cache");
64
64
  !isSecure && reply.raw.setHeader("Connection", "keep-alive");
65
65
 
66
66
  CONNECTIONS.push(reply.raw);
67
67
 
68
- serverSentEvents = (data) =>
68
+ serverSentEvents = (data) => {
69
+ if (/\.(jsx?|tsx?)$/.test(data.file)) {
70
+ data.file = data.file.replace(".ts", ".js").replace(".jsx", ".js");
71
+ }
69
72
  CONNECTIONS.forEach((rep) => {
70
73
  rep.write(`data: ${JSON.stringify(data)}\n\n`);
71
74
  });
75
+ };
72
76
  });
73
77
  return fastify;
74
78
  }
75
79
 
76
- export function getPostCSSConfig() {
80
+ export async function getPostCSSConfig() {
77
81
  try {
78
- return postcssrc({});
82
+ return await postcssrc({});
79
83
  } catch {
80
84
  return { plugins: [cssnano], options: {}, file: "" };
81
85
  }
@@ -89,15 +93,14 @@ async function getBundleConfig() {
89
93
  esbuild: {},
90
94
  "html-minifier-terser": {},
91
95
  critical: {},
96
+ deletePrev: true,
92
97
  };
93
98
 
94
99
  try {
95
- const cfgPath = path.resolve(process.cwd(), "bundle.config.cjs");
96
- await access(cfgPath, 0);
100
+ const cfgPath = path.resolve(process.cwd(), "bundle.config.js");
97
101
  const config = await import(`file://${cfgPath}`);
98
102
  return { ...base, ...config.default };
99
103
  } catch {
100
- console.log("catch");
101
104
  return base;
102
105
  }
103
106
  }
@@ -114,7 +117,7 @@ export function addHMRCode(
114
117
 
115
118
  const script = createScript(
116
119
  { type: "module" },
117
- getHMRCode(file, htmlIdMap.get(file), bundleConfig.src, bundleConfig.build)
120
+ getHMRCode(file, htmlIdMap.get(file), bundleConfig.src)
118
121
  );
119
122
 
120
123
  let DOM;
@@ -139,22 +142,35 @@ function randomText() {
139
142
  return Math.random().toString(32).slice(2);
140
143
  }
141
144
 
142
- function getHMRCode(file: string, id: string, src: string, build: string) {
143
- return `import { render, html, $, $$, setInsertDiffing } from "hydro-js";
145
+ function getHMRCode(file: string, id: string, src: string) {
146
+ return `import { render, html, $, $$, setShouldSetReactivity } from "hydro-js";
147
+ window.isHMR = true;
144
148
  if (!window.eventsource${id}) {
145
- setInsertDiffing(true);
146
- window.eventsource${id} = new EventSource("/events");
149
+ window.eventsource${id} = new EventSource("/hmr");
150
+ window.eventsource${id}.addEventListener('error', (e) => {
151
+ setTimeout(() => {
152
+ window.eventsource${id} = new EventSource("/hmr");
153
+ }, 1000);
154
+ });
147
155
  window.eventsource${id}.addEventListener("message", ({ data }) => {
148
156
  const dataObj = JSON.parse(data);
149
157
  const file = "${file}";
150
158
 
151
159
  if (file === dataObj.file && "html" in dataObj) {
160
+ let newHTML;
161
+ try {
162
+ newHTML = html\`\${dataObj.html}\`
163
+ } catch {
164
+ setShouldSetReactivity(false);
165
+ newHTML = html\`\${dataObj.html}\`
166
+ setShouldSetReactivity(true);
167
+ }
168
+
152
169
  if (dataObj.html.startsWith('<!DOCTYPE html>') || dataObj.html.startsWith('<html')) {
153
170
  document.head.remove(); // Don't try to diff the head – just re-run the scripts
154
- render(html\`\${dataObj.html}\`, document.documentElement);
171
+ render(newHTML, document.documentElement, false);
155
172
  } else {
156
173
  const hmrID = "${id}";
157
- const newHTML = html\`\${dataObj.html}\`;
158
174
  const hmrElems = Array.from(newHTML.childNodes);
159
175
  const hmrWheres = Array.from($$(\`[data-hmr="\${hmrID}"]\`))
160
176
  // render new elements for old elements. Then, remove rest old elements and add add new elements after the last old one
@@ -177,10 +193,12 @@ function getHMRCode(file: string, id: string, src: string, build: string) {
177
193
  }
178
194
  }
179
195
 
180
- setTimeout(() => dispatchEvent(new Event("popstate")));
181
- } else if (dataObj.file.endsWith("css")) {
196
+ if (dataObj.file === \`${src}/index.html\`) {
197
+ dispatchEvent(new Event("popstate"));
198
+ }
199
+ } else if (dataObj.file.endsWith(".css")) {
182
200
  updateElem("link");
183
- } else if (dataObj.file.endsWith("js")) {
201
+ } else if (dataObj.file.endsWith(".js")) {
184
202
  updateElem("script")
185
203
  }
186
204
 
@@ -191,14 +209,22 @@ function getHMRCode(file: string, id: string, src: string, build: string) {
191
209
  const elem = $(\`[data-hmr="\${hmrId}"] \${type}[\${attr}^="\${noSrcFile}"]\`); // could be $(\`\${type}[data-hmr="\${hmrId}"][\${attr}^="\${noSrcFile}"]\`) ?
192
210
 
193
211
  if (elem) {
194
- const clone = document.createElement(type);
195
- for (const key of elem.getAttributeNames()) {
196
- clone.setAttribute(key, elem.getAttribute(key));
212
+ updateOne(type, attr, elem)
213
+ } else {
214
+ for(const e of $$(\`[data-hmr="\${hmrId}"] \${type}\`)) {
215
+ updateOne(type, attr, e);
197
216
  }
198
- clone.setAttribute(attr, elem.getAttribute(attr) + "?v=" + String(Math.random().toFixed(4)).slice(2));
199
- render(clone, elem, false);
200
217
  }
201
218
  }
219
+
220
+ function updateOne(type, attr, elem) {
221
+ const clone = document.createElement(type);
222
+ for (const key of elem.getAttributeNames()) {
223
+ clone.setAttribute(key, elem.getAttribute(key));
224
+ }
225
+ clone.setAttribute(attr, elem.getAttribute(attr) + "?v=" + String(Math.random().toFixed(4)).slice(2));
226
+ render(clone, elem, false);
227
+ }
202
228
  });
203
229
  }
204
230
  `;