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/src/bundle.mts CHANGED
@@ -1,732 +1,394 @@
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
- }
19
+ fileCopy,
20
+ createDefaultServer,
21
+ getPostCSSConfig,
22
+ getBuildPath,
23
+ createDir,
24
+ bundleConfig,
25
+ serverSentEvents,
26
+ addHMRCode,
27
+ } from "./utils.mjs";
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
+ let 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|jsx?|tsx?)$/;
45
+ const execFilePromise = promisify(execFile);
46
+
47
+ if (bundleConfig.deletePrev) {
48
+ await rm(bundleConfig.build, { force: true, recursive: true });
49
+ }
111
50
 
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
- }
51
+ glob(`${bundleConfig.src}/**/*`, build);
123
52
 
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
- });
53
+ async function build(err: any, files: string[], firstRun = true) {
54
+ if (err) {
55
+ console.error(err);
56
+ process.exit(1);
156
57
  }
157
58
 
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
- 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
- });
59
+ if (isHMR && firstRun) {
60
+ fastify = await createDefaultServer(isSecure);
61
+ fastify.listen(bundleConfig.port);
62
+ console.log(`💻 Sever listening on port ${bundleConfig.port}.`);
214
63
  }
215
64
 
216
- // THE BUNDLE CODE
217
- // Glob all files and transform the code
218
- glob(`${SOURCE_FOLDER}/**/*.html`, {}, (err, files) => {
219
- errorHandler(err);
220
-
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
- });
65
+ for (const file of files) {
66
+ await createDir(file);
240
67
 
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;
68
+ if (!SUPPORTED_FILES.test(file)) {
69
+ if (handlerFile) {
70
+ const { stdout } = await execFilePromise("node", [handlerFile, file]);
71
+ if (String(stdout)) console.log("📋 Logging Handler: ", String(stdout));
72
+ } else {
73
+ await fileCopy(file);
74
+ }
246
75
  } 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);
76
+ if (file.endsWith(".html")) {
77
+ await writeInlineScripts(file);
78
+ } else if (file.endsWith(".css")) {
79
+ await minifyCSS(file, getBuildPath(file));
279
80
  } else {
280
- DOM = parseFragment(fileText);
81
+ inlineFiles.add(file);
281
82
  }
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
- );
83
+ }
308
84
  }
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
- });
85
+ await minifyCode();
86
+ for (const file of inlineFiles) {
87
+ if (INLINE_BUNDLE_FILE.test(file)) {
88
+ inlineFiles.delete(file);
89
+ await rm(file);
90
+ }
344
91
  }
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
- });
92
+ for (const file of files) {
93
+ if (file.endsWith(".html")) {
94
+ await minifyHTML(file, getBuildPath(file));
95
+ }
368
96
  }
369
97
 
