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/dist/bundle.mjs CHANGED
@@ -1,628 +1,327 @@
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.js";
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
+ const 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.`);
29
- }
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
- });
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|m?jsx?|m?tsx?)$/;
31
+ const execFilePromise = promisify(execFile);
32
+ await rm(bundleConfig.build, { force: true, recursive: true });
33
+ glob(`${bundleConfig.src}/**/*.*`, build);
34
+ async function build(err, files) {
35
+ if (err) {
36
+ console.error(err);
37
+ process.exit(1);
38
+ }
39
+ if (isHMR) {
40
+ fastify = await createDefaultServer(isSecure);
41
+ fastify.listen(bundleConfig.port);
42
+ console.log(`💻 Sever listening on port ${bundleConfig.port}.`);
43
+ }
44
+ for (const file of files) {
45
+ await createDir(file);
46
+ if (!SUPPORTED_FILES.test(file)) {
47
+ if (handlerFile) {
48
+ const { stdout } = await execFilePromise("node", [handlerFile, file]);
49
+ console.log("📋 Logging Handler: ", String(stdout));
50
+ }
51
+ else {
52
+ await fileCopy(file);
111
53
  }
112
54
  }
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);
55
+ else {
56
+ if (file.endsWith(".html")) {
57
+ await writeInlineScripts(file);
129
58
  }
130
- });
59
+ else if (file.endsWith(".css")) {
60
+ await minifyCSS(file, getBuildPath(file));
61
+ }
62
+ else {
63
+ inlineFiles.add(file);
64
+ }
65
+ }
66
+ }
67
+ await minifyCode();
68
+ for (const file of inlineFiles) {
69
+ if (INLINE_BUNDLE_FILE.test(file)) {
70
+ inlineFiles.delete(file);
71
+ await rm(file);
72
+ }
131
73
  }
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;
74
+ for (const file of files) {
75
+ if (file.endsWith(".html")) {
76
+ await minifyHTML(file, getBuildPath(file));
77
+ }
78
+ }
79
+ console.log(`🚀 Build finished in ${(performance.now() - timer).toFixed(2)}ms ✨`);
139
80
  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
- },
81
+ console.log(`⌛ Waiting for file changes ...`);
82
+ if (postcssFile) {
83
+ const postCSSWatcher = watch(postcssFile);
84
+ const tailwindCSSWatcher = watch(postcssFile.replace("postcss", "tailwind")); // Assuming that the file ext is the same
85
+ const cssFiles = files.filter((file) => file.endsWith(".css"));
86
+ postCSSWatcher.on("change", async () => await rebuildCSS(cssFiles, "postcss"));
87
+ tailwindCSSWatcher.on("change", async () => await rebuildCSS(cssFiles, "tailwind"));
88
+ }
89
+ const watcher = watch(bundleConfig.src);
90
+ let addCount = 0; // The add watcher will add all the files initially - do not rebuild them
91
+ watcher.on("add", async (file) => {
92
+ if (addCount++ <= files.length || INLINE_BUNDLE_FILE.test(file)) {
93
+ return;
147
94
  }
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"));
95
+ file = String.raw `${file}`.replace(/\\/g, "/"); // glob and chokidar diff
96
+ await rebuild(file);
97
+ console.log(`⚡ added ${file} to the build`);
155
98
  });
156
- fastify.register(fastifyStatic, {
157
- root: path.join(process.cwd(), BUILD_FOLDER),
99
+ watcher.on("change", async (file) => {
100
+ if (INLINE_BUNDLE_FILE.test(file)) {
101
+ return;
102
+ }
103
+ file = String.raw `${file}`.replace(/\\/g, "/");
104
+ await rebuild(file);
105
+ console.log(`⚡ modified ${file} on the build`);
158
106
  });
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
- });
107
+ watcher.on("unlink", async (file) => {
108
+ if (INLINE_BUNDLE_FILE.test(file)) {
109
+ return;
110
+ }
111
+ file = String.raw `${file}`.replace(/\\/g, "/");
112
+ inlineFiles.delete(file);
113
+ const buildFile = getBuildPath(file)
114
+ .replace(".ts", ".js")
115
+ .replace(".jsx", ".js");
116
+ await rm(buildFile);
117
+ const bfDir = buildFile.split("/").slice(0, -1).join("/");
118
+ const stats = await readdir(bfDir);
119
+ if (!stats.length)
120
+ await rm(bfDir);
121
+ console.log(`⚡ deleted ${file} from the build`);
167
122
  });
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
- }
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;
195
- }
196
- else {
197
- globHTML.emit("getReady");
198
- }
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
- });
213
- });
214
- }
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);
123
+ async function rebuild(file) {
124
+ // Rebuild all CSS because a change in any file might need to trigger PostCSS zu rebuild(e.g. Tailwind CSS)
125
+ await rebuildCSS(files.filter((file) => file.endsWith(".css")));
126
+ let html;
127
+ if (file.endsWith(".html")) {
128
+ // To refill the inlineFiles needed to build JS
129
+ for (const htmlFile of files.filter((file) => file.endsWith("html"))) {
130
+ await writeInlineScripts(htmlFile);
131
+ }
132
+ await minifyCode();
133
+ for (const file of inlineFiles) {
134
+ if (INLINE_BUNDLE_FILE.test(file)) {
135
+ inlineFiles.delete(file);
136
+ await rm(file);
137
+ }
138
+ }
139
+ html = await minifyHTML(file, getBuildPath(file));
140
+ }
141
+ else if (/(m?jsx?|m?tsx?)$/.test(file)) {
142
+ await minifyCode();
223
143
  }
224
144
  else {
225
- DOM = parseFragment(fileText);
145
+ const { stdout } = await execFilePromise("node", [handlerFile, file]);
146
+ console.log("📋 Logging Handler: ", String(stdout));
226
147
  }
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
- });
148
+ serverSentEvents({ file, html });
149
+ }
150
+ }
151
+ }
152
+ async function minifyCSS(file, buildFile) {
153
+ try {
154
+ const fileText = await readFile(file, { encoding: "utf-8" });
155
+ const result = await CSSprocessor.process(fileText, {
156
+ ...options,
157
+ from: file,
158
+ to: buildFile,
239
159
  });
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);
247
- }
248
- catch { }
249
- }));
160
+ await writeFile(buildFile, result.css);
250
161
  }
251
- function minifyTSJS(isInline = false, file) {
252
- return esbuild
253
- .build({
254
- entryPoints: Array.from(JSTSFiles),
162
+ catch (err) {
163
+ console.error(err);
164
+ }
165
+ }
166
+ async function minifyCode() {
167
+ try {
168
+ return await esbuild.build({
169
+ entryPoints: Array.from(inlineFiles),
255
170
  charset: "utf8",
256
171
  format: "esm",
257
172
  incremental: isHMR,
258
173
  sourcemap: isHMR,
259
174
  splitting: true,
260
175
  define: {
261
- "process.env.NODE_ENV": isHMR ? '"development"' : '"production"',
176
+ "process.env.NODE_ENV": process.env.NODE_ENV,
262
177
  },
263
178
  loader: { ".js": "jsx", ".ts": "tsx" },
264
179
  bundle: true,
265
180
  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
- }
181
+ outdir: bundleConfig.build,
182
+ outbase: bundleConfig.src,
183
+ ...bundleConfig.esbuild,
283
184
  });
185
+ // Stop app from crashing.
284
186
  }
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) => {
187
+ catch (err) {
188
+ if (!isHMR) {
304
189
  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
190
  }
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
- }
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 { }
191
+ let missingPkg = false;
192
+ if (err?.errors) {
193
+ for (const error of err.errors) {
194
+ if (error.text?.startsWith("Could not resolve")) {
195
+ missingPkg = true;
196
+ const packageNameRegex = /(?<=").*(?=")/;
197
+ const [pkgName] = error.text.match(packageNameRegex);
198
+ console.log(`📦 Package ${pkgName} was installed for you`);
199
+ await awaitSpawn(process.platform === "win32" ? "npm.cmd" : "npm", [
200
+ "install",
201
+ pkgName,
202
+ ]);
442
203
  }
443
204
  }
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();
205
+ if (missingPkg) {
206
+ missingPkg = false;
207
+ return minifyCode();
479
208
  }
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
209
  }
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
- }
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
210
  }
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())
211
+ }
212
+ const htmlFilesCache = new Map();
213
+ async function writeInlineScripts(file) {
214
+ let fileText = await readFile(file, { encoding: "utf-8" });
215
+ let DOM;
216
+ if (fileText.includes("<!DOCTYPE html>") || fileText.includes("<html")) {
217
+ DOM = parse(fileText);
218
+ }
219
+ else {
220
+ DOM = parseFragment(fileText);
527
221
  }
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);
222
+ if (isHMR) {
223
+ fileText = addHMRCode(fileText, file, DOM);
535
224
  }
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
- }
225
+ htmlFilesCache.set(file, [fileText, DOM]);
226
+ const scripts = findElements(DOM, (e) => getTagName(e) === "script");
227
+ for (let index = 0; index < scripts.length; index++) {
228
+ const script = scripts[index];
229
+ const scriptTextNode = script.childNodes[0];
230
+ const isReferencedScript = script.attrs.find((a) => a.name === "src");
231
+ const scriptContent = scriptTextNode?.value;
232
+ if (!scriptContent || isReferencedScript)
233
+ continue;
234
+ const jsFile = file.replace(".html", `-bundle-${index}.tsx`);
235
+ inlineFiles.add(jsFile);
236
+ await writeFile(jsFile, scriptContent);
545
237
  }
546
- }
547
- }`;
548
- function randomText() {
549
- return Math.random().toString(32).slice(2);
238
+ }
239
+ async function minifyHTML(file, buildFile) {
240
+ let fileText, DOM;
241
+ if (htmlFilesCache.has(file)) {
242
+ const cache = htmlFilesCache.get(file);
243
+ fileText = cache[0];
244
+ DOM = cache[1];
550
245
  }
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);
246
+ else {
247
+ fileText = await readFile(file, { encoding: "utf-8" });
248
+ if (fileText.includes("<!DOCTYPE html>") || fileText.includes("<html")) {
249
+ DOM = parse(fileText);
562
250
  }
563
251
  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) }));
