html-bundle 5.5.0 → 6.0.1-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/README.md +30 -21
- package/dist/bundle.mjs +327 -0
- package/dist/utils.js +168 -0
- package/dist/utils.mjs +166 -0
- package/package.json +3 -2
- package/src/bundle.mts +381 -0
- package/src/index.d.ts +3 -0
- package/src/utils.mts +203 -0
- package/dist/bundle.js +0 -625
- package/src/bundle.ts +0 -732
package/src/bundle.ts
DELETED
|
@@ -1,732 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import { performance } from "perf_hooks";
|
|
5
|
-
import Event from "events";
|
|
6
|
-
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";
|
|
17
|
-
import esbuild from "esbuild";
|
|
18
|
-
import critical from "critical";
|
|
19
|
-
import { minify } from "html-minifier-terser";
|
|
20
|
-
import { watch } from "chokidar";
|
|
21
|
-
import { serialize, parse, parseFragment } from "parse5";
|
|
22
|
-
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
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
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
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
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
|
-
});
|
|
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;
|
|
246
|
-
} 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);
|
|
279
|
-
} else {
|
|
280
|
-
DOM = parseFragment(fileText);
|
|
281
|
-
}
|
|
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
|
-
);
|
|
308
|
-
}
|
|
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
|
-
});
|
|
344
|
-
}
|
|
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
|
-
});
|
|
368
|
-
}
|
|
369
|
-
|
|
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
|
-
}
|
|
379
|
-
|
|
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`
|
|
392
|
-
);
|
|
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
|
-
}
|
|
422
|
-
|
|
423
|
-
fileText = serialize(DOM);
|
|
424
|
-
|
|
425
|
-
// Minify HTML
|
|
426
|
-
fileText = await minify(fileText, {
|
|
427
|
-
collapseWhitespace: true,
|
|
428
|
-
removeComments: true,
|
|
429
|
-
});
|
|
430
|
-
|
|
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);
|
|
461
|
-
|
|
462
|
-
taskEmitter.emit("done");
|
|
463
|
-
|
|
464
|
-
if (serverSentEvents) {
|
|
465
|
-
serverSentEvents({
|
|
466
|
-
html: addHMRCode(fileText, buildFilename),
|
|
467
|
-
filename: buildFilename,
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Helper functions from here
|
|
474
|
-
function createPostCSSConfig() {
|
|
475
|
-
try {
|
|
476
|
-
return postcssrc.sync({});
|
|
477
|
-
} catch {
|
|
478
|
-
return { plugins: [cssnano], options: {}, file: "" };
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
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
|
-
}
|
|
492
|
-
|
|
493
|
-
async function rebuild(filename: string) {
|
|
494
|
-
const [buildFilename] = getBuildNames(filename);
|
|
495
|
-
|
|
496
|
-
if (/\.(j|t)sx?$/.test(filename)) {
|
|
497
|
-
JSTSFiles.add(filename);
|
|
498
|
-
} else if (filename.endsWith(".css")) {
|
|
499
|
-
await minifyCSS(filename, buildFilename);
|
|
500
|
-
}
|
|
501
|
-
|
|
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);
|
|
515
|
-
}
|
|
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 {}
|
|
526
|
-
}
|
|
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}\`;
|
|
552
|
-
} else {
|
|
553
|
-
newHTML = html\`<body>\${dataObj.html}</body>\`
|
|
554
|
-
isBody = true
|
|
555
|
-
}
|
|
556
|
-
|
|
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;
|
|
583
|
-
|
|
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]);
|
|
597
|
-
}
|
|
598
|
-
|
|
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));
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
};
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
if (!window.updateCSS) {
|
|
612
|
-
window.updateCSS = function updateCSS(updateAttr) {
|
|
613
|
-
$$('link').forEach(updateAttr("href"));
|
|
614
|
-
window.fnToLastCalled.set(window.updateCSS, performance.now())
|
|
615
|
-
}
|
|
616
|
-
}
|
|
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
|
-
}
|
|
624
|
-
}
|
|
625
|
-
if (!window.fnToLastCalled) {
|
|
626
|
-
window.fnToLastCalled = new Map();
|
|
627
|
-
}
|
|
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
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}`;
|
|
636
|
-
|
|
637
|
-
function randomText() {
|
|
638
|
-
return Math.random().toString(32).slice(2);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
const htmlIdMap = new Map();
|
|
642
|
-
function addHMRCode(html: string, filename: string) {
|
|
643
|
-
if (!htmlIdMap.has(filename)) {
|
|
644
|
-
htmlIdMap.set(filename, randomText());
|
|
645
|
-
}
|
|
646
|
-
|
|
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);
|
|
656
|
-
} 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
|
-
);
|
|
663
|
-
}
|
|
664
|
-
|
|
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
|
-
});
|
|
673
|
-
|
|
674
|
-
return serialize(ast);
|
|
675
|
-
}
|
|
676
|
-
|
|
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();
|
|
682
|
-
|
|
683
|
-
if (endName!.endsWith("index.html")) {
|
|
684
|
-
//@ts-ignore
|
|
685
|
-
fastify.get(filePath.join("/") + "/", HMRHandler);
|
|
686
|
-
}
|
|
687
|
-
//@ts-ignore
|
|
688
|
-
fastify.get(newFilename, HMRHandler);
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
|
|
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
|
-
});
|
|
701
|
-
|
|
702
|
-
reply.header("Content-Type", "text/html; charset=UTF-8");
|
|
703
|
-
return reply.send(addHMRCode(file, filename));
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function errorHandler(err: Error | null) {
|
|
707
|
-
if (err) {
|
|
708
|
-
console.error(err);
|
|
709
|
-
process.exit(1);
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function tsMaybeX2JS(filename: string): string {
|
|
714
|
-
return filename.replace(".ts", ".js").replace(".jsx", ".js");
|
|
715
|
-
}
|
|
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
|
-
});
|
|
732
|
-
}
|