370
- async function minifyHTML(filename: string, buildFilename: string) {
371
- let fileText = fs.readFileSync(filename, { encoding: "utf-8" });
98
+ console.log(
99
+ `🚀 Build finished in ${(performance.now() - timer).toFixed(2)}ms ✨`
100
+ );
372
101
 
373
- let DOM;
374
- if (fileText.includes("<!DOCTYPE html>") || fileText.includes("<html")) {
375
- DOM = parse(fileText);
376
- } else {
377
- DOM = parseFragment(fileText);
378
- }
102
+ if (isHMR && firstRun) {
103
+ console.log(`⌛ Waiting for file changes ...`);
379
104
 
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`
105
+ if (postcssFile) {
106
+ const postCSSWatcher = watch(postcssFile);
107
+ const tailwindCSSWatcher = watch(
108
+ postcssFile.replace("postcss", "tailwind")
109
+ ); // Assuming that the file ext is the same
110
+ const tsConfigWatcher = watch(
111
+ postcssFile.split("\\").slice(0, -1).join("\\") + "\\tsconfig.json"
392
112
  );
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
113
 
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,
114
+ const cssFiles = files.filter((file) => file.endsWith(".css"));
115
+ postCSSWatcher.on(
116
+ "change",
117
+ async () => await rebuildCSS(cssFiles, "postcss")
118
+ );
119
+ tailwindCSSWatcher.on(
120
+ "change",
121
+ async () => await rebuildCSS(cssFiles, "tailwind")
122
+ );
123
+ tsConfigWatcher.on("change", async () => {
124
+ timer = performance.now();
125
+ await build(null, files, false);
418
126
  });
419
- //@ts-ignore
420
- node.value = css;
421
127
  }
422
128
 
423
- fileText = serialize(DOM);
129
+ const watcher = watch(bundleConfig.src);
130
+ watcher.on("add", async (file) => {
131
+ file = String.raw`${file}`.replace(/\\/g, "/"); // glob and chokidar diff
132
+ if (files.includes(file) || INLINE_BUNDLE_FILE.test(file)) {
133
+ return;
134
+ }
424
135
 
425
- // Minify HTML
426
- fileText = await minify(fileText, {
427
- collapseWhitespace: true,
428
- removeComments: true,
429
- });
136
+ await rebuild(file);
430
137
 
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);
138
+ console.log(`⚡ added ${file} to the build`);
139
+ });
140
+ watcher.on("change", async (file) => {
141
+ if (INLINE_BUNDLE_FILE.test(file)) {
142
+ return;
143
+ }
144
+ file = String.raw`${file}`.replace(/\\/g, "/");
461
145
 
462
- taskEmitter.emit("done");
146
+ await rebuild(file);
463
147
 
464
- if (serverSentEvents) {
465
- serverSentEvents({
466
- html: addHMRCode(fileText, buildFilename),
467
- filename: buildFilename,
468
- });
148
+ console.log(`⚡ modified ${file} on the build`);
149
+ });
150
+ watcher.on("unlink", async (file) => {
151
+ if (INLINE_BUNDLE_FILE.test(file)) {
152
+ return;
469
153
  }
470
- }
471
- }
154
+ file = String.raw`${file}`.replace(/\\/g, "/");
472
155
 
473
- // Helper functions from here
474
- function createPostCSSConfig() {
475
- try {
476
- return postcssrc.sync({});
477
- } catch {
478
- return { plugins: [cssnano], options: {}, file: "" };
479
- }
480
- }
156
+ inlineFiles.delete(file);
157
+ const buildFile = getBuildPath(file)
158
+ .replace(".ts", ".js")
159
+ .replace(".jsx", ".js");
160
+ await rm(buildFile);
481
161
 
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
- }
162
+ const bfDir = buildFile.split("/").slice(0, -1).join("/");
163
+ const stats = await readdir(bfDir);
164
+ if (!stats.length) await rm(bfDir);
492
165
 
493
- async function rebuild(filename: string) {
494
- const [buildFilename] = getBuildNames(filename);
166
+ console.log(`⚡ deleted ${file} from the build`);
167
+ });
495
168
 
496
- if (/\.(j|t)sx?$/.test(filename)) {
497
- JSTSFiles.add(filename);
498
- } else if (filename.endsWith(".css")) {
499
- await minifyCSS(filename, buildFilename);
500
- }
169
+ async function rebuild(file: string) {
170
+ // Rebuild all CSS because a change in any file might need to trigger PostCSS zu rebuild(e.g. Tailwind CSS)
171
+ await rebuildCSS(files.filter((file) => file.endsWith(".css")));
501
172
 
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);
173
+ let html;
174
+ if (file.endsWith(".html")) {
175
+ // To refill the inlineFiles needed to build JS
176
+ for (const htmlFile of files.filter((file) => file.endsWith(".html"))) {
177
+ await writeInlineScripts(htmlFile);
515
178
  }
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 {}
179
+ await minifyCode();
180
+ for (const file of inlineFiles) {
181
+ if (INLINE_BUNDLE_FILE.test(file)) {
182
+ inlineFiles.delete(file);
183
+ await rm(file);
184
+ }
526
185
  }
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}\`;
186
+ html = await minifyHTML(file, getBuildPath(file));
187
+ } else if (/\.(jsx?|tsx?)$/.test(file)) {
188
+ inlineFiles.add(file);
189
+ await minifyCode();
552
190
  } else {
553
- newHTML = html\`<body>\${dataObj.html}</body>\`
554
- isBody = true
191
+ const { stdout } = await execFilePromise("node", [handlerFile, file]);
192
+ if (String(stdout)) console.log("📋 Logging Handler: ", String(stdout));
555
193
  }
556
194
 
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;
195
+ serverSentEvents?.({ file, html });
196
+ }
197
+ }
198
+ }
583
199
 
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]);
200
+ async function minifyCSS(file: string, buildFile: string) {
201
+ try {
202
+ const fileText = await readFile(file, { encoding: "utf-8" });
203
+ const result = await CSSprocessor.process(fileText, {
204
+ ...options,
205
+ from: file,
206
+ to: buildFile,
207
+ });
208
+ await writeFile(buildFile, result.css);
209
+ } catch (err) {
210
+ console.error(err);
211
+ }
212
+ }
213
+
214
+ async function minifyCode(): Promise<unknown> {
215
+ try {
216
+ return await esbuild.build({
217
+ entryPoints: Array.from(inlineFiles),
218
+ charset: "utf8",
219
+ format: "esm",
220
+ incremental: isHMR,
221
+ sourcemap: isHMR,
222
+ splitting: true,
223
+ define: {
224
+ "process.env.NODE_ENV": `"${process.env.NODE_ENV}"`,
225
+ },
226
+ loader: { ".js": "jsx", ".ts": "tsx" },
227
+ bundle: true,
228
+ minify: true,
229
+ outdir: bundleConfig.build,
230
+ outbase: bundleConfig.src,
231
+ ...bundleConfig.esbuild,
232
+ });
233
+ // Stop app from crashing.
234
+ } catch (err: any) {
235
+ if (!isHMR) {
236
+ console.error(err);
597
237
  }
598
238
 
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));
239
+ let missingPkg = false;
240
+ if (err?.errors) {
241
+ for (const error of err.errors) {
242
+ if (error.text?.startsWith("Could not resolve")) {
243
+ missingPkg = true;
244
+ const packageNameRegex = /(?<=").*(?=")/;
245
+ const [pkgName] = error.text.match(packageNameRegex);
246
+ console.log(`📦 Package ${pkgName} was installed for you`);
247
+
248
+ await awaitSpawn(process.platform === "win32" ? "npm.cmd" : "npm", [
249
+ "install",
250
+ pkgName,
251
+ ]);
606
252
  }
607
253
  }
608
- };
609
- });
610
254
 
611
- if (!window.updateCSS) {
612
- window.updateCSS = function updateCSS(updateAttr) {
613
- $$('link').forEach(updateAttr("href"));
614
- window.fnToLastCalled.set(window.updateCSS, performance.now())
255
+ if (missingPkg) {
256
+ missingPkg = false;
257
+ return minifyCode();
258
+ }
615
259
  }
616
260
  }
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
- }
261
+ }
262
+
263
+ const htmlFilesCache = new Map();
264
+ async function writeInlineScripts(file: string) {
265
+ let fileText = await readFile(file, { encoding: "utf-8" });
266
+
267
+ let DOM;
268
+ if (fileText.includes("<!DOCTYPE html>") || fileText.includes("<html")) {
269
+ DOM = parse(fileText);
270
+ } else {
271
+ DOM = parseFragment(fileText);
624
272
  }
625
- if (!window.fnToLastCalled) {
626
- window.fnToLastCalled = new Map();
273
+
274
+ if (isHMR) {
275
+ fileText = addHMRCode(fileText, file, DOM);
627
276
  }
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
- }
277
+ htmlFilesCache.set(file, [fileText, DOM]);
278
+
279
+ const scripts = findElements(DOM, (e) => getTagName(e) === "script");
280
+ for (let index = 0; index < scripts.length; index++) {
281
+ const script = scripts[index];
282
+ const scriptTextNode = script.childNodes[0] as TextNode;
283
+ const isReferencedScript = script.attrs.find((a) => a.name === "src");
284
+ const scriptContent = scriptTextNode?.value;
285
+ if (!scriptContent || isReferencedScript) continue;
286
+
287
+ const jsFile = file.replace(".html", `-bundle-${index}.tsx`);
288
+ inlineFiles.add(jsFile);
289
+ await writeFile(jsFile, scriptContent);
634
290
  }
635
- }`;
291
+ }
636
292
 
637
- function randomText() {
638
- return Math.random().toString(32).slice(2);
639
- }
293
+ async function minifyHTML(file: string, buildFile: string) {
294
+ let fileText, DOM;
640
295
 
641
- const htmlIdMap = new Map();
642
- function addHMRCode(html: string, filename: string) {
643
- if (!htmlIdMap.has(filename)) {
644
- htmlIdMap.set(filename, randomText());
645
- }
296
+ if (htmlFilesCache.has(file)) {
297
+ const cache = htmlFilesCache.get(file);
298
+ fileText = cache[0];
299
+ DOM = cache[1];
300
+ } else {
301
+ fileText = await readFile(file, { encoding: "utf-8" });
646
302
 
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);
303
+ if (fileText.includes("<!DOCTYPE html>") || fileText.includes("<html")) {
304
+ DOM = parse(fileText);
656
305
  } 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
- );
306
+ DOM = parseFragment(fileText);
663
307
  }
308
+ }
664
309
 
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
- });
310
+ // Minify Code
311
+ const scripts = findElements(DOM, (e) => getTagName(e) === "script");
312
+ for (let index = 0; index < scripts.length; index++) {
313
+ const script = scripts[index];
314
+ const scriptTextNode = script.childNodes[0] as TextNode;
315
+ const isReferencedScript = script.attrs.find((a) => a.name === "src");
316
+ if (!scriptTextNode?.value || isReferencedScript) continue;
317
+
318
+ // Use bundled file
319
+ const buildInlineScript = buildFile.replace(".html", `-bundle-${index}.js`);
673
320
 
674
- return serialize(ast);
321
+ const scriptContent = await readFile(buildInlineScript, {
322
+ encoding: "utf-8",
323
+ });
324
+ await rm(buildInlineScript);
325
+ scriptTextNode.value = scriptContent.replace(
326
+ TEMPLATE_LITERAL_MINIFIER,
327
+ " "
328
+ );
675
329
  }
