html-bundle 6.0.1-beta → 6.0.4

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,17 +1,23 @@
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
 
19
+ ## Demo
20
+
15
21
  ![Demo](./example.gif)
16
22
 
17
23
  ## Installation and Usage
@@ -39,7 +45,7 @@ $ npm run build
39
45
 
40
46
  ## CLI
41
47
 
42
- `--hmr`: boots up a static server and enables Hot Module Replacement. **This generates a development build.**<br>
48
+ `--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
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>
44
50
  `--critical`: uses critical to extract and inline critical-path CSS to HTML.<br>
45
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,6 +58,7 @@ Generate the config in the root and call it "bundle.config.js"
52
58
  **src:** input path. Default to "src"<br>
53
59
  **build:** output path. Defaults to "build"<br>
54
60
  **port:** For the HMR Server. Defaults to 5000<br>
61
+ **deletePrev:** Whether to delelte the build folder. Defaults to true<br>
55
62
  **esbuild:** Your additional config<br>
56
63
  **html-minifier-terser:** Your additional config<br>
57
64
  **critical:** Your additional config<br>
@@ -76,13 +83,14 @@ Set `"jsxFactory": "h"` in `tsconfig.json` for JSX.
76
83
  <title>Example</title>
77
84
  <meta name="Description" content="Example for html-bundle" />
78
85
  <script type="module">
79
- import { render, h } from "hydro-js";
86
+ import { render, h, reactive } from "hydro-js";
80
87
 
81
- function Example() {
82
- return <main id="app">Testing html-bundle</main>;
88
+ function Example({ name }) {
89
+ return <main id="app">Hi {name}</main>;
83
90
  }
84
91
 
85
- render(<Example />, "#app");
92
+ const name = reactive("Tester");
93
+ render(<Example name={name} />, "#app");
86
94
  </script>
87
95
  <style>
88
96
  body {
@@ -96,11 +104,7 @@ Set `"jsxFactory": "h"` in `tsconfig.json` for JSX.
96
104
  </html>
97
105
  ```
98
106
 
