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 +22 -13
- package/dist/bundle.mjs +33 -18
- package/dist/{utils.js → utils.mjs} +52 -25
- package/logo.jpg +0 -0
- package/package.json +1 -1
- package/src/bundle.mts +32 -18
- package/src/{utils.ts → utils.mts} +49 -23
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
|
-
-
|
|
16
|
+
- 🚋 Watcher on PostCSS and Tailwind CSS and TS Config
|
|
13
17
|
- 🛡️ Almost no need to restart
|
|
14
18
|
|
|
15
|
-

|
|
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
|
|
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">
|
|
84
|
+
function Example({ name }) {
|
|
85
|
+
return <main id="app">Hi {name}</main>;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
|
|
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
|

|
|
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": "
|
|
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("
|
|
169
|
+
render(<Example />, document.getElementById("app"));
|
|
165
170
|
</script>
|
|
166
171
|
<body>
|
|
167
|
-
<div id="
|
|
172
|
+
<div id="app"></div>
|
|
168
173
|
</body>
|
|
169
174
|
</html>
|
|
170
175
|
```
|
|
176
|
+
|
|
177
|
+
### Demo
|
|
178
|
+
|
|
179
|
+

|
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.
|
|
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
|
-
|
|
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|
|
|
30
|
+
const SUPPORTED_FILES = /\.(html|css|jsx?|tsx?)$/;
|
|
31
31
|
const execFilePromise = promisify(execFile);
|
|
32
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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 {
|
|
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("/
|
|
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) =>
|
|
49
|
-
|
|
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.
|
|
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
|
|
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
|
|
106
|
-
return `import { render, html, $, $$,
|
|
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
|
-
|
|
109
|
-
window.eventsource${id}
|
|
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(
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
package/src/bundle.mts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
bundleConfig,
|
|
25
25
|
serverSentEvents,
|
|
26
26
|
addHMRCode,
|
|
27
|
-
} from "./utils.
|
|
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
|
-
|
|
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|
|
|
44
|
+
const SUPPORTED_FILES = /\.(html|css|jsx?|tsx?)$/;
|
|
45
45
|
const execFilePromise = promisify(execFile);
|
|
46
46
|
|
|
47
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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 {
|
|
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("/
|
|
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.
|
|
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
|
|
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
|
|
143
|
-
return `import { render, html, $, $$,
|
|
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
|
-
|
|
146
|
-
window.eventsource${id}
|
|
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(
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
`;
|