676
330
 
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();
331
+ // Minify Inline Style
332
+ const styles = findElements(DOM, (e) => getTagName(e) === "style");
333
+ for (const style of styles) {
334
+ const node = style.childNodes[0] as TextNode;
335
+ const styleContent = node?.value;
336
+ if (!styleContent) continue;
682
337
 
683
- if (endName!.endsWith("index.html")) {
684
- //@ts-ignore
685
- fastify.get(filePath.join("/") + "/", HMRHandler);
686
- }
687
- //@ts-ignore
688
- fastify.get(newFilename, HMRHandler);
338
+ const { css } = await CSSprocessor.process(styleContent, {
339
+ ...options,
340
+ from: undefined,
689
341
  });
342
+ node.value = css;
690
343
  }
691
344
 
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
- });
345
+ fileText = serialize(DOM);
701
346
 
702
- reply.header("Content-Type", "text/html; charset=UTF-8");
703
- return reply.send(addHMRCode(file, filename));
347
+ // Minify HTML
348
+ try {
349
+ fileText = await minify(fileText, {
350
+ collapseWhitespace: true,
351
+ removeComments: true,
352
+ ...bundleConfig["html-minifier-terser"],
353
+ });
354
+ } catch (e) {
355
+ console.error(e);
704
356
  }