99
- ### Output
100
-
101
- ![Output](output.JPG)
102
-
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,10 @@ 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
  ```
package/dist/bundle.mjs CHANGED
@@ -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 });
33
- glob(`${bundleConfig.src}/**/*.*`, build);
34
- async function build(err, files) {
32
+ if (bundleConfig.deletePrev) {
33
+ await rm(bundleConfig.build, { force: true, recursive: true });
34
+ }
35
+ glob(`${bundleConfig.src}/**/*`, build);
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,22 +80,26 @@ 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
- let addCount = 0; // The add watcher will add all the files initially - do not rebuild them
91
98
  watcher.on("add", async (file) => {
92
- if (addCount++ <= files.length || INLINE_BUNDLE_FILE.test(file)) {
99
+ file = String.raw `${file}`.replace(/\\/g, "/"); // glob and chokidar diff
100
+ if (files.includes(file) || INLINE_BUNDLE_FILE.test(file)) {
93
101
  return;
94
102
  }
95
- file = String.raw `${file}`.replace(/\\/g, "/"); // glob and chokidar diff
96
103
  await rebuild(file);
97
104
  console.log(`⚡ added ${file} to the build`);
98
105
  });
@@ -126,7 +133,7 @@ async function build(err, files) {
126
133
  let html;
127
134
  if (file.endsWith(".html")) {
128
135
  // To refill the inlineFiles needed to build JS
129
- for (const htmlFile of files.filter((file) => file.endsWith("html"))) {
136
+ for (const htmlFile of files.filter((file) => file.endsWith(".html"))) {
130
137
  await writeInlineScripts(htmlFile);
131
138
  }
132
139
  await minifyCode();
@@ -138,14 +145,16 @@ async function build(err, files) {
138
145
  }
139
146
  html = await minifyHTML(file, getBuildPath(file));
140
147
  }
141
- else if (/(m?jsx?|m?tsx?)$/.test(file)) {
148
+ else if (/\.(jsx?|tsx?)$/.test(file)) {
149
+ inlineFiles.add(file);
142
150
  await minifyCode();
143
151
  }
144
152
  else {
145
153
  const { stdout } = await execFilePromise("node", [handlerFile, file]);
146
- console.log("📋 Logging Handler: ", String(stdout));
154
+ if (String(stdout))
155
+ console.log("📋 Logging Handler: ", String(stdout));
147
156
  }
148
- serverSentEvents({ file, html });
157
+ serverSentEvents?.({ file, html });
149
158
  }
150
159
  }
151
160
  }
@@ -173,7 +182,7 @@ async function minifyCode() {
173
182
  sourcemap: isHMR,
174
183
  splitting: true,
175
184
  define: {
176
- "process.env.NODE_ENV": process.env.NODE_ENV,
185
+ "process.env.NODE_ENV": `"${process.env.NODE_ENV}"`,
177
186
  },
178
187
  loader: { ".js": "jsx", ".ts": "tsx" },
179
188
  bundle: true,
@@ -195,11 +204,11 @@ async function minifyCode() {
195
204
  missingPkg = true;
196
205
  const packageNameRegex = /(?<=").*(?=")/;
197
206
  const [pkgName] = error.text.match(packageNameRegex);
198
- console.log(`📦 Package ${pkgName} was installed for you`);
199
207
  await awaitSpawn(process.platform === "win32" ? "npm.cmd" : "npm", [
200
208
  "install",
201
209
  pkgName,
202
210
  ]);
211
+ console.log(`📦 Package ${pkgName} was installed for you`);
203
212
  }
204
213
  }
205
214
  if (missingPkg) {
@@ -283,11 +292,16 @@ async function minifyHTML(file, buildFile) {
283
292
  }
284
293
  fileText = serialize(DOM);
285
294
  // Minify HTML
286
- fileText = await minify(fileText, {
287
- collapseWhitespace: true,
288
- removeComments: true,
289
- ...bundleConfig["html-minifier-terser"],
290
- });
295
+ try {
296
+ fileText = await minify(fileText, {
297
+ collapseWhitespace: true,
298
+ removeComments: true,
299
+ ...bundleConfig["html-minifier-terser"],
300
+ });
301
+ }
302
+ catch (e) {
303
+ console.error(e);
304
+ }
291
305
  if (!isCritical) {
292
306
  await writeFile(buildFile, fileText);
293
307
  return fileText;
package/dist/utils.mjs CHANGED
@@ -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,6 +72,7 @@ async function getBundleConfig() {
67
72
  esbuild: {},
68
73
  "html-minifier-terser": {},
69
74
  critical: {},
75
+ deletePrev: true,
70
76
  };
71
77
  try {
72
78
  const cfgPath = path.resolve(process.cwd(), "bundle.config.js");
@@ -101,21 +107,35 @@ function randomText() {
101
107
  return Math.random().toString(32).slice(2);
102
108
  }
103
109
  function getHMRCode(file, id, src) {
104
- return `import { render, html, $, $$, setInsertDiffing } from "hydro-js";
110
+ return `import { render, html, $, $$, setShouldSetReactivity } from "hydro-js";
111
+ window.isHMR = true;
112
+ window.lastCalled = new Map();
105
113
  if (!window.eventsource${id}) {
106
- setInsertDiffing(true);
107
- window.eventsource${id} = new EventSource("/events");
114
+ window.eventsource${id} = new EventSource("/hmr");
115
+ window.eventsource${id}.addEventListener('error', (e) => {
116
+ setTimeout(() => {
117
+ window.eventsource${id} = new EventSource("/hmr");
118
+ }, 1000);
119
+ });
108
120
  window.eventsource${id}.addEventListener("message", ({ data }) => {
109
121
  const dataObj = JSON.parse(data);
110
122
  const file = "${file}";
111
123
 
112
124
  if (file === dataObj.file && "html" in dataObj) {
125
+ let newHTML;
126
+ try {
127
+ newHTML = html\`\${dataObj.html}\`
128
+ } catch {
129
+ setShouldSetReactivity(false);
130
+ newHTML = html\`\${dataObj.html}\`
131
+ setShouldSetReactivity(true);
132
+ }
133
+
113
134
  if (dataObj.html.startsWith('<!DOCTYPE html>') || dataObj.html.startsWith('<html')) {
114
135
  document.head.remove(); // Don't try to diff the head – just re-run the scripts
115
- render(html\`\${dataObj.html}\`, document.documentElement);
136
+ render(newHTML, document.documentElement, false);
116
137
  } else {
117
138
  const hmrID = "${id}";
118
- const newHTML = html\`\${dataObj.html}\`;
119
139
  const hmrElems = Array.from(newHTML.childNodes);
120
140
  const hmrWheres = Array.from($$(\`[data-hmr="\${hmrID}"]\`))
121
141
  // render new elements for old elements. Then, remove rest old elements and add add new elements after the last old one
@@ -137,13 +157,35 @@ function getHMRCode(file, id, src) {
137
157
  }
138
158
  }
139
159
  }
160
+
161
+ $$('link[rel="stylesheet"][href]').forEach(link => {
162
+ link.setAttribute("href", link.getAttribute("href") + "?v=" + String(Math.random().toFixed(4)).slice(2));
163
+ })
164
+ if (dataObj.html.includes("<script")) updateElem("script");
165
+
140
166
 
141
- setTimeout(() => dispatchEvent(new Event("popstate")));
142
- } else if (dataObj.file.endsWith("css")) {
143
- updateElem("link");
144
- } else if (dataObj.file.endsWith("js")) {
145
- updateElem("script")
167
+ if (dataObj.file === \`${src}/index.html\`) {
168
+ dispatchEvent(new Event("popstate"));
169
+ }
170
+ } else if (dataObj.file.endsWith(".css")) {
171
+ const now = performance.now();
172
+ if (!window.lastCalled.has(dataObj.file) || now - window.lastCalled.get(dataObj.file) > 100) {
173
+ $$('link[rel="stylesheet"][href]').forEach(link => {
174
+ link.setAttribute("href", link.getAttribute("href") + "?v=" + String(Math.random().toFixed(4)).slice(2));
175
+ })
176
+ window.lastCalled.set(dataObj.file, now)
177
+ }
178
+ } else if (dataObj.file.endsWith(".js")) {
179
+ const now = performance.now();
180
+ if (!window.lastCalled.has(dataObj.file) || now - window.lastCalled.get(dataObj.file) > 100) {
181
+ $$('link[rel="stylesheet"][href]').forEach(link => {
182
+ link.setAttribute("href", link.getAttribute("href") + "?v=" + String(Math.random().toFixed(4)).slice(2));
183
+ })
184
+ updateElem("script");
185
+ window.lastCalled.set(dataObj.file, now)
186
+ }
146
187
  }
188
+
147
189
 
148
190
  function updateElem(type) {
149
191
  const hmrId = "${id}";
@@ -152,14 +194,22 @@ function getHMRCode(file, id, src) {
152
194
  const elem = $(\`[data-hmr="\${hmrId}"] \${type}[\${attr}^="\${noSrcFile}"]\`); // could be $(\`\${type}[data-hmr="\${hmrId}"][\${attr}^="\${noSrcFile}"]\`) ?
153
195
 
154
196
  if (elem) {
155
- const clone = document.createElement(type);
156
- for (const key of elem.getAttributeNames()) {
157
- clone.setAttribute(key, elem.getAttribute(key));
197
+ updateOne(type, attr, elem)
198
+ } else {
199
+ for(const e of $$(\`[data-hmr="\${hmrId}"] \${type}\`)) {
200
+ updateOne(type, attr, e);
158
201
  }
159
- clone.setAttribute(attr, elem.getAttribute(attr) + "?v=" + String(Math.random().toFixed(4)).slice(2));
160
- render(clone, elem, false);
161
202
  }
162
203
  }
204
+
205
+ function updateOne(type, attr, elem) {
206
+ const clone = document.createElement(type);
207
+ for (const key of elem.getAttributeNames()) {
208
+ clone.setAttribute(key, elem.getAttribute(key));
209
+ }
210
+ clone.setAttribute(attr, elem.getAttribute(attr) + "?v=" + String(Math.random().toFixed(4)).slice(2));
211
+ render(clone, elem, false);
212
+ }
163
213
  });
164
214
  }
165
215
  `;
