html-bundle 5.5.2 → 6.0.1

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/dist/bundle.mjs CHANGED
@@ -1,628 +1,341 @@
1
1
  #!/usr/bin/env node
2
- import fs from "fs";
3
2
  import { performance } from "perf_hooks";
4
- import Event from "events";
3
+ import { readFile, rm, writeFile, readdir } from "fs/promises";
4
+ import { execFile } from "child_process";
5
+ import { promisify } from "util";
5
6
  import glob from "glob";
6
- import path from "path";
7
- import Fastify from "fastify";
8
- import fastifyStatic from "fastify-static";
9
7
  import postcss from "postcss";
10
- import postcssrc from "postcss-load-config";
11
- import cssnano from "cssnano";
12
8
  import esbuild from "esbuild";
13
9
  import critical from "critical";
14
10
  import { minify } from "html-minifier-terser";
15
11
  import { watch } from "chokidar";
16
12
  import { serialize, parse, parseFragment } from "parse5";
17
- import { createScript, getTagName, appendChild, findElement, findElements, } from "@web/parse5-utils";
18
- // CLI and options
19
- const isCritical = process.argv.includes("--critical");
20
- const isHMR = process.argv.includes("--hmr");
21
- const isCSP = process.argv.includes("--csp");
22
- const isSecure = process.argv.includes("--secure");
23
- const isServeOnly = process.argv.includes("--serveOnly");
13
+ import { getTagName, findElements } from "@web/parse5-utils";
14
+ import awaitSpawn from "await-spawn";
15
+ import { fileCopy, createDefaultServer, getPostCSSConfig, getBuildPath, createDir, bundleConfig, serverSentEvents, addHMRCode, } from "./utils.mjs";
16
+ const isHMR = process.argv.includes("--hmr") || bundleConfig.hmr;
17
+ const isCritical = process.argv.includes("--critical") || bundleConfig.critical;
18
+ const isSecure = process.argv.includes("--secure") || bundleConfig.secure; // uses CSP for critical too
19
+ const handlerFile = process.argv.includes("--handler")
20
+ ? process.argv[process.argv.indexOf("--handler") + 1]
21
+ : bundleConfig.handler;
22
+ process.env.NODE_ENV = isHMR ? "development" : "production"; // just in case other tools are using it
23
+ let timer = performance.now();
24
+ let { plugins, options, file: postcssFile } = await getPostCSSConfig();
25
+ let CSSprocessor = postcss(plugins);
24
26
  let fastify;
