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 CHANGED
@@ -1,4 +1,30 @@
1
1
  import type { AstroIntegration } from "astro";
2
- import { type MinifierOptions as HTMLMinifierOptions } from "html-minifier-next";
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAC9C,OAAO,EACN,KAAK,eAAe,IAAI,mBAAmB,EAE3C,MAAM,oBAAoB,CAAC;AAE5B,MAAM,CAAC,OAAO,UAAU,YAAY,CACnC,OAAO,EAAE,mBAAmB,GAC1B,gBAAgB,CAqHlB"}
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 { minify as minifyHtml, } from "html-minifier-next";
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
- // TODO: Use workers?
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 timeStart = performance.now(); // --- TIMED BLOCK START ---
31
- const html = await readFile(assetPath, {
32
- encoding: "utf8",
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 savings = htmlSize - minifiedHtmlSize;
49
- const savingsStr = savings < 1000
50
- ? `${savings}B`
51
- : savings < 1000000
52
- ? `${(savings / 1000).toFixed(1)}kB`
53
- : `${(savings / 1000000).toFixed(2)}MB`;
54
- const time = timeEnd - timeStart;
55
- const timeStr = time < 1000
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", `(-${savingsStr}) (+${timeStr})`));
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 retrieve the available parallelism from the OS, even if we don't actually run the tasks in different threads.
64
- // It's just used as an indicator of machine capabilities and usually a good value for batching.
65
- const maxExecutingTasksSize = getAvailableParallelism();
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":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=minify-html-file-worker.d.ts.map
@@ -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.1",
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": "tsc --watch",
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 --input-type=module --eval=\"import f from 'node:fs/promises';const j=JSON.parse(await f.readFile('jsr.json','utf8'));j.version=process.env.npm_package_version||j.version;await f.writeFile('jsr.json',JSON.stringify(j,null,2)+'\\n','utf8');console.log('Updated jsr.json for version',j.version)\" && git add jsr.json"
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
- type MinifierOptions as HTMLMinifierOptions,
9
- minify as minifyHtml,
10
- } from "html-minifier-next";
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
- // TODO: Use workers?
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 timeStart = performance.now(); // --- TIMED BLOCK START ---
41
-
42
- const html = await readFile(assetPath, {
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 savings = htmlSize - minifiedHtmlSize;
64
- const savingsStr =
65
- savings < 1000
66
- ? `${savings}B`
67
- : savings < 1000000
68
- ? `${(savings / 1000).toFixed(1)}kB`
69
- : `${(savings / 1000000).toFixed(2)}MB`;
70
- const time = timeEnd - timeStart;
71
- const timeStr =
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("dim", `(-${savingsStr}) (+${timeStr})`),
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 retrieve the available parallelism from the OS, even if we don't actually run the tasks in different threads.
84
- // It's just used as an indicator of machine capabilities and usually a good value for batching.
85
- const maxExecutingTasksSize = getAvailableParallelism();
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
+ }