package/example.gif CHANGED
Binary file
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.1-beta",
3
+ "version": "6.0.4",
4
4
  "description": "A very simple bundler for HTML SFC",
5
5
  "bin": "./dist/bundle.mjs",
6
6
  "scripts": {
@@ -36,11 +36,12 @@
36
36
  "chokidar": "^3.5.2",
37
37
  "critical": "^4.0.1",
38
38
  "cssnano": "^5.0.14",
39
- "esbuild": "^0.14.8",
40
- "fastify": "^3.25.2",
39
+ "esbuild": "^0.14.9",
40
+ "fastify": "^3.25.3",
41
41
  "fastify-static": "^4.5.0",
42
42
  "glob": "^7.2.0",
43
43
  "html-minifier-terser": "^6.1.0",
44
+ "hydro-js": "^1.5.3",
44
45
  "parse5": "^6.0.1",
45
46
  "postcss": "^8.4.5",
46
47
  "postcss-load-config": "^3.1.0"
package/src/bundle.mts CHANGED
@@ -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
- glob(`${bundleConfig.src}/**/*.*`, build);
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,15 +120,18 @@ 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);
121
- let addCount = 0; // The add watcher will add all the files initially - do not rebuild them
122
130
  watcher.on("add", async (file) => {
123
- if (addCount++ <= files.length || INLINE_BUNDLE_FILE.test(file)) {
131
+ file = String.raw`${file}`.replace(/\\/g, "/"); // glob and chokidar diff
132
+ if (files.includes(file) || INLINE_BUNDLE_FILE.test(file)) {
124
133
  return;
125
134
  }
126
- file = String.raw`${file}`.replace(/\\/g, "/"); // glob and chokidar diff
127
135
 
128
136
  await rebuild(file);
129
137
 
@@ -165,7 +173,7 @@ async function build(err: any, files: string[]) {
165
173
  let html;
166
174
  if (file.endsWith(".html")) {
167
175
  // To refill the inlineFiles needed to build JS
168
- for (const htmlFile of files.filter((file) => file.endsWith("html"))) {
176
+ for (const htmlFile of files.filter((file) => file.endsWith(".html"))) {
169
177
  await writeInlineScripts(htmlFile);
170
178
  }
171
179
  await minifyCode();
@@ -176,14 +184,15 @@ async function build(err: any, files: string[]) {
176
184
  }
177
185
  }
178
186
  html = await minifyHTML(file, getBuildPath(file));
179
- } else if (/(m?jsx?|m?tsx?)$/.test(file)) {
187
+ } else if (/\.(jsx?|tsx?)$/.test(file)) {
188
+ inlineFiles.add(file);
180
189
  await minifyCode();
181
190
  } else {
182
191
  const { stdout } = await execFilePromise("node", [handlerFile, file]);
183
- console.log("📋 Logging Handler: ", String(stdout));
192
+ if (String(stdout)) console.log("📋 Logging Handler: ", String(stdout));
184
193
  }
185
194
 
186
- serverSentEvents!({ file, html });
195
+ serverSentEvents?.({ file, html });
187
196
  }
188
197
  }
189
198
  }
@@ -212,7 +221,7 @@ async function minifyCode(): Promise<unknown> {
212
221
  sourcemap: isHMR,
213
222
  splitting: true,
214
223
  define: {
215
- "process.env.NODE_ENV": process.env.NODE_ENV,
224
+ "process.env.NODE_ENV": `"${process.env.NODE_ENV}"`,
216
225
  },
217
226
  loader: { ".js": "jsx", ".ts": "tsx" },
218
227
  bundle: true,
@@ -234,12 +243,13 @@ async function minifyCode(): Promise<unknown> {
234
243
  missingPkg = true;
235
244
  const packageNameRegex = /(?<=").*(?=")/;
236
245
  const [pkgName] = error.text.match(packageNameRegex);
237
- console.log(`📦 Package ${pkgName} was installed for you`);
238
246
 
239
247
  await awaitSpawn(process.platform === "win32" ? "npm.cmd" : "npm", [
240
248
  "install",
241
249
  pkgName,
242
250
  ]);
251
+
252
+ console.log(`📦 Package ${pkgName} was installed for you`);
243
253
  }
244
254
  }
245
255
 
@@ -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);
package/src/utils.mts CHANGED
@@ -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,6 +93,7 @@ async function getBundleConfig() {
89
93
  esbuild: {},
90
94
  "html-minifier-terser": {},
91
95
  critical: {},
96
+ deletePrev: true,
92
97
  };