705
357
 
706
- function errorHandler(err: Error | null) {
707
- if (err) {
358
+ if (!isCritical) {
359
+ await writeFile(buildFile, fileText);
360
+ return fileText;
361
+ } else {
362
+ const buildFileArr = buildFile.split("/");
363
+ const fileWithBase = buildFileArr.pop();
364
+ const buildDir = buildFileArr.join("/");
365
+
366
+ // critical is generating the files on the fs
367
+ try {
368
+ const { html } = await critical.generate({
369
+ base: buildDir,
370
+ html: fileText,
371
+ target: fileWithBase,
372
+ inline: !isSecure,
373
+ extract: true,
374
+ rebase: () => {},
375
+ ...bundleConfig.critical,
376
+ });
377
+ return html;
378
+ } catch (err) {
708
379
  console.error(err);
709
- process.exit(1);
710
380
  }
711
381
  }
382
+ }
712
383
 
713
- function tsMaybeX2JS(filename: string): string {
714
- return filename.replace(".ts", ".js").replace(".jsx", ".js");
384
+ async function rebuildCSS(files: string[], config?: string) {
385
+ const newConfig = await getPostCSSConfig();
386
+ plugins = newConfig.plugins;
387
+ options = newConfig.options;
388
+ CSSprocessor = postcss(plugins as AcceptedPlugin[]);
389
+ for (const file of files) {
390
+ await minifyCSS(file, getBuildPath(file));
715
391
  }
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
- });
392
+
393
+ if (config) console.log(`⚡ modified ${config}.config`);
732
394
  }