25
- if (isServeOnly) {
26
- createDefaultServer();
27
- fastify.listen(5000);
28
- console.log(`💻 Sever listening on port 5000.`);
27
+ const inlineFiles = new Set();
28
+ const TEMPLATE_LITERAL_MINIFIER = /\n\s+/g;
29
+ const INLINE_BUNDLE_FILE = /-bundle-\d+.tsx$/;
30
+ const SUPPORTED_FILES = /\.(html|css|jsx?|tsx?)$/;
31
+ const execFilePromise = promisify(execFile);
32
+ if (bundleConfig.deletePrev) {
33
+ await rm(bundleConfig.build, { force: true, recursive: true });
29
34
  }
30
- else {
31
- process.env.NODE_ENV = isHMR ? "development" : "production";
32
- let { plugins, options, file } = createPostCSSConfig();
33
- let CSSprocessor = postcss(plugins);
34
- // Performance Observer and file watcher
35
- const globHTML = new Event.EventEmitter();
36
- const taskEmitter = new Event.EventEmitter();
37
- let start = performance.now();
38
- let addedHMR = false;
39
- let expectedTasks = 0; // This will be increased in globHandlers
40
- let finishedTasks = 0; // Current status
41
- taskEmitter.on("done", () => {
42
- finishedTasks++;
43
- if (finishedTasks === expectedTasks) {
44
- console.log(`🚀 Build finished in ${(performance.now() - start).toFixed(2)}ms ✨`);
45
- if (isHMR && !addedHMR) {
46
- addedHMR = true;
47
- if (file) {
48
- const postCSSWatcher = watch(file);
49
- const tailwindCSSWatcher = watch(file.replace("postcss", "tailwind"));
50
- postCSSWatcher.on("change", () => rebuildCSS("postcss"));
51
- tailwindCSSWatcher.on("change", () => rebuildCSS("tailwind"));
52
- }
53
- console.log(`⌛ Waiting for file changes ...`);
54
- const watcher = watch(SOURCE_FOLDER);
55
- // The add watcher will add all the files initially - do not rebuild them
56
- let initialAdd = 0;
57
- let hasJSTS = false;
58
- watcher.on("add", (filename) => {
59
- start = performance.now();
60
- rebuildCSS();
61
- // Return if it was added by the build system itself
62
- if (/-bundle-\d+\.(j|t)sx?$/.test(filename)) {
63
- return;
64
- }
65
- filename = String.raw `${filename}`.replace(/\\/g, "/");
66
- if (filename.endsWith(".html") || filename.endsWith(".css")) {
67
- initialAdd++;
68
- }
69
- else if (hasJSTS === false && /\.(j|t)sx?$/.test(filename)) {
70
- hasJSTS = true;
71
- initialAdd++;
72
- }
73
- if (initialAdd <= expectedTasks)
74
- return;
75
- const [buildFilename, buildPathDir] = getBuildNames(filename);
76
- fs.mkdir(buildPathDir, { recursive: true }, (err) => {
77
- errorHandler(err);
78
- rebuild(filename);
79
- console.log(`⚡ added ${buildFilename}`);
80
- });
81
- });
82
- watcher.on("change", (filename) => {
83
- start = performance.now();
84
- rebuildCSS();
85
- // Return if it was changed by the build system itself
86
- if (/-bundle-\d+\.(j|t)sx?$/.test(filename)) {
87
- return;
88
- }
89
- filename = String.raw `${filename}`.replace(/\\/g, "/");
90
- rebuild(filename);
91
- const [buildFilename] = getBuildNames(filename);
92
- console.log(`⚡ modified ${buildFilename}`);
93
- });
94
- watcher.on("unlink", (filename) => {
95
- start = performance.now();
96
- // Return if it was deleted by the build system itself
97
- if (/-bundle-\d+\.(j|t)sx?$/.test(filename)) {
98
- return;
99
- }
100
- filename = String.raw `${filename}`.replace(/\\/g, "/");
101
- JSTSFiles.delete(filename);
102
- const [buildFilename, buildPathDir] = getBuildNames(filename);
103
- fs.rm(tsMaybeX2JS(buildFilename), (err) => {
104
- errorHandler(err);
105
- console.log(`⚡ deleted ${buildFilename}`);
106
- const length = fs.readdirSync(buildPathDir).length;
107
- if (!length)
108
- fs.rmdir(buildPathDir, errorHandler);
109
- });
110
- });
35
+ glob(`${bundleConfig.src}/**/*`, build);
36
+ async function build(err, files, firstRun = true) {
37
+ if (err) {
38
+ console.error(err);
39
+ process.exit(1);
40
+ }
41
+ if (isHMR && firstRun) {
42
+ fastify = await createDefaultServer(isSecure);
43
+ fastify.listen(bundleConfig.port);
44
+ console.log(`💻 Sever listening on port ${bundleConfig.port}.`);
45
+ }
46
+ for (const file of files) {
47
+ await createDir(file);
48
+ if (!SUPPORTED_FILES.test(file)) {
49
+ if (handlerFile) {
50
+ const { stdout } = await execFilePromise("node", [handlerFile, file]);
51
+ if (String(stdout))
52
+ console.log("📋 Logging Handler: ", String(stdout));
53
+ }
54
+ else {
55
+ await fileCopy(file);
111
56
  }
112
57
  }
113
- });
114
- function rebuildCSS(config) {
115
- start = performance.now();
116
- if (config)
117
- console.log(`⚡ modified ${config}.config – CSS will rebuild now.`);
118
- const newConfig = createPostCSSConfig();
119
- plugins = newConfig.plugins;
120
- options = newConfig.options;
121
- CSSprocessor = postcss(plugins);
122
- glob(`${SOURCE_FOLDER}/**/*.css`, {}, (err, files) => {
123
- errorHandler(err);
124
- expectedTasks += files.length;
125
- for (const filename of files) {
126
- const [buildFilename, buildPathDir] = getBuildNames(filename);
127
- fs.mkdirSync(buildPathDir, { recursive: true });
128
- minifyCSS(filename, buildFilename);
58
+ else {
59
+ if (file.endsWith(".html")) {
60
+ await writeInlineScripts(file);
129
61
  }
130
- });
131
- }
132
- // Basic configuration
133
- const SOURCE_FOLDER = "src";
134
- const BUILD_FOLDER = "build";
135
- const TEMPLATE_LITERAL_MINIFIER = /\n\s+/g;
136
- const CONNECTIONS = []; // HMR
137
- let htmlTasks = 0;
138
- let serverSentEvents;
139
- if (isHMR) {
140
- fastify = Fastify(isSecure
141
- ? {
142
- http2: true,
143
- https: {
144
- key: fs.readFileSync(path.join(process.cwd(), "localhost-key.pem")),
145
- cert: fs.readFileSync(path.join(process.cwd(), "localhost.pem")),
146
- },
62
+ else if (file.endsWith(".css")) {
63
+ await minifyCSS(file, getBuildPath(file));
64
+ }
65
+ else {
66
+ inlineFiles.add(file);
147
67
  }
148
- : void 0);
149
- fastify.setNotFoundHandler((_req, reply) => {
150
- const file = fs.readFileSync(path.join(process.cwd(), BUILD_FOLDER, "/index.html"), {
151
- encoding: "utf-8",
152
- });
153
- reply.header("Content-Type", "text/html; charset=UTF-8");
154
- return reply.send(addHMRCode(file, "build/index.html"));
155
- });
156
- fastify.register(fastifyStatic, {
157
- root: path.join(process.cwd(), BUILD_FOLDER),
158
- });
159
- fastify.get("/events", (_req, reply) => {
160
- reply.raw.setHeader("Content-Type", "text/event-stream");
161
- reply.raw.setHeader("Cache-Control", "no-cache");
162
- !isSecure && reply.raw.setHeader("Connection", "keep-alive");
163
- CONNECTIONS.push(reply.raw);
164
- serverSentEvents = (data) => CONNECTIONS.forEach((rep) => {
165
- rep.write(`data: ${JSON.stringify(data)}\n\n`);
166
- });
167
- });
168
- }
169
- // THE BUNDLE CODE
170
- // Glob all files and transform the code
171
- glob(`${SOURCE_FOLDER}/**/*.html`, {}, (err, files) => {
172
- errorHandler(err);
173
- expectedTasks += files.length;
174
- htmlTasks += files.length;
175
- if (isHMR) {
176
- createHMRHandlers(files);
177
- fastify.listen(5000);
178
- console.log(`💻 Sever listening on port 5000.`);
179
- }
180
- });
181
- glob(`${SOURCE_FOLDER}/**/*.css`, {}, (err, files) => {
182
- errorHandler(err);
183
- expectedTasks += files.length;
184
- for (const filename of files) {
185
- const [buildFilename, buildPathDir] = getBuildNames(filename);
186
- fs.mkdirSync(buildPathDir, { recursive: true });
187
- minifyCSS(filename, buildFilename);
188
68
  }
189
- });
190
- const JSTSFiles = new Set();
191
- glob(`${SOURCE_FOLDER}/**/!(*.d).{ts,js,tsx,jsx}`, {}, (err, files) => {
192
- errorHandler(err);
193
- if (files.length) {
194
- expectedTasks += 1;
69
+ }
70
+ await minifyCode();
71
+ for (const file of inlineFiles) {
72
+ if (INLINE_BUNDLE_FILE.test(file)) {
73
+ inlineFiles.delete(file);
74
+ await rm(file);
195
75
  }
196
- else {
197
- globHTML.emit("getReady");
76
+ }
77
+ for (const file of files) {
78
+ if (file.endsWith(".html")) {
79
+ await minifyHTML(file, getBuildPath(file));
198
80
  }
199
- files.forEach((file) => JSTSFiles.add(file));
200
- minifyTSJS().catch(errorHandler);
201
- });
202
- globHTML.on("getReady", () => {
203
- if (expectedTasks - htmlTasks === finishedTasks) {
204
- // After CSS and JS because critical needs file built css files and inline script might reference js files.
205
- glob(`${SOURCE_FOLDER}/**/*.html`, {}, async (err, files) => {
206
- errorHandler(err);
207
- await createGlobalJS(files);
208
- files.forEach((filename) => {
209
- const [buildFilename, buildPathDir] = getBuildNames(filename);
210
- fs.mkdirSync(buildPathDir, { recursive: true });
211
- minifyHTML(filename, buildFilename);
212
- });
81
+ }
82
+ console.log(`🚀 Build finished in ${(performance.now() - timer).toFixed(2)}ms ✨`);
83
+ if (isHMR && firstRun) {
84
+ console.log(`⌛ Waiting for file changes ...`);
85
+ if (postcssFile) {
86
+ const postCSSWatcher = watch(postcssFile);
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");
89
+ const cssFiles = files.filter((file) => file.endsWith(".css"));
90
+ postCSSWatcher.on("change", async () => await rebuildCSS(cssFiles, "postcss"));
91
+ tailwindCSSWatcher.on("change", async () => await rebuildCSS(cssFiles, "tailwind"));
92
+ tsConfigWatcher.on("change", async () => {
93
+ timer = performance.now();
94
+ await build(null, files, false);
213
95
  });
214
96
  }
215
- });
216
- function createGlobalJS(files) {
217
- const scriptFilenames = [];
218
- files.forEach((filename) => {
219
- const fileText = fs.readFileSync(filename, { encoding: "utf-8" });
220
- let DOM;
221
- if (fileText.includes("<!DOCTYPE html>") || fileText.includes("<html")) {
222
- DOM = parse(fileText);
97
+ const watcher = watch(bundleConfig.src);
98
+ watcher.on("add", async (file) => {
99
+ file = String.raw `${file}`.replace(/\\/g, "/"); // glob and chokidar diff
100
+ if (files.includes(file) || INLINE_BUNDLE_FILE.test(file)) {
101
+ return;
223
102
  }
224
- else {
225
- DOM = parseFragment(fileText);
103
+ await rebuild(file);
104
+ console.log(`⚡ added ${file} to the build`);
105
+ });
106
+ watcher.on("change", async (file) => {
107
+ if (INLINE_BUNDLE_FILE.test(file)) {
108
+ return;
226
109
  }
227
- const scripts = findElements(DOM, (e) => getTagName(e) === "script");
228
- scripts.forEach((script, index) => {
229
- const scriptTextNode = script.childNodes[0];
230
- const isReferencedScript = script.attrs.find((a) => a.name === "src");
231
- //@ts-ignore
232
- const scriptContent = scriptTextNode?.value;
233
- if (!scriptContent || isReferencedScript)
234
- return;
235
- const jsFilename = filename.replace(".html", `-bundle-${index}.js`);
236
- scriptFilenames.push(jsFilename);
237
- fs.writeFileSync(jsFilename, scriptContent);
238
- });
110
+ file = String.raw `${file}`.replace(/\\/g, "/");
111
+ await rebuild(file);
112
+ console.log(`⚡ modified ${file} on the build`);
239
113
  });
240
- scriptFilenames.forEach((file) => JSTSFiles.add(file));
241
- return minifyTSJS(true)
242
- .catch(console.error)
243
- .finally(() => scriptFilenames.forEach((file) => {
244
- JSTSFiles.delete(file);
245
- try {
246
- fs.rmSync(file);
114
+ watcher.on("unlink", async (file) => {
115
+ if (INLINE_BUNDLE_FILE.test(file)) {
116
+ return;
117
+ }
118
+ file = String.raw `${file}`.replace(/\\/g, "/");
119
+ inlineFiles.delete(file);
120
+ const buildFile = getBuildPath(file)
121
+ .replace(".ts", ".js")
122
+ .replace(".jsx", ".js");
123
+ await rm(buildFile);
124
+ const bfDir = buildFile.split("/").slice(0, -1).join("/");
125
+ const stats = await readdir(bfDir);
126
+ if (!stats.length)
127
+ await rm(bfDir);
128
+ console.log(`⚡ deleted ${file} from the build`);
129
+ });
130
+ async function rebuild(file) {
131
+ // Rebuild all CSS because a change in any file might need to trigger PostCSS zu rebuild(e.g. Tailwind CSS)
132
+ await rebuildCSS(files.filter((file) => file.endsWith(".css")));
133
+ let html;
134
+ if (file.endsWith(".html")) {
135
+ // To refill the inlineFiles needed to build JS
136
+ for (const htmlFile of files.filter((file) => file.endsWith(".html"))) {
137
+ await writeInlineScripts(htmlFile);
138
+ }
139
+ await minifyCode();
140
+ for (const file of inlineFiles) {
141
+ if (INLINE_BUNDLE_FILE.test(file)) {
142
+ inlineFiles.delete(file);
143
+ await rm(file);
144
+ }
145
+ }
146
+ html = await minifyHTML(file, getBuildPath(file));
147
+ }
148
+ else if (/\.(jsx?|tsx?)$/.test(file)) {
149
+ inlineFiles.add(file);
150
+ await minifyCode();
151
+ }
152
+ else {
153
+ const { stdout } = await execFilePromise("node", [handlerFile, file]);
154
+ if (String(stdout))
155
+ console.log("📋 Logging Handler: ", String(stdout));
247
156
  }
248
- catch { }
249
- }));
157
+ serverSentEvents?.({ file, html });
158
+ }
159
+ }
160
+ }
161
+ async function minifyCSS(file, buildFile) {
162
+ try {
163
+ const fileText = await readFile(file, { encoding: "utf-8" });
164
+ const result = await CSSprocessor.process(fileText, {
165
+ ...options,
166
+ from: file,
167
+ to: buildFile,
168
+ });
169
+ await writeFile(buildFile, result.css);
250
170
  }
251
- function minifyTSJS(isInline = false, file) {
252
- return esbuild
253
- .build({
254
- entryPoints: Array.from(JSTSFiles),
171
+ catch (err) {
172
+ console.error(err);
173
+ }
174
+ }
175
+ async function minifyCode() {
176
+ try {
177
+ return await esbuild.build({
178
+ entryPoints: Array.from(inlineFiles),
255
179
  charset: "utf8",
256
180
  format: "esm",
257
181
  incremental: isHMR,
258
182
  sourcemap: isHMR,
259
183
  splitting: true,
260
184
  define: {
261
- "process.env.NODE_ENV": isHMR ? '"development"' : '"production"',
185
+ "process.env.NODE_ENV": `"${process.env.NODE_ENV}"`,
262
186
  },
263
187
  loader: { ".js": "jsx", ".ts": "tsx" },
264
188
  bundle: true,
265
189
  minify: true,
266
- outdir: BUILD_FOLDER,
267
- outbase: SOURCE_FOLDER,
268
- })
269
- .then(() => {
270
- if (!isInline) {
271
- taskEmitter.emit("done");
272
- globHTML.emit("getReady");
273
- if (serverSentEvents && file) {
274
- const changedFile = tsMaybeX2JS(file);
275
- const [buildFilename] = getBuildNames(changedFile);
276
- const js = fs.readFileSync(buildFilename, { encoding: "utf8" });
277
- serverSentEvents({
278
- js,
279
- filename: buildFilename.split(`${BUILD_FOLDER}/`).pop(),
280
- });
281
- }
282
- }
190
+ outdir: bundleConfig.build,
191
+ outbase: bundleConfig.src,
192
+ ...bundleConfig.esbuild,
283
193
  });
194
+ // Stop app from crashing.
284
195
  }
285
- function minifyCSS(filename, buildFilename) {
286
- const fileText = fs.readFileSync(filename, { encoding: "utf-8" });
287
- return CSSprocessor.process(fileText, {
288
- ...options,
289
- from: filename,
290
- to: buildFilename,
291
- })
292
- .then((result) => {
293
- fs.writeFileSync(buildFilename, result.css);
294
- taskEmitter.emit("done");
295
- globHTML.emit("getReady");
296
- if (serverSentEvents) {
297
- serverSentEvents({
298
- css: result.css,
299
- filename: buildFilename.split(`${BUILD_FOLDER}/`).pop(),
300
- });
301
- }
302
- })
303
- .catch((err) => {
196
+ catch (err) {
197
+ if (!isHMR) {
304
198
  console.error(err);
305
- });
306
- }
307
- async function minifyHTML(filename, buildFilename) {
308
- let fileText = fs.readFileSync(filename, { encoding: "utf-8" });
309
- let DOM;
310
- if (fileText.includes("<!DOCTYPE html>") || fileText.includes("<html")) {
311
- DOM = parse(fileText);
312
- }
313
- else {
314
- DOM = parseFragment(fileText);
315
- }
316
- // Minify Code
317
- const scripts = findElements(DOM, (e) => getTagName(e) === "script");
318
- scripts.forEach((script, index) => {
319
- const scriptTextNode = script.childNodes[0];
320
- const isReferencedScript = script.attrs.find((a) => a.name === "src");
321
- //@ts-ignore
322
- if (!scriptTextNode?.value || isReferencedScript)
323
- return;
324
- // Use bundled file and remove it from fs
325
- const bundledFilename = buildFilename.replace(".html", `-bundle-${index}.js`);
326
- try {
327
- const scriptContent = fs.readFileSync(bundledFilename, {
328
- encoding: "utf-8",
329
- });
330
- fs.rmSync(bundledFilename);
331
- // Replace src with bundled code
332
- //@ts-ignore
333
- scriptTextNode.value = scriptContent.replace(TEMPLATE_LITERAL_MINIFIER, " ");
334
- }
335
- catch { }
336
- });
337
- // Minify Inline Style
338
- const styles = findElements(DOM, (e) => getTagName(e) === "style");
339
- for (const style of styles) {
340
- const node = style.childNodes[0];
341
- //@ts-ignore
342
- const styleContent = node?.value;
343
- if (!styleContent)
344
- continue;
345
- const { css } = await CSSprocessor.process(styleContent, {
346
- ...options,
347
- from: undefined,
348
- });
349
- //@ts-ignore
350
- node.value = css;
351
199
  }
352
- fileText = serialize(DOM);
353
- // Minify HTML
354
- fileText = await minify(fileText, {
355
- collapseWhitespace: true,
356
- removeComments: true,
357
- });
358
- if (isCritical) {
359
- const buildFilenameArr = buildFilename.split("/");
360
- const fileWithBase = buildFilenameArr.pop();
361
- const buildDir = buildFilenameArr.join("/");
362
- // critical is generating the files on the fs
363
- return critical
364
- .generate({
365
- base: buildDir,
366
- html: fileText,
367
- target: fileWithBase,
368
- inline: !isCSP,
369
- extract: true,
370
- rebase: () => { },
371
- })
372
- .then(({ html }) => {
373
- taskEmitter.emit("done");
374
- if (serverSentEvents) {
375
- serverSentEvents({
376
- html: addHMRCode(html, buildFilename),
377
- filename: buildFilename,
378
- });
379
- }
380
- })
381
- .catch((err) => {
382
- console.error(err);
383
- });
384
- }
385
- else {
386
- fs.writeFileSync(buildFilename, fileText);
387
- taskEmitter.emit("done");
388
- if (serverSentEvents) {
389
- serverSentEvents({
390
- html: addHMRCode(fileText, buildFilename),
391
- filename: buildFilename,
392
- });
393
- }
394
- }
395
- }
396
- // Helper functions from here
397
- function createPostCSSConfig() {
398
- try {
399
- return postcssrc.sync({});
400
- }
401
- catch {
402
- return { plugins: [cssnano], options: {}, file: "" };
403
- }
404
- }
405
- function getBuildNames(filename) {
406
- const buildFilename = filename.replace(`${SOURCE_FOLDER}/`, `${BUILD_FOLDER}/`);
407
- const buildFilenameArr = buildFilename.split("/");
408
- buildFilenameArr.pop();
409
- const buildPathDir = buildFilenameArr.join("/");
410
- return [buildFilename, buildPathDir];
411
- }
412
- async function rebuild(filename) {
413
- const [buildFilename] = getBuildNames(filename);
414
- if (/\.(j|t)sx?$/.test(filename)) {
415
- JSTSFiles.add(filename);
416
- }
417
- else if (filename.endsWith(".css")) {
418
- await minifyCSS(filename, buildFilename);
419
- }
420
- glob(`${SOURCE_FOLDER}/**/*.html`, {}, async (err, files) => {
421
- errorHandler(err);
422
- await createGlobalJS(files);
423
- if (filename.endsWith(".html")) {
424
- minifyHTML(filename, buildFilename);
425
- }
426
- else if (/\.(j|t)sx?$/.test(filename) ||
427
- (filename.endsWith(".css") && isCritical)) {
428
- for (const htmlFilename of files) {
429
- const [htmlBuildFilename] = getBuildNames(htmlFilename);
430
- await minifyHTML(htmlFilename, htmlBuildFilename);
431
- }
432
- if (/\.(j|t)sx?$/.test(filename) && serverSentEvents) {
433
- const jsFile = tsMaybeX2JS(buildFilename);
434
- try {
435
- // Do not try to send empty files
436
- serverSentEvents({
437
- js: fs.readFileSync(jsFile, { encoding: "utf-8" }),
438
- filename: jsFile.split(`${BUILD_FOLDER}/`).pop(),
439
- });
440
- }
441
- catch { }
200
+ let missingPkg = false;
201
+ if (err?.errors) {
202
+ for (const error of err.errors) {
203
+ if (error.text?.startsWith("Could not resolve")) {
204
+ missingPkg = true;
205
+ const packageNameRegex = /(?<=").*(?=")/;
206
+ const [pkgName] = error.text.match(packageNameRegex);
207
+ console.log(`📦 Package ${pkgName} was installed for you`);
208
+ await awaitSpawn(process.platform === "win32" ? "npm.cmd" : "npm", [
209
+ "install",
210
+ pkgName,
211
+ ]);
442
212
  }
443
213
  }
444
- });
445
- }
446
- const getHMRCode = (filename, id) => `import { render, html, setShouldSetReactivity, $$, setGlobalSchedule, setInsertDiffing } from "https://unpkg.com/hydro-js/dist/library.js";
447
-
448
- if (!window.eventsource${id}) {
449
- setGlobalSchedule(false);
450
- setShouldSetReactivity(false);
451
-
452
- window.eventsource${id} = new EventSource("/events");
453
- window.eventsource${id}.addEventListener("message", ({ data }) => {
454
- const dataObj = JSON.parse(data);
455
-
456
- if ("html" in dataObj && "${filename}" === dataObj.filename) {
457
- setInsertDiffing(true);
458
-
459
- let newHTML;
460
- let isBody;
461
-
462
- if (dataObj.html.startsWith('<!DOCTYPE html>') || dataObj.html.startsWith('<html')) {
463
- newHTML = html\`\${dataObj.html}\`;
464
- } else {
465
- newHTML = html\`<body>\${dataObj.html}</body>\`
466
- isBody = true
467
- }
468
-
469
- if (isBody) {
470
- const hmrID = "${id}";
471
- const hmrElems = Array.from(newHTML.childNodes);
472
- const hmrWheres = Array.from($$(\`[data-hmr="\${hmrID}"]\`))
473
- // Render new Elements in old Elements, also remove rest old Elements and add add new elements after the last old one
474
- hmrWheres.forEach((where, index) => {
475
- if (index < hmrElems.length) {
476
- render(hmrElems[index], where);
477
- } else {
478
- where.remove();
214
+ if (missingPkg) {
215
+ missingPkg = false;
216
+ return minifyCode();
479
217
  }
480
- });
481
- for (let rest = hmrWheres.length; rest < hmrElems.length; rest++) {
482
- if (hmrWheres.length) {
483
- const template = document.createElement('template');
484
- hmrElems[hmrWheres.length - 1].after(template);
485
- render(hmrElems[rest], template);
486
- template.remove();
487
- } else {
488
- render(hmrElems[rest])
489
- }
490
- }
491
- } else {
492
- const oldElementCount = document.body.querySelectorAll('*').length;
493
- render(newHTML, document.documentElement);
494
- const newElementCount = document.body.querySelectorAll('*').length;
495
-
496
- // Looks like JS did not reload? Last resort - hard refresh
497
- if (newElementCount < 5 && Math.abs(newElementCount - oldElementCount) > 10) {
498
- location.reload()
499
218
  }
500
- }
501
- setInsertDiffing(false);
502
- if (dataObj.filename === 'build/index.html') {
503
- dispatchEvent(new Event("popstate"));
504
- }
505
- } else if ("css" in dataObj) {
506
- window.onceEveryXTime(100, window.updateCSS, [updateAttr]);
507
- } else if ("js" in dataObj) {
508
- window.onceEveryXTime(100, window.updateJS, [updateAttr]);
509
219
  }
510
-
511
- function updateAttr (attr, forceUpdate = false) {
512
- return (elem) => {
513
- const attrValue = elem[attr].replace(/\\?v=.*/, "");
514
- if (forceUpdate || attrValue.endsWith(dataObj.filename)) {
515
- elem[attr] = \`\${attrValue}?v=\${Math.random().toFixed(4)}\`;
516
- } else if (elem.localName === 'script' && !elem.src && new RegExp(\`["']\${dataObj.filename}["']\`).test(elem.textContent)) {
517
- elem.setAttribute('data-inline', String(Math.random().toFixed(4)).slice(2));
518
- }
519
- }
520
- };
521
- });
522
-
523
- if (!window.updateCSS) {
524
- window.updateCSS = function updateCSS(updateAttr) {
525
- $$('link').forEach(updateAttr("href"));
526
- window.fnToLastCalled.set(window.updateCSS, performance.now())
220
+ }
221
+ const htmlFilesCache = new Map();
222
+ async function writeInlineScripts(file) {
223
+ let fileText = await readFile(file, { encoding: "utf-8" });
224
+ let DOM;
225
+ if (fileText.includes("<!DOCTYPE html>") || fileText.includes("<html")) {
226
+ DOM = parse(fileText);
527
227
  }
528
- }
529
- if (!window.updateJS) {
530
- window.updateJS = function updateJS(updateAttr) {
531
- window.fnToLastCalled.set(window.updateJS, performance.now());
532
- const copy = html\`\${document.documentElement.outerHTML}\`;
533
- copy.querySelectorAll('script').forEach(updateAttr("src"));
534
- render(copy, document.documentElement);
228
+ else {
229
+ DOM = parseFragment(fileText);
535
230
  }
536
- }
537
- if (!window.fnToLastCalled) {
538
- window.fnToLastCalled = new Map();
539
- }
540
- if (!window.onceEveryXTime) {
541
- window.onceEveryXTime = function (time, fn, params) {
542
- if (!window.fnToLastCalled.has(fn) || performance.now() - window.fnToLastCalled.get(fn) > time) {
543
- fn(...params)
544
- }
231
+ if (isHMR) {
232
+ fileText = addHMRCode(fileText, file, DOM);
545
233
  }
546
- }
547
- }`;
548
- function randomText() {
549
- return Math.random().toString(32).slice(2);
234
+ htmlFilesCache.set(file, [fileText, DOM]);
235
+ const scripts = findElements(DOM, (e) => getTagName(e) === "script");
236
+ for (let index = 0; index < scripts.length; index++) {
237
+ const script = scripts[index];
238
+ const scriptTextNode = script.childNodes[0];
239
+ const isReferencedScript = script.attrs.find((a) => a.name === "src");
240
+ const scriptContent = scriptTextNode?.value;
241
+ if (!scriptContent || isReferencedScript)
242
+ continue;
243
+ const jsFile = file.replace(".html", `-bundle-${index}.tsx`);
244
+ inlineFiles.add(jsFile);
245
+ await writeFile(jsFile, scriptContent);
550
246
  }
551
- const htmlIdMap = new Map();
552
- function addHMRCode(html, filename) {
553
- if (!htmlIdMap.has(filename)) {
554
- htmlIdMap.set(filename, randomText());
555
- }
556
- const script = createScript({ type: "module" }, getHMRCode(filename, htmlIdMap.get(filename)));
557
- let ast;
558
- if (html.includes("<!DOCTYPE html>") || html.includes("<html")) {
559
- ast = parse(html);
560
- const headNode = findElement(ast, (e) => getTagName(e) === "head");
561
- appendChild(headNode, script);
247
+ }
248
+ async function minifyHTML(file, buildFile) {
249
+ let fileText, DOM;
250
+ if (htmlFilesCache.has(file)) {
251
+ const cache = htmlFilesCache.get(file);
252
+ fileText = cache[0];
253
+ DOM = cache[1];
254
+ }
255
+ else {
256
+ fileText = await readFile(file, { encoding: "utf-8" });
257
+ if (fileText.includes("<!DOCTYPE html>") || fileText.includes("<html")) {
258
+ DOM = parse(fileText);
562
259
  }
563
260
  else {
564
- ast = parseFragment(html);
565
- appendChild(ast, script);
566
- ast.childNodes.forEach((node) =>
567
- //@ts-ignore
568
- node.attrs?.push({ name: "data-hmr", value: htmlIdMap.get(filename) }));
261
+ DOM = parseFragment(fileText);
569
262
  }
570
- // Burst CSS cache
571
- findElements(ast, (e) => getTagName(e) === "link").forEach((link) => {
572
- const href = link.attrs.find((attr) => attr.name === "href");
573
- const rel = link.attrs.find((attr) => attr.name === "rel");
574
- if (rel.value === "stylesheet") {
575
- href.value += `?v=${Math.random().toFixed(4)}`;
576
- }
263
+ }
264
+ // Minify Code
265
+ const scripts = findElements(DOM, (e) => getTagName(e) === "script");
266
+ for (let index = 0; index < scripts.length; index++) {
267
+ const script = scripts[index];
268
+ const scriptTextNode = script.childNodes[0];
269
+ const isReferencedScript = script.attrs.find((a) => a.name === "src");
270
+ if (!scriptTextNode?.value || isReferencedScript)
271
+ continue;
272
+ // Use bundled file
273
+ const buildInlineScript = buildFile.replace(".html", `-bundle-${index}.js`);
274
+ const scriptContent = await readFile(buildInlineScript, {
275
+ encoding: "utf-8",
577
276
  });
578
- return serialize(ast);
277
+ await rm(buildInlineScript);
278
+ scriptTextNode.value = scriptContent.replace(TEMPLATE_LITERAL_MINIFIER, " ");
579
279
  }
580
- function createHMRHandlers(files) {
581
- files.forEach((filename) => {
582
- const newFilename = "/" + filename.replace(/src\//, "");
583
- const filePath = newFilename.split("/");
584
- const endName = filePath.pop();
585
- if (endName.endsWith("index.html")) {
586
- //@ts-ignore
587
- fastify.get(filePath.join("/") + "/", HMRHandler);
588
- }
589
- //@ts-ignore
590
- fastify.get(newFilename, HMRHandler);
280
+ // Minify Inline Style
281
+ const styles = findElements(DOM, (e) => getTagName(e) === "style");
282
+ for (const style of styles) {
283
+ const node = style.childNodes[0];
284
+ const styleContent = node?.value;
285
+ if (!styleContent)
286
+ continue;
287
+ const { css } = await CSSprocessor.process(styleContent, {
288
+ ...options,
289
+ from: undefined,
591
290
  });
291
+ node.value = css;
592
292
  }
593
- function HMRHandler(request, reply) {
594
- let filename = request.url;
595
- if (filename.endsWith("/")) {
596
- filename += "index.html";
597
- }
598
- filename = BUILD_FOLDER + filename;
599
- const file = fs.readFileSync(filename, {
600
- encoding: "utf-8",
293
+ fileText = serialize(DOM);
294
+ // Minify HTML
295
+ try {
296
+ fileText = await minify(fileText, {
297
+ collapseWhitespace: true,
298
+ removeComments: true,
299
+ ...bundleConfig["html-minifier-terser"],
601
300
  });
602
- reply.header("Content-Type", "text/html; charset=UTF-8");
603
- return reply.send(addHMRCode(file, filename));
604
301
  }
605
- function errorHandler(err) {
606
- if (err) {
302
+ catch (e) {
303
+ console.error(e);
304
+ }
305
+ if (!isCritical) {
306
+ await writeFile(buildFile, fileText);
307
+ return fileText;
308
+ }
309
+ else {
310
+ const buildFileArr = buildFile.split("/");
311
+ const fileWithBase = buildFileArr.pop();
312
+ const buildDir = buildFileArr.join("/");
313
+ // critical is generating the files on the fs
314
+ try {
315
+ const { html } = await critical.generate({
316
+ base: buildDir,
317
+ html: fileText,
318
+ target: fileWithBase,
319
+ inline: !isSecure,
320
+ extract: true,
321
+ rebase: () => { },
322
+ ...bundleConfig.critical,
323
+ });
324
+ return html;
325
+ }
326
+ catch (err) {
607
327
  console.error(err);
608
- process.exit(1);
609
328
  }
610
329
  }
611
- function tsMaybeX2JS(filename) {
612
- return filename.replace(".ts", ".js").replace(".jsx", ".js");
613
- }
614
330
  }
615
- function createDefaultServer(BUILD_FOLDER = "build") {
616
- fastify = Fastify(isSecure
617
- ? {
618
- http2: true,
619
- https: {
620
- key: fs.readFileSync(path.join(process.cwd(), "localhost-key.pem")),
621
- cert: fs.readFileSync(path.join(process.cwd(), "localhost.pem")),
622
- },
623
- }
624
- : void 0);
625
- fastify.register(fastifyStatic, {
626
- root: path.join(process.cwd(), BUILD_FOLDER),
627
- });
331
+ async function rebuildCSS(files, config) {
332
+ const newConfig = await getPostCSSConfig();
333
+ plugins = newConfig.plugins;
334
+ options = newConfig.options;
335
+ CSSprocessor = postcss(plugins);
336
+ for (const file of files) {
337
+ await minifyCSS(file, getBuildPath(file));
338
+ }
339
+ if (config)
340
+ console.log(`⚡ modified ${config}.config`);
628
341
  }