astro-html-minifier-next 0.0.1 → 0.1.0
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/index.d.ts +27 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -32
- package/dist/index.js.map +1 -1
- package/dist/minify-html-file-worker-pool.d.ts +26 -0
- package/dist/minify-html-file-worker-pool.d.ts.map +1 -0
- package/dist/minify-html-file-worker-pool.js +89 -0
- package/dist/minify-html-file-worker-pool.js.map +1 -0
- package/dist/minify-html-file-worker.d.ts +2 -0
- package/dist/minify-html-file-worker.d.ts.map +1 -0
- package/dist/minify-html-file-worker.js +16 -0
- package/dist/minify-html-file-worker.js.map +1 -0
- package/dist/minify-html-file.d.ts +7 -0
- package/dist/minify-html-file.d.ts.map +1 -0
- package/dist/minify-html-file.js +22 -0
- package/dist/minify-html-file.js.map +1 -0
- package/package.json +5 -4
- package/src/index.ts +89 -40
- package/src/minify-html-file-worker-pool.ts +123 -0
- package/src/minify-html-file-worker.ts +29 -0
- package/src/minify-html-file.ts +38 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
import type { AstroIntegration } from "astro";
|
|
2
|
-
import {
|
|
2
|
+
import type { MinifierOptions as MinifyHTMLOptions } from "html-minifier-next";
|
|
3
|
+
export interface HTMLMinifierOptions extends MinifyHTMLOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Option specific to `astro-html-minifier-next` used to specify the maximum
|
|
6
|
+
* number of worker threads to spawn when minifying files.
|
|
7
|
+
* When set to `0`, `astro-html-minifier-next` will not create any worker
|
|
8
|
+
* threads and will do the minification in the main thread.
|
|
9
|
+
*
|
|
10
|
+
* Note: If unable to do a structured clone of the `html-minifier-next`
|
|
11
|
+
* options according to the
|
|
12
|
+
* [HTML structured clone
|
|
13
|
+
* algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm),
|
|
14
|
+
* `astro-html-minifier-next` will do the minification on the main
|
|
15
|
+
* thread, even if this option is not set to `0`.
|
|
16
|
+
*
|
|
17
|
+
* @default `Math.max(1, os.availableParallelism() - 1)`
|
|
18
|
+
*/
|
|
19
|
+
maxWorkers?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* An Astro integration that minifies HTML assets using
|
|
23
|
+
* [html-minifier-next](https://www.npmjs.com/package/html-minifier-next).
|
|
24
|
+
*
|
|
25
|
+
* @param options The options passed to the `minify` function of
|
|
26
|
+
* [html-minifier-next](https://www.npmjs.com/package/html-minifier-next).
|
|
27
|
+
* @returns The Astro integration.
|
|
28
|
+
*/
|
|
3
29
|
export default function htmlMinifier(options: HTMLMinifierOptions): AstroIntegration;
|
|
4
30
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAC9C,OAAO,KAAK,EAAE,eAAe,IAAI,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAI/E,MAAM,WAAW,mBAAoB,SAAQ,iBAAiB;IAC7D;;;;;;;;;;;;;;OAcG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAkBD;;;;;;;GAOG;AACH,MAAM,CAAC,OAAO,UAAU,YAAY,CACnC,OAAO,EAAE,mBAAmB,GAC1B,gBAAgB,CA6HlB"}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
2
1
|
import { availableParallelism as getAvailableParallelism } from "node:os";
|
|
3
2
|
import { relative as getRelativePath } from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
4
|
import { styleText } from "node:util";
|
|
6
|
-
import {
|
|
5
|
+
import { minifyHTMLFile } from "./minify-html-file.js";
|
|
6
|
+
import { MinifyHTMLFileWorkerPool } from "./minify-html-file-worker-pool.js";
|
|
7
|
+
/**
|
|
8
|
+
* Check if a value can be transferred to a worker.
|
|
9
|
+
* @param {*} value
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
function isTransferable(value) {
|
|
13
|
+
try {
|
|
14
|
+
// Attempt to do a structured clone of the value.
|
|
15
|
+
// If it succeeds, it should be transferable to a worker.
|
|
16
|
+
structuredClone(value);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* An Astro integration that minifies HTML assets using
|
|
25
|
+
* [html-minifier-next](https://www.npmjs.com/package/html-minifier-next).
|
|
26
|
+
*
|
|
27
|
+
* @param options The options passed to the `minify` function of
|
|
28
|
+
* [html-minifier-next](https://www.npmjs.com/package/html-minifier-next).
|
|
29
|
+
* @returns The Astro integration.
|
|
30
|
+
*/
|
|
7
31
|
export default function htmlMinifier(options) {
|
|
8
32
|
// API Reference: https://docs.astro.build/en/reference/integrations-reference/
|
|
9
33
|
return {
|
|
@@ -12,8 +36,15 @@ export default function htmlMinifier(options) {
|
|
|
12
36
|
"astro:build:done": async ({ logger, dir: distUrl, assets }) => {
|
|
13
37
|
logger.info(styleText(["bgGreen", "black"], " minifying html assets "));
|
|
14
38
|
const totalTimeStart = performance.now(); // --- TIMED BLOCK START ---
|
|
15
|
-
|
|
39
|
+
const availableParallelism = getAvailableParallelism();
|
|
40
|
+
const { maxWorkers = Math.max(1, availableParallelism - 1), ...minifyHTMLOptions } = options;
|
|
41
|
+
let workerPool;
|
|
42
|
+
if (maxWorkers > 0 && isTransferable(minifyHTMLOptions)) {
|
|
43
|
+
workerPool = new MinifyHTMLFileWorkerPool(maxWorkers, minifyHTMLOptions);
|
|
44
|
+
}
|
|
16
45
|
const tasks = [];
|
|
46
|
+
let tasksTotal = 0;
|
|
47
|
+
let tasksDone = 0;
|
|
17
48
|
const controller = new AbortController();
|
|
18
49
|
const signal = controller.signal;
|
|
19
50
|
const distPath = fileURLToPath(distUrl);
|
|
@@ -27,42 +58,30 @@ export default function htmlMinifier(options) {
|
|
|
27
58
|
const relativeAssetPath = getRelativePath(distPath, assetPath);
|
|
28
59
|
const logLineAssetPath = ` ${logLineArrow} /${relativeAssetPath} `;
|
|
29
60
|
tasks.push(async () => {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
signal,
|
|
34
|
-
});
|
|
35
|
-
const minifiedHtml = await minifyHtml(html, options);
|
|
36
|
-
const htmlSize = Buffer.byteLength(html);
|
|
37
|
-
const minifiedHtmlSize = Buffer.byteLength(minifiedHtml);
|
|
38
|
-
if (minifiedHtmlSize >= htmlSize) {
|
|
39
|
-
// No actual file size savings, so we skip writing the file or logging anything.
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
await writeFile(assetPath, minifiedHtml, {
|
|
43
|
-
encoding: "utf8",
|
|
44
|
-
signal,
|
|
45
|
-
});
|
|
46
|
-
const timeEnd = performance.now(); // --- TIMED BLOCK END ---
|
|
61
|
+
const { savings, time } = workerPool
|
|
62
|
+
? await workerPool.minifyHTMLFile(assetPath)
|
|
63
|
+
: await minifyHTMLFile(assetPath, minifyHTMLOptions, signal);
|
|
47
64
|
// Log a nice summary of the minification savings and the time it took.
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
65
|
+
const savingsSign = savings > 0 ? "-" : "+";
|
|
66
|
+
const savingsAbs = Math.abs(savings);
|
|
67
|
+
const savingsWithUnit = savingsAbs < 1024
|
|
68
|
+
? `${savingsAbs}B`
|
|
69
|
+
: savingsAbs < 1048576
|
|
70
|
+
? `${(savingsAbs / 1024).toFixed(1)}kB`
|
|
71
|
+
: `${(savingsAbs / 1048576).toFixed(2)}MB`;
|
|
72
|
+
const timeWithUnit = time < 1000
|
|
56
73
|
? `${Math.round(time)}ms`
|
|
57
74
|
: `${(time / 1000).toFixed(2)}s`;
|
|
58
75
|
logger.info(logLineAssetPath +
|
|
59
|
-
styleText("dim", `(
|
|
76
|
+
styleText(savings <= 0 ? "yellow" : "dim", `(${savingsSign}${savingsWithUnit}${savings <= 0 ? ", skipped" : ""}) `) +
|
|
77
|
+
styleText("dim", `(+${timeWithUnit}) (${++tasksDone}/${tasksTotal})`));
|
|
60
78
|
});
|
|
79
|
+
tasksTotal++;
|
|
61
80
|
}
|
|
62
81
|
}
|
|
63
|
-
// We
|
|
64
|
-
//
|
|
65
|
-
const maxExecutingTasksSize =
|
|
82
|
+
// We use a quadruple of the available parallelism here, even if we don't actually run the tasks in different threads or anything.
|
|
83
|
+
// The available parallelism is a good indicator of machine capabilities, and the multiplier gives a good balance of speed and resource usage.
|
|
84
|
+
const maxExecutingTasksSize = availableParallelism * 4;
|
|
66
85
|
// This holds the current batch of promises that are waiting to fulfill.
|
|
67
86
|
const executingTasks = new Set();
|
|
68
87
|
// Batch the tasks to avoid minifying too many files at once, which could lead to memory and performance issues.
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,IAAI,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,QAAQ,IAAI,eAAe,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAqB7E;;;;GAIG;AACH,SAAS,cAAc,CAAC,KAAc;IACrC,IAAI,CAAC;QACJ,iDAAiD;QACjD,yDAAyD;QACzD,eAAe,CAAC,KAAK,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC;IACb,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,KAAK,CAAC;IACd,CAAC;AACF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,OAAO,UAAU,YAAY,CACnC,OAA4B;IAE5B,+EAA+E;IAC/E,OAAO;QACN,IAAI,EAAE,0BAA0B;QAChC,KAAK,EAAE;YACN,kBAAkB,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE;gBAC9D,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,yBAAyB,CAAC,CAAC,CAAC;gBAExE,MAAM,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,4BAA4B;gBAEtE,MAAM,oBAAoB,GAAG,uBAAuB,EAAE,CAAC;gBACvD,MAAM,EACL,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,oBAAoB,GAAG,CAAC,CAAC,EAClD,GAAG,iBAAiB,EACpB,GAAG,OAAO,CAAC;gBAEZ,IAAI,UAAgD,CAAC;gBACrD,IAAI,UAAU,GAAG,CAAC,IAAI,cAAc,CAAC,iBAAiB,CAAC,EAAE,CAAC;oBACzD,UAAU,GAAG,IAAI,wBAAwB,CACxC,UAAU,EACV,iBAAiB,CACjB,CAAC;gBACH,CAAC;gBAED,MAAM,KAAK,GAA4B,EAAE,CAAC;gBAC1C,IAAI,UAAU,GAAG,CAAC,CAAC;gBACnB,IAAI,SAAS,GAAG,CAAC,CAAC;gBAElB,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;gBACzC,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;gBAEjC,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;gBACxC,MAAM,YAAY,GAAG,SAAS,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;gBAC7C,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;oBACzC,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;wBAClC,MAAM,SAAS,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;wBAC1C,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;4BAChD,SAAS;wBACV,CAAC;wBAED,MAAM,iBAAiB,GAAG,eAAe,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;wBAC/D,MAAM,gBAAgB,GAAG,KAAK,YAAY,KAAK,iBAAiB,GAAG,CAAC;wBACpE,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;4BACrB,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,UAAU;gCACnC,CAAC,CAAC,MAAM,UAAU,CAAC,cAAc,CAAC,SAAS,CAAC;gCAC5C,CAAC,CAAC,MAAM,cAAc,CAAC,SAAS,EAAE,iBAAiB,EAAE,MAAM,CAAC,CAAC;4BAE9D,uEAAuE;4BACvE,MAAM,WAAW,GAAG,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;4BAC5C,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;4BACrC,MAAM,eAAe,GACpB,UAAU,GAAG,IAAI;gCAChB,CAAC,CAAC,GAAG,UAAU,GAAG;gCAClB,CAAC,CAAC,UAAU,GAAG,OAAO;oCACrB,CAAC,CAAC,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;oCACvC,CAAC,CAAC,GAAG,CAAC,UAAU,GAAG,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;4BAC9C,MAAM,YAAY,GACjB,IAAI,GAAG,IAAI;gCACV,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI;gCACzB,CAAC,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;4BACnC,MAAM,CAAC,IAAI,CACV,gBAAgB;gCACf,SAAS,CACR,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,EAC/B,IAAI,WAAW,GAAG,eAAe,GAAG,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,IAAI,CACvE;gCACD,SAAS,CACR,KAAK,EACL,KAAK,YAAY,MAAM,EAAE,SAAS,IAAI,UAAU,GAAG,CACnD,CACF,CAAC;wBACH,CAAC,CAAC,CAAC;wBAEH,UAAU,EAAE,CAAC;oBACd,CAAC;gBACF,CAAC;gBAED,kIAAkI;gBAClI,8IAA8I;gBAC9I,MAAM,qBAAqB,GAAG,oBAAoB,GAAG,CAAC,CAAC;gBAEvD,wEAAwE;gBACxE,MAAM,cAAc,GAAG,IAAI,GAAG,EAAiB,CAAC;gBAEhD,gHAAgH;gBAChH,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;oBAC1B,MAAM,WAAW,GAAG,IAAI,EAAE;yBACxB,IAAI,CAAC,GAAG,EAAE;wBACV,cAAc,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;oBACpC,CAAC,CAAC;yBACD,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;wBACZ,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;4BACrB,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;wBACrB,CAAC;wBACD,MAAM,CAAC,CAAC;oBACT,CAAC,CAAC,CAAC;oBAEJ,cAAc,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;oBAEhC,IAAI,cAAc,CAAC,IAAI,IAAI,qBAAqB,EAAE,CAAC;wBAClD,8FAA8F;wBAC9F,kFAAkF;wBAClF,MAAM,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;oBACpC,CAAC;oBAED,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;wBACpB,MAAM,MAAM,CAAC,MAAM,CAAC;oBACrB,CAAC;gBACF,CAAC;gBAED,0CAA0C;gBAC1C,MAAM,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;gBAElC,MAAM,YAAY,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,0BAA0B;gBAElE,2CAA2C;gBAC3C,MAAM,SAAS,GAAG,YAAY,GAAG,cAAc,CAAC;gBAChD,MAAM,YAAY,GACjB,SAAS,GAAG,IAAI;oBACf,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI;oBAC9B,CAAC,CAAC,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;gBACxC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,kBAAkB,YAAY,GAAG,CAAC,CAAC,CAAC;YACpE,CAAC;SACD;KACD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
import type { MinifierOptions as MinifyHTMLOptions } from "html-minifier-next";
|
|
3
|
+
import type { MinifyHTMLFileResult } from "./minify-html-file.js";
|
|
4
|
+
interface MinifyHTMLFileWorker extends Worker {
|
|
5
|
+
_currentResolve?: (result: MinifyHTMLFileResult) => void;
|
|
6
|
+
_currentReject?: (error: unknown) => void;
|
|
7
|
+
}
|
|
8
|
+
export type MinifyHTMLWorkerInput = string;
|
|
9
|
+
export type MinifyHTMLWorkerOutput = MinifyHTMLFileResult | {
|
|
10
|
+
error: unknown;
|
|
11
|
+
};
|
|
12
|
+
export declare class MinifyHTMLFileWorkerPool {
|
|
13
|
+
protected maxWorkers: number;
|
|
14
|
+
protected minifyHTMLOptions: MinifyHTMLOptions;
|
|
15
|
+
protected workerUrl: URL;
|
|
16
|
+
protected pool: Set<Worker>;
|
|
17
|
+
protected idle: Worker[];
|
|
18
|
+
protected queue: ((value: Worker) => void)[];
|
|
19
|
+
constructor(maxWorkers: number, minifyHTMLOptions: MinifyHTMLOptions);
|
|
20
|
+
protected getAvailableWorker(): Promise<MinifyHTMLFileWorker>;
|
|
21
|
+
protected releaseWorker(worker: Worker): void;
|
|
22
|
+
protected removeWorker(worker: Worker): void;
|
|
23
|
+
minifyHTMLFile(htmlFile: string): Promise<MinifyHTMLFileResult>;
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=minify-html-file-worker-pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"minify-html-file-worker-pool.d.ts","sourceRoot":"","sources":["../src/minify-html-file-worker-pool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,KAAK,EAAE,eAAe,IAAI,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAElE,UAAU,oBAAqB,SAAQ,MAAM;IAC5C,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACzD,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC1C;AAED,MAAM,MAAM,qBAAqB,GAAG,MAAM,CAAC;AAC3C,MAAM,MAAM,sBAAsB,GAAG,oBAAoB,GAAG;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/E,qBAAa,wBAAwB;IACpC,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,iBAAiB,EAAE,iBAAiB,CAAC;IAE/C,SAAS,CAAC,SAAS,EAAE,GAAG,CAAC;IACzB,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5B,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IACzB,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC,EAAE,CAAC;gBAEjC,UAAU,EAAE,MAAM,EAAE,iBAAiB,EAAE,iBAAiB;cAUpD,kBAAkB,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAqDnE,SAAS,CAAC,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAc7C,SAAS,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAU/B,cAAc,CAC1B,QAAQ,EAAE,MAAM,GAEd,OAAO,CAAC,oBAAoB,CAAC;CAWhC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
export class MinifyHTMLFileWorkerPool {
|
|
3
|
+
maxWorkers;
|
|
4
|
+
minifyHTMLOptions;
|
|
5
|
+
workerUrl;
|
|
6
|
+
pool;
|
|
7
|
+
idle;
|
|
8
|
+
queue;
|
|
9
|
+
constructor(maxWorkers, minifyHTMLOptions) {
|
|
10
|
+
this.maxWorkers = maxWorkers;
|
|
11
|
+
this.minifyHTMLOptions = minifyHTMLOptions;
|
|
12
|
+
this.workerUrl = new URL("./minify-html-file-worker.js", import.meta.url);
|
|
13
|
+
this.pool = new Set();
|
|
14
|
+
this.idle = [];
|
|
15
|
+
this.queue = [];
|
|
16
|
+
}
|
|
17
|
+
async getAvailableWorker() {
|
|
18
|
+
// If there is an idle worker, use it.
|
|
19
|
+
if (this.idle.length) {
|
|
20
|
+
const worker = this.idle.shift();
|
|
21
|
+
if (worker !== undefined) {
|
|
22
|
+
return worker;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// If we can create a new worker, do so.
|
|
26
|
+
if (this.pool.size < this.maxWorkers) {
|
|
27
|
+
const worker = new Worker(this.workerUrl, {
|
|
28
|
+
workerData: this.minifyHTMLOptions,
|
|
29
|
+
});
|
|
30
|
+
worker.on("message", async (message) => {
|
|
31
|
+
if ("error" in message) {
|
|
32
|
+
worker._currentReject?.(message.error);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
worker._currentResolve?.(message);
|
|
36
|
+
}
|
|
37
|
+
worker._currentResolve = worker._currentReject = undefined;
|
|
38
|
+
this.releaseWorker(worker);
|
|
39
|
+
});
|
|
40
|
+
worker.on("error", (error) => {
|
|
41
|
+
worker._currentReject?.(error);
|
|
42
|
+
worker._currentResolve = worker._currentReject = undefined;
|
|
43
|
+
});
|
|
44
|
+
worker.on("exit", (exitCode) => {
|
|
45
|
+
this.removeWorker(worker);
|
|
46
|
+
if (exitCode !== 0) {
|
|
47
|
+
worker._currentReject?.(new Error(`Worker failed with exit code ${exitCode}.`));
|
|
48
|
+
worker._currentResolve = worker._currentReject = undefined;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
this.pool.add(worker);
|
|
52
|
+
return worker;
|
|
53
|
+
}
|
|
54
|
+
// Otherwise, wait for a worker to free up.
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
// When a worker frees up, they will check the queue and resolve this promise.
|
|
57
|
+
this.queue.push(resolve);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
releaseWorker(worker) {
|
|
61
|
+
// If there is a queued request for a worker, resolve it.
|
|
62
|
+
if (this.queue.length) {
|
|
63
|
+
const resolve = this.queue.shift();
|
|
64
|
+
if (resolve !== undefined) {
|
|
65
|
+
resolve(worker);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Otherwise, keep the worker as idle.
|
|
70
|
+
this.idle.push(worker);
|
|
71
|
+
}
|
|
72
|
+
removeWorker(worker) {
|
|
73
|
+
this.pool.delete(worker);
|
|
74
|
+
// If a worker is force stopped by the system, it might still be in the idle list.
|
|
75
|
+
const idleIndex = this.idle.indexOf(worker);
|
|
76
|
+
if (idleIndex !== -1) {
|
|
77
|
+
this.idle.splice(idleIndex, 1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async minifyHTMLFile(htmlFile) {
|
|
81
|
+
const worker = await this.getAvailableWorker();
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
worker._currentResolve = resolve;
|
|
84
|
+
worker._currentReject = reject;
|
|
85
|
+
worker.postMessage(htmlFile);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=minify-html-file-worker-pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"minify-html-file-worker-pool.js","sourceRoot":"","sources":["../src/minify-html-file-worker-pool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"minify-html-file-worker.d.ts","sourceRoot":"","sources":["../src/minify-html-file-worker.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { isMainThread, workerData as minifyHTMLOptions, parentPort, } from "node:worker_threads";
|
|
2
|
+
import { minifyHTMLFile } from "./minify-html-file.js";
|
|
3
|
+
if (isMainThread) {
|
|
4
|
+
throw new Error("Not a worker thread.");
|
|
5
|
+
}
|
|
6
|
+
// biome-ignore-start lint/style/noNonNullAssertion: I can assume `parentPort` is not null.
|
|
7
|
+
parentPort.on("message", async (htmlFile) => {
|
|
8
|
+
try {
|
|
9
|
+
parentPort.postMessage((await minifyHTMLFile(htmlFile, minifyHTMLOptions)));
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
parentPort.postMessage({ error });
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
// biome-ignore-end lint/style/noNonNullAssertion: See start.
|
|
16
|
+
//# sourceMappingURL=minify-html-file-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"minify-html-file-worker.js","sourceRoot":"","sources":["../src/minify-html-file-worker.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,YAAY,EACZ,UAAU,IAAI,iBAAiB,EAC/B,UAAU,GACV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAMvD,IAAI,YAAY,EAAE,CAAC;IAClB,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;AACzC,CAAC;AAED,2FAA2F;AAC3F,UAAW,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,QAA+B,EAAE,EAAE;IACnE,IAAI,CAAC;QACJ,UAAW,CAAC,WAAW,CACtB,CAAC,MAAM,cAAc,CACpB,QAAQ,EACR,iBAAiB,CACjB,CAAkC,CACnC,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,UAAW,CAAC,WAAW,CAAC,EAAE,KAAK,EAAmC,CAAC,CAAC;IACrE,CAAC;AACF,CAAC,CAAC,CAAC;AACH,6DAA6D"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type MinifierOptions as MinifyHTMLOptions } from "html-minifier-next";
|
|
2
|
+
export interface MinifyHTMLFileResult {
|
|
3
|
+
savings: number;
|
|
4
|
+
time: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function minifyHTMLFile(htmlFile: string, minifyHTMLOptions: MinifyHTMLOptions, signal?: AbortSignal): Promise<MinifyHTMLFileResult>;
|
|
7
|
+
//# sourceMappingURL=minify-html-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"minify-html-file.d.ts","sourceRoot":"","sources":["../src/minify-html-file.ts"],"names":[],"mappings":"AACA,OAAO,EACN,KAAK,eAAe,IAAI,iBAAiB,EAEzC,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,oBAAoB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACb;AAED,wBAAsB,cAAc,CACnC,QAAQ,EAAE,MAAM,EAChB,iBAAiB,EAAE,iBAAiB,EACpC,MAAM,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,oBAAoB,CAAC,CAsB/B"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { minify as minifyHTML, } from "html-minifier-next";
|
|
3
|
+
export async function minifyHTMLFile(htmlFile, minifyHTMLOptions, signal) {
|
|
4
|
+
const timeStart = performance.now(); // --- TIMED BLOCK START ---
|
|
5
|
+
const html = await readFile(htmlFile, {
|
|
6
|
+
encoding: "utf8",
|
|
7
|
+
signal,
|
|
8
|
+
});
|
|
9
|
+
const minifiedHTML = await minifyHTML(html, minifyHTMLOptions);
|
|
10
|
+
const savings = Buffer.byteLength(html) - Buffer.byteLength(minifiedHTML);
|
|
11
|
+
if (savings > 0) {
|
|
12
|
+
// Only write the minified HTML to the file if it's smaller.
|
|
13
|
+
await writeFile(htmlFile, minifiedHTML, {
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
signal,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const timeEnd = performance.now(); // --- TIMED BLOCK END ---
|
|
19
|
+
const time = timeEnd - timeStart;
|
|
20
|
+
return { savings, time };
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=minify-html-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"minify-html-file.js","sourceRoot":"","sources":["../src/minify-html-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAEN,MAAM,IAAI,UAAU,GACpB,MAAM,oBAAoB,CAAC;AAO5B,MAAM,CAAC,KAAK,UAAU,cAAc,CACnC,QAAgB,EAChB,iBAAoC,EACpC,MAAoB;IAEpB,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,4BAA4B;IAEjE,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE;QACrC,QAAQ,EAAE,MAAM;QAChB,MAAM;KACN,CAAC,CAAC;IACH,MAAM,YAAY,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;IAE/D,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;IAC1E,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QACjB,4DAA4D;QAC5D,MAAM,SAAS,CAAC,QAAQ,EAAE,YAAY,EAAE;YACvC,QAAQ,EAAE,MAAM;YAChB,MAAM;SACN,CAAC,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,0BAA0B;IAE7D,MAAM,IAAI,GAAG,OAAO,GAAG,SAAS,CAAC;IACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC1B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-html-minifier-next",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Astro integration for html-minifier-next",
|
|
5
5
|
"homepage": "https://github.com/jonasgeiler/astro-html-minifier-next#readme",
|
|
6
6
|
"bugs": "https://github.com/jonasgeiler/astro-html-minifier-next/issues",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"astro": "^5.0.0"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
+
"@types/html-minifier-next": "^2.1.0",
|
|
21
22
|
"html-minifier-next": "^2.1.8"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@biomejs/biome": "2.2.6",
|
|
25
|
-
"@types/html-minifier-next": "2.1.0",
|
|
26
26
|
"@types/node": "24.7.2",
|
|
27
27
|
"astro": "5.14.5",
|
|
28
28
|
"typescript": "5.9.3"
|
|
@@ -40,11 +40,12 @@
|
|
|
40
40
|
"./package.json": "./package.json"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
|
-
"dev": "
|
|
43
|
+
"dev": "pnpm run build --watch",
|
|
44
44
|
"check": "biome check",
|
|
45
45
|
"fix": "biome check --fix",
|
|
46
46
|
"unsafe-fix": "biome check --unsafe --fix",
|
|
47
|
+
"prebuild": "node --eval='require(`fs`).rmSync(`dist`,{recursive:true,force:true})'",
|
|
47
48
|
"build": "tsc",
|
|
48
|
-
"version": "node --
|
|
49
|
+
"version": "node --eval='const f=`jsr.json`,v=process.env.npm_package_version,s=require(`fs`);s.writeFileSync(f,JSON.stringify({...JSON.parse(s.readFileSync(f,`utf8`)),version:v},null,2)+`\\n`);console.log(`Updated ${f} for v${v}`)' && git add jsr.json"
|
|
49
50
|
}
|
|
50
51
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,55 @@
|
|
|
1
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
2
1
|
import { availableParallelism as getAvailableParallelism } from "node:os";
|
|
3
2
|
import { relative as getRelativePath } from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
4
|
import { styleText } from "node:util";
|
|
6
5
|
import type { AstroIntegration } from "astro";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
import type { MinifierOptions as MinifyHTMLOptions } from "html-minifier-next";
|
|
7
|
+
import { minifyHTMLFile } from "./minify-html-file.js";
|
|
8
|
+
import { MinifyHTMLFileWorkerPool } from "./minify-html-file-worker-pool.js";
|
|
9
|
+
|
|
10
|
+
export interface HTMLMinifierOptions extends MinifyHTMLOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Option specific to `astro-html-minifier-next` used to specify the maximum
|
|
13
|
+
* number of worker threads to spawn when minifying files.
|
|
14
|
+
* When set to `0`, `astro-html-minifier-next` will not create any worker
|
|
15
|
+
* threads and will do the minification in the main thread.
|
|
16
|
+
*
|
|
17
|
+
* Note: If unable to do a structured clone of the `html-minifier-next`
|
|
18
|
+
* options according to the
|
|
19
|
+
* [HTML structured clone
|
|
20
|
+
* algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm),
|
|
21
|
+
* `astro-html-minifier-next` will do the minification on the main
|
|
22
|
+
* thread, even if this option is not set to `0`.
|
|
23
|
+
*
|
|
24
|
+
* @default `Math.max(1, os.availableParallelism() - 1)`
|
|
25
|
+
*/
|
|
26
|
+
maxWorkers?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if a value can be transferred to a worker.
|
|
31
|
+
* @param {*} value
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
function isTransferable(value: unknown): boolean {
|
|
35
|
+
try {
|
|
36
|
+
// Attempt to do a structured clone of the value.
|
|
37
|
+
// If it succeeds, it should be transferable to a worker.
|
|
38
|
+
structuredClone(value);
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
11
44
|
|
|
45
|
+
/**
|
|
46
|
+
* An Astro integration that minifies HTML assets using
|
|
47
|
+
* [html-minifier-next](https://www.npmjs.com/package/html-minifier-next).
|
|
48
|
+
*
|
|
49
|
+
* @param options The options passed to the `minify` function of
|
|
50
|
+
* [html-minifier-next](https://www.npmjs.com/package/html-minifier-next).
|
|
51
|
+
* @returns The Astro integration.
|
|
52
|
+
*/
|
|
12
53
|
export default function htmlMinifier(
|
|
13
54
|
options: HTMLMinifierOptions,
|
|
14
55
|
): AstroIntegration {
|
|
@@ -21,10 +62,27 @@ export default function htmlMinifier(
|
|
|
21
62
|
|
|
22
63
|
const totalTimeStart = performance.now(); // --- TIMED BLOCK START ---
|
|
23
64
|
|
|
24
|
-
|
|
65
|
+
const availableParallelism = getAvailableParallelism();
|
|
66
|
+
const {
|
|
67
|
+
maxWorkers = Math.max(1, availableParallelism - 1),
|
|
68
|
+
...minifyHTMLOptions
|
|
69
|
+
} = options;
|
|
70
|
+
|
|
71
|
+
let workerPool: MinifyHTMLFileWorkerPool | undefined;
|
|
72
|
+
if (maxWorkers > 0 && isTransferable(minifyHTMLOptions)) {
|
|
73
|
+
workerPool = new MinifyHTMLFileWorkerPool(
|
|
74
|
+
maxWorkers,
|
|
75
|
+
minifyHTMLOptions,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
25
79
|
const tasks: (() => Promise<void>)[] = [];
|
|
80
|
+
let tasksTotal = 0;
|
|
81
|
+
let tasksDone = 0;
|
|
82
|
+
|
|
26
83
|
const controller = new AbortController();
|
|
27
84
|
const signal = controller.signal;
|
|
85
|
+
|
|
28
86
|
const distPath = fileURLToPath(distUrl);
|
|
29
87
|
const logLineArrow = styleText("green", "▶");
|
|
30
88
|
for (const assetUrls of assets.values()) {
|
|
@@ -37,52 +95,43 @@ export default function htmlMinifier(
|
|
|
37
95
|
const relativeAssetPath = getRelativePath(distPath, assetPath);
|
|
38
96
|
const logLineAssetPath = ` ${logLineArrow} /${relativeAssetPath} `;
|
|
39
97
|
tasks.push(async () => {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
encoding: "utf8",
|
|
44
|
-
signal,
|
|
45
|
-
});
|
|
46
|
-
const minifiedHtml = await minifyHtml(html, options);
|
|
47
|
-
|
|
48
|
-
const htmlSize = Buffer.byteLength(html);
|
|
49
|
-
const minifiedHtmlSize = Buffer.byteLength(minifiedHtml);
|
|
50
|
-
if (minifiedHtmlSize >= htmlSize) {
|
|
51
|
-
// No actual file size savings, so we skip writing the file or logging anything.
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
await writeFile(assetPath, minifiedHtml, {
|
|
56
|
-
encoding: "utf8",
|
|
57
|
-
signal,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const timeEnd = performance.now(); // --- TIMED BLOCK END ---
|
|
98
|
+
const { savings, time } = workerPool
|
|
99
|
+
? await workerPool.minifyHTMLFile(assetPath)
|
|
100
|
+
: await minifyHTMLFile(assetPath, minifyHTMLOptions, signal);
|
|
61
101
|
|
|
62
102
|
// Log a nice summary of the minification savings and the time it took.
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
103
|
+
const savingsSign = savings > 0 ? "-" : "+";
|
|
104
|
+
const savingsAbs = Math.abs(savings);
|
|
105
|
+
const savingsWithUnit =
|
|
106
|
+
savingsAbs < 1024
|
|
107
|
+
? `${savingsAbs}B`
|
|
108
|
+
: savingsAbs < 1048576
|
|
109
|
+
? `${(savingsAbs / 1024).toFixed(1)}kB`
|
|
110
|
+
: `${(savingsAbs / 1048576).toFixed(2)}MB`;
|
|
111
|
+
const timeWithUnit =
|
|
72
112
|
time < 1000
|
|
73
113
|
? `${Math.round(time)}ms`
|
|
74
114
|
: `${(time / 1000).toFixed(2)}s`;
|
|
75
115
|
logger.info(
|
|
76
116
|
logLineAssetPath +
|
|
77
|
-
styleText(
|
|
117
|
+
styleText(
|
|
118
|
+
savings <= 0 ? "yellow" : "dim",
|
|
119
|
+
`(${savingsSign}${savingsWithUnit}${savings <= 0 ? ", skipped" : ""}) `,
|
|
120
|
+
) +
|
|
121
|
+
styleText(
|
|
122
|
+
"dim",
|
|
123
|
+
`(+${timeWithUnit}) (${++tasksDone}/${tasksTotal})`,
|
|
124
|
+
),
|
|
78
125
|
);
|
|
79
126
|
});
|
|
127
|
+
|
|
128
|
+
tasksTotal++;
|
|
80
129
|
}
|
|
81
130
|
}
|
|
82
131
|
|
|
83
|
-
// We
|
|
84
|
-
//
|
|
85
|
-
const maxExecutingTasksSize =
|
|
132
|
+
// We use a quadruple of the available parallelism here, even if we don't actually run the tasks in different threads or anything.
|
|
133
|
+
// The available parallelism is a good indicator of machine capabilities, and the multiplier gives a good balance of speed and resource usage.
|
|
134
|
+
const maxExecutingTasksSize = availableParallelism * 4;
|
|
86
135
|
|
|
87
136
|
// This holds the current batch of promises that are waiting to fulfill.
|
|
88
137
|
const executingTasks = new Set<Promise<void>>();
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
import type { MinifierOptions as MinifyHTMLOptions } from "html-minifier-next";
|
|
3
|
+
import type { MinifyHTMLFileResult } from "./minify-html-file.js";
|
|
4
|
+
|
|
5
|
+
interface MinifyHTMLFileWorker extends Worker {
|
|
6
|
+
_currentResolve?: (result: MinifyHTMLFileResult) => void;
|
|
7
|
+
_currentReject?: (error: unknown) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type MinifyHTMLWorkerInput = string;
|
|
11
|
+
export type MinifyHTMLWorkerOutput = MinifyHTMLFileResult | { error: unknown };
|
|
12
|
+
|
|
13
|
+
export class MinifyHTMLFileWorkerPool {
|
|
14
|
+
protected maxWorkers: number;
|
|
15
|
+
protected minifyHTMLOptions: MinifyHTMLOptions;
|
|
16
|
+
|
|
17
|
+
protected workerUrl: URL;
|
|
18
|
+
protected pool: Set<Worker>;
|
|
19
|
+
protected idle: Worker[];
|
|
20
|
+
protected queue: ((value: Worker) => void)[];
|
|
21
|
+
|
|
22
|
+
constructor(maxWorkers: number, minifyHTMLOptions: MinifyHTMLOptions) {
|
|
23
|
+
this.maxWorkers = maxWorkers;
|
|
24
|
+
this.minifyHTMLOptions = minifyHTMLOptions;
|
|
25
|
+
|
|
26
|
+
this.workerUrl = new URL("./minify-html-file-worker.js", import.meta.url);
|
|
27
|
+
this.pool = new Set();
|
|
28
|
+
this.idle = [];
|
|
29
|
+
this.queue = [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
protected async getAvailableWorker(): Promise<MinifyHTMLFileWorker> {
|
|
33
|
+
// If there is an idle worker, use it.
|
|
34
|
+
if (this.idle.length) {
|
|
35
|
+
const worker = this.idle.shift();
|
|
36
|
+
if (worker !== undefined) {
|
|
37
|
+
return worker;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// If we can create a new worker, do so.
|
|
42
|
+
if (this.pool.size < this.maxWorkers) {
|
|
43
|
+
const worker = new Worker(this.workerUrl, {
|
|
44
|
+
workerData: this.minifyHTMLOptions,
|
|
45
|
+
}) as MinifyHTMLFileWorker;
|
|
46
|
+
|
|
47
|
+
worker.on("message", async (message: MinifyHTMLWorkerOutput) => {
|
|
48
|
+
if ("error" in message) {
|
|
49
|
+
worker._currentReject?.(message.error);
|
|
50
|
+
} else {
|
|
51
|
+
worker._currentResolve?.(message);
|
|
52
|
+
}
|
|
53
|
+
worker._currentResolve = worker._currentReject = undefined;
|
|
54
|
+
|
|
55
|
+
this.releaseWorker(worker);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
worker.on("error", (error) => {
|
|
59
|
+
worker._currentReject?.(error);
|
|
60
|
+
worker._currentResolve = worker._currentReject = undefined;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
worker.on("exit", (exitCode) => {
|
|
64
|
+
this.removeWorker(worker);
|
|
65
|
+
|
|
66
|
+
if (exitCode !== 0) {
|
|
67
|
+
worker._currentReject?.(
|
|
68
|
+
new Error(`Worker failed with exit code ${exitCode}.`),
|
|
69
|
+
);
|
|
70
|
+
worker._currentResolve = worker._currentReject = undefined;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
this.pool.add(worker);
|
|
75
|
+
return worker;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Otherwise, wait for a worker to free up.
|
|
79
|
+
return new Promise<Worker>((resolve) => {
|
|
80
|
+
// When a worker frees up, they will check the queue and resolve this promise.
|
|
81
|
+
this.queue.push(resolve);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected releaseWorker(worker: Worker): void {
|
|
86
|
+
// If there is a queued request for a worker, resolve it.
|
|
87
|
+
if (this.queue.length) {
|
|
88
|
+
const resolve = this.queue.shift();
|
|
89
|
+
if (resolve !== undefined) {
|
|
90
|
+
resolve(worker);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Otherwise, keep the worker as idle.
|
|
96
|
+
this.idle.push(worker);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
protected removeWorker(worker: Worker): void {
|
|
100
|
+
this.pool.delete(worker);
|
|
101
|
+
|
|
102
|
+
// If a worker is force stopped by the system, it might still be in the idle list.
|
|
103
|
+
const idleIndex = this.idle.indexOf(worker);
|
|
104
|
+
if (idleIndex !== -1) {
|
|
105
|
+
this.idle.splice(idleIndex, 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public async minifyHTMLFile(
|
|
110
|
+
htmlFile: string,
|
|
111
|
+
// TODO: Signal?
|
|
112
|
+
): Promise<MinifyHTMLFileResult> {
|
|
113
|
+
const worker = await this.getAvailableWorker();
|
|
114
|
+
|
|
115
|
+
return new Promise<MinifyHTMLFileResult>((resolve, reject) => {
|
|
116
|
+
worker._currentResolve = resolve;
|
|
117
|
+
worker._currentReject = reject;
|
|
118
|
+
worker.postMessage(htmlFile satisfies MinifyHTMLWorkerInput);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// TODO: Destroy function
|
|
123
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isMainThread,
|
|
3
|
+
workerData as minifyHTMLOptions,
|
|
4
|
+
parentPort,
|
|
5
|
+
} from "node:worker_threads";
|
|
6
|
+
import { minifyHTMLFile } from "./minify-html-file.js";
|
|
7
|
+
import type {
|
|
8
|
+
MinifyHTMLWorkerInput,
|
|
9
|
+
MinifyHTMLWorkerOutput,
|
|
10
|
+
} from "./minify-html-file-worker-pool.js";
|
|
11
|
+
|
|
12
|
+
if (isMainThread) {
|
|
13
|
+
throw new Error("Not a worker thread.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// biome-ignore-start lint/style/noNonNullAssertion: I can assume `parentPort` is not null.
|
|
17
|
+
parentPort!.on("message", async (htmlFile: MinifyHTMLWorkerInput) => {
|
|
18
|
+
try {
|
|
19
|
+
parentPort!.postMessage(
|
|
20
|
+
(await minifyHTMLFile(
|
|
21
|
+
htmlFile,
|
|
22
|
+
minifyHTMLOptions,
|
|
23
|
+
)) satisfies MinifyHTMLWorkerOutput,
|
|
24
|
+
);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
parentPort!.postMessage({ error } satisfies MinifyHTMLWorkerOutput);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
// biome-ignore-end lint/style/noNonNullAssertion: See start.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import {
|
|
3
|
+
type MinifierOptions as MinifyHTMLOptions,
|
|
4
|
+
minify as minifyHTML,
|
|
5
|
+
} from "html-minifier-next";
|
|
6
|
+
|
|
7
|
+
export interface MinifyHTMLFileResult {
|
|
8
|
+
savings: number;
|
|
9
|
+
time: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function minifyHTMLFile(
|
|
13
|
+
htmlFile: string,
|
|
14
|
+
minifyHTMLOptions: MinifyHTMLOptions,
|
|
15
|
+
signal?: AbortSignal,
|
|
16
|
+
): Promise<MinifyHTMLFileResult> {
|
|
17
|
+
const timeStart = performance.now(); // --- TIMED BLOCK START ---
|
|
18
|
+
|
|
19
|
+
const html = await readFile(htmlFile, {
|
|
20
|
+
encoding: "utf8",
|
|
21
|
+
signal,
|
|
22
|
+
});
|
|
23
|
+
const minifiedHTML = await minifyHTML(html, minifyHTMLOptions);
|
|
24
|
+
|
|
25
|
+
const savings = Buffer.byteLength(html) - Buffer.byteLength(minifiedHTML);
|
|
26
|
+
if (savings > 0) {
|
|
27
|
+
// Only write the minified HTML to the file if it's smaller.
|
|
28
|
+
await writeFile(htmlFile, minifiedHTML, {
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
signal,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const timeEnd = performance.now(); // --- TIMED BLOCK END ---
|
|
35
|
+
|
|
36
|
+
const time = timeEnd - timeStart;
|
|
37
|
+
return { savings, time };
|
|
38
|
+
}
|