93
98
 
94
99
  try {
@@ -138,21 +143,35 @@ function randomText() {
138
143
  }
139
144
 
140
145
  function getHMRCode(file: string, id: string, src: string) {
141
- return `import { render, html, $, $$, setInsertDiffing } from "hydro-js";
146
+ return `import { render, html, $, $$, setShouldSetReactivity } from "hydro-js";
147
+ window.isHMR = true;
148
+ window.lastCalled = new Map();
142
149
  if (!window.eventsource${id}) {
143
- setInsertDiffing(true);
144
- window.eventsource${id} = new EventSource("/events");
150
+ window.eventsource${id} = new EventSource("/hmr");
151
+ window.eventsource${id}.addEventListener('error', (e) => {
152
+ setTimeout(() => {
153
+ window.eventsource${id} = new EventSource("/hmr");
154
+ }, 1000);
155
+ });
145
156
  window.eventsource${id}.addEventListener("message", ({ data }) => {
146
157
  const dataObj = JSON.parse(data);
147
158
  const file = "${file}";
148
159
 
149
160
  if (file === dataObj.file && "html" in dataObj) {
161
+ let newHTML;
162
+ try {
163
+ newHTML = html\`\${dataObj.html}\`
164
+ } catch {
165
+ setShouldSetReactivity(false);
166
+ newHTML = html\`\${dataObj.html}\`
167
+ setShouldSetReactivity(true);
168
+ }
169
+
150
170
  if (dataObj.html.startsWith('<!DOCTYPE html>') || dataObj.html.startsWith('<html')) {
151
171
  document.head.remove(); // Don't try to diff the head – just re-run the scripts
152
- render(html\`\${dataObj.html}\`, document.documentElement);
172
+ render(newHTML, document.documentElement, false);
153
173
  } else {
154
174
  const hmrID = "${id}";
155
- const newHTML = html\`\${dataObj.html}\`;
156
175
  const hmrElems = Array.from(newHTML.childNodes);
157
176
  const hmrWheres = Array.from($$(\`[data-hmr="\${hmrID}"]\`))
158
177
  // render new elements for old elements. Then, remove rest old elements and add add new elements after the last old one
@@ -174,13 +193,35 @@ function getHMRCode(file: string, id: string, src: string) {
174
193
  }
175
194
  }
176
195
  }
196
+
197
+ $$('link[rel="stylesheet"][href]').forEach(link => {
198
+ link.setAttribute("href", link.getAttribute("href") + "?v=" + String(Math.random().toFixed(4)).slice(2));
199
+ })
200
+ if (dataObj.html.includes("<script")) updateElem("script");
201
+
177
202
 
178
- setTimeout(() => dispatchEvent(new Event("popstate")));
179
- } else if (dataObj.file.endsWith("css")) {
180
- updateElem("link");
181
- } else if (dataObj.file.endsWith("js")) {
182
- updateElem("script")
203
+ if (dataObj.file === \`${src}/index.html\`) {
204
+ dispatchEvent(new Event("popstate"));
205
+ }
206
+ } else if (dataObj.file.endsWith(".css")) {
207
+ const now = performance.now();
208
+ if (!window.lastCalled.has(dataObj.file) || now - window.lastCalled.get(dataObj.file) > 100) {
209
+ $$('link[rel="stylesheet"][href]').forEach(link => {
210
+ link.setAttribute("href", link.getAttribute("href") + "?v=" + String(Math.random().toFixed(4)).slice(2));
211
+ })
212
+ window.lastCalled.set(dataObj.file, now)
213
+ }
214
+ } else if (dataObj.file.endsWith(".js")) {
215
+ const now = performance.now();
216
+ if (!window.lastCalled.has(dataObj.file) || now - window.lastCalled.get(dataObj.file) > 100) {
217
+ $$('link[rel="stylesheet"][href]').forEach(link => {
218
+ link.setAttribute("href", link.getAttribute("href") + "?v=" + String(Math.random().toFixed(4)).slice(2));
219
+ })
220
+ updateElem("script");
221
+ window.lastCalled.set(dataObj.file, now)
222
+ }
183
223
  }
224
+
184
225
 
185
226
  function updateElem(type) {
186
227
  const hmrId = "${id}";
@@ -189,14 +230,22 @@ function getHMRCode(file: string, id: string, src: string) {
189
230
  const elem = $(\`[data-hmr="\${hmrId}"] \${type}[\${attr}^="\${noSrcFile}"]\`); // could be $(\`\${type}[data-hmr="\${hmrId}"][\${attr}^="\${noSrcFile}"]\`) ?
190
231
 
191
232
  if (elem) {
192
- const clone = document.createElement(type);
193
- for (const key of elem.getAttributeNames()) {
194
- clone.setAttribute(key, elem.getAttribute(key));
233
+ updateOne(type, attr, elem)
234
+ } else {
235
+ for(const e of $$(\`[data-hmr="\${hmrId}"] \${type}\`)) {
236
+ updateOne(type, attr, e);
195
237
  }
196
- clone.setAttribute(attr, elem.getAttribute(attr) + "?v=" + String(Math.random().toFixed(4)).slice(2));
197
- render(clone, elem, false);
198
238
  }
199
239
  }
240
+
241
+ function updateOne(type, attr, elem) {
242
+ const clone = document.createElement(type);
243
+ for (const key of elem.getAttributeNames()) {
244
+ clone.setAttribute(key, elem.getAttribute(key));
245
+ }
246
+ clone.setAttribute(attr, elem.getAttribute(attr) + "?v=" + String(Math.random().toFixed(4)).slice(2));
247
+ render(clone, elem, false);
248
+ }
200
249
  });
201
250
  }
202
251
  `;
package/dist/utils.js DELETED
@@ -1,168 +0,0 @@
1
- import { access, copyFile, mkdir, readFile } from "fs/promises";
2
- import path from "path";
3
- import Fastify from "fastify";
4
- import fastifyStatic from "fastify-static";
5
- import postcssrc from "postcss-load-config";
6
- import cssnano from "cssnano";
7
- import { parse, parseFragment, serialize } from "parse5";
8
- import { createScript, getTagName, findElement, appendChild, } from "@web/parse5-utils";
9
- export const bundleConfig = await getBundleConfig();
10
- export function fileCopy(file) {
11
- return copyFile(file, getBuildPath(file));
12
- }
13
- export function createDir(file) {
14
- const buildPath = getBuildPath(file);
15
- const dir = buildPath.split("/").slice(0, -1).join("/");
16
- return mkdir(dir, { recursive: true });
17
- }
18
- export function getBuildPath(file) {
19
- return file.replace(`${bundleConfig.src}/`, `${bundleConfig.build}/`);
20
- }
21
- const CONNECTIONS = []; // In order to send the HMR information
22
- export let serverSentEvents;
23
- export async function createDefaultServer(isSecure) {
24
- const fastify = Fastify(isSecure
25
- ? {
26
- http2: true,
27
- https: {
28
- key: await readFile(path.join(process.cwd(), "localhost-key.pem")),
29
- cert: await readFile(path.join(process.cwd(), "localhost.pem")),
30
- },
31
- }
32
- : void 0);
33
- fastify.setNotFoundHandler(async (_req, reply) => {
34
- const file = await readFile(path.join(process.cwd(), bundleConfig.build, "/index.html"), {
35
- encoding: "utf-8",
36
- });
37
- reply.header("Content-Type", "text/html; charset=UTF-8");
38
- return reply.send(addHMRCode(file, `${bundleConfig.src}/index.html`));
39
- });
40
- fastify.register(fastifyStatic, {
41
- root: path.join(process.cwd(), bundleConfig.build),
42
- });
43
- fastify.get("/events", (_req, reply) => {
44
- reply.raw.setHeader("Content-Type", "text/event-stream");
45
- reply.raw.setHeader("Cache-Control", "no-cache");
46
- !isSecure && reply.raw.setHeader("Connection", "keep-alive");
47
- CONNECTIONS.push(reply.raw);
48
- serverSentEvents = (data) => CONNECTIONS.forEach((rep) => {
49
- rep.write(`data: ${JSON.stringify(data)}\n\n`);
50
- });
51
- });
52
- return fastify;
53
- }
54
- export function getPostCSSConfig() {
55
- try {
56
- return postcssrc({});
57
- }
58
- catch {
59
- return { plugins: [cssnano], options: {}, file: "" };
60
- }
61
- }
62
- async function getBundleConfig() {
63
- const base = {
64
- build: "build",
65
- src: "src",
66
- port: 5000,
67
- esbuild: {},
68
- "html-minifier-terser": {},
69
- critical: {},
70
- };
71
- try {
72
- const cfgPath = path.resolve(process.cwd(), "bundle.config.cjs");
73
- await access(cfgPath, 0);
74
- const config = await import(`file://${cfgPath}`);
75
- return { ...base, ...config.default };
76
- }
77
- catch {
78
- console.log("catch");
79
- return base;
80
- }
81
- }
82
- const htmlIdMap = new Map();
83
- export function addHMRCode(html, file, ast) {
84
- if (!htmlIdMap.has(file)) {
85
- htmlIdMap.set(file, randomText());
86
- }
87
- const script = createScript({ type: "module" }, getHMRCode(file, htmlIdMap.get(file), bundleConfig.src, bundleConfig.build));
88
- let DOM;
89
- if (html.includes("<!DOCTYPE html>") || html.includes("<html")) {
90
- DOM = ast || parse(html);
91
- const headNode = findElement(DOM, (e) => getTagName(e) === "head");
92
- appendChild(headNode, script);
93
- }
94
- else {
95
- DOM = ast || parseFragment(html);
96
- appendChild(DOM, script);
97
- }
98
- //@ts-ignore
99
- DOM.childNodes.forEach((node) => node.attrs?.push({ name: "data-hmr", value: htmlIdMap.get(file) }));
100
- return serialize(DOM);
101
- }
102
- function randomText() {
103
- return Math.random().toString(32).slice(2);
104
- }
105
- function getHMRCode(file, id, src, build) {
106
- return `import { render, html, $, $$, setInsertDiffing } from "hydro-js";
107
- if (!window.eventsource${id}) {
108
- setInsertDiffing(true);
109
- window.eventsource${id} = new EventSource("/events");
110
- window.eventsource${id}.addEventListener("message", ({ data }) => {
111
- const dataObj = JSON.parse(data);
112
- const file = "${file}";
113
-
114
- if (file === dataObj.file && "html" in dataObj) {
115
- if (dataObj.html.startsWith('<!DOCTYPE html>') || dataObj.html.startsWith('<html')) {
116
- document.head.remove(); // Don't try to diff the head – just re-run the scripts
117
- render(html\`\${dataObj.html}\`, document.documentElement);
118
- } else {
119
- const hmrID = "${id}";
120
- const newHTML = html\`\${dataObj.html}\`;
121
- const hmrElems = Array.from(newHTML.childNodes);
122
- const hmrWheres = Array.from($$(\`[data-hmr="\${hmrID}"]\`))
123
- // render new elements for old elements. Then, remove rest old elements and add add new elements after the last old one
124
- hmrWheres.forEach((where, index) => {
125
- if (index < hmrElems.length) {
126
- render(hmrElems[index], where, false);
127
- } else {
128
- where.remove();
129
- }
130
- });
131
- for (let rest = hmrWheres.length; rest < hmrElems.length; rest++) {
132
- if (hmrWheres.length) {
133
- const template = document.createElement('template');
134
- hmrElems[hmrWheres.length - 1].after(template);
135
- render(hmrElems[rest], template, false);
136
- template.remove();
137
- } else {
138
- render(hmrElems[rest], false, false)
139
- }
140
- }
141
- }
142
-
143
- setTimeout(() => dispatchEvent(new Event("popstate")));
144
- } else if (dataObj.file.endsWith("css")) {
145
- updateElem("link");
146
- } else if (dataObj.file.endsWith("js")) {
147
- updateElem("script")
148
- }
149
-
150
- function updateElem(type) {
151
- const hmrId = "${id}";
152
- const noSrcFile = dataObj.file.replace(\`${src}/\`, '');
153
- const attr = type === "script" ? "src" : "href";
154
- const elem = $(\`[data-hmr="\${hmrId}"] \${type}[\${attr}^="\${noSrcFile}"]\`); // could be $(\`\${type}[data-hmr="\${hmrId}"][\${attr}^="\${noSrcFile}"]\`) ?
155
-
156
- if (elem) {
157
- const clone = document.createElement(type);
158
- for (const key of elem.getAttributeNames()) {
159
- clone.setAttribute(key, elem.getAttribute(key));
160
- }
161
- clone.setAttribute(attr, elem.getAttribute(attr) + "?v=" + String(Math.random().toFixed(4)).slice(2));
162
- render(clone, elem, false);
163
- }
164
- }
165
- });
166
- }
167
- `;
168
- }
package/output.JPG DELETED
Binary file