html-bundle 5.5.2 → 6.0.0-beta

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