252
+ DOM = parseFragment(fileText);
569
253
  }
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
- }
577
- });
578
- return serialize(ast);
579
254
  }
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);
255
+ // Minify Code
256
+ const scripts = findElements(DOM, (e) => getTagName(e) === "script");
257
+ for (let index = 0; index < scripts.length; index++) {
258
+ const script = scripts[index];
259
+ const scriptTextNode = script.childNodes[0];
260
+ const isReferencedScript = script.attrs.find((a) => a.name === "src");
261
+ if (!scriptTextNode?.value || isReferencedScript)
262
+ continue;
263
+ // Use bundled file
264
+ const buildInlineScript = buildFile.replace(".html", `-bundle-${index}.js`);
265
+ const scriptContent = await readFile(buildInlineScript, {
266
+ encoding: "utf-8",
591
267
  });
268
+ await rm(buildInlineScript);
269
+ scriptTextNode.value = scriptContent.replace(TEMPLATE_LITERAL_MINIFIER, " ");
592
270
  }
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",
271
+ // Minify Inline Style
272
+ const styles = findElements(DOM, (e) => getTagName(e) === "style");
273
+ for (const style of styles) {
274
+ const node = style.childNodes[0];
275
+ const styleContent = node?.value;
276
+ if (!styleContent)
277
+ continue;
278
+ const { css } = await CSSprocessor.process(styleContent, {
279
+ ...options,
280
+ from: undefined,
601
281
  });
602
- reply.header("Content-Type", "text/html; charset=UTF-8");
603
- return reply.send(addHMRCode(file, filename));
282
+ node.value = css;
283
+ }
284
+ fileText = serialize(DOM);
285
+ // Minify HTML
286
+ fileText = await minify(fileText, {
287
+ collapseWhitespace: true,
288
+ removeComments: true,
289
+ ...bundleConfig["html-minifier-terser"],
290
+ });
291
+ if (!isCritical) {
292
+ await writeFile(buildFile, fileText);
293
+ return fileText;
604
294
  }
605
- function errorHandler(err) {
606
- if (err) {
295
+ else {
296
+ const buildFileArr = buildFile.split("/");
297
+ const fileWithBase = buildFileArr.pop();
298
+ const buildDir = buildFileArr.join("/");
299
+ // critical is generating the files on the fs
300
+ try {
301
+ const { html } = await critical.generate({
302
+ base: buildDir,
303
+ html: fileText,
304
+ target: fileWithBase,
305
+ inline: !isSecure,
306
+ extract: true,
307
+ rebase: () => { },
308
+ ...bundleConfig.critical,
309
+ });
310
+ return html;
311
+ }
312
+ catch (err) {
607
313
  console.error(err);
608
- process.exit(1);
609
314
  }
610
315
  }
611
- function tsMaybeX2JS(filename) {
612
- return filename.replace(".ts", ".js").replace(".jsx", ".js");
613
- }
614
316
  }
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
- });
317
+ async function rebuildCSS(files, config) {
318
+ const newConfig = await getPostCSSConfig();
319
+ plugins = newConfig.plugins;
320
+ options = newConfig.options;
321
+ CSSprocessor = postcss(plugins);
322
+ for (const file of files) {
323
+ await minifyCSS(file, getBuildPath(file));
324
+ }
325
+ if (config)
326
+ console.log(`⚡ modified ${config}.config`);
628
327
  }