diff --git a/package-lock.json b/package-lock.json index 14dec6c..a448c82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "11.1.0", "license": "MIT", "dependencies": { + "jest-worker": "^30.2.0", "schema-utils": "^4.2.0", "serialize-javascript": "^6.0.2" }, @@ -157,6 +158,7 @@ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2340,7 +2342,8 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.0.18.tgz", "integrity": "sha512-EF77RqROHL+4LhMGW5NTeKqfUd/e4OOv6EDFQ/UQQiFyWuqkEKyEz0NDILxOFxWUEVdjT2GQ2cC7t12B6pESwg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cspell/dict-dart": { "version": "2.3.1", @@ -2480,14 +2483,16 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.12.tgz", "integrity": "sha512-JFffQ1dDVEyJq6tCDWv0r/RqkdSnV43P2F/3jJ9rwLgdsOIXwQbXrz6QDlvQLVvNSnORH9KjDtenFTGDyzfCaA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cspell/dict-html-symbol-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.4.tgz", "integrity": "sha512-afea+0rGPDeOV9gdO06UW183Qg6wRhWVkgCFwiO3bDupAoyXRuvupbb5nUyqSTsLXIKL8u8uXQlJ9pkz07oVXw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cspell/dict-java": { "version": "5.0.12", @@ -2685,7 +2690,8 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.2.3.tgz", "integrity": "sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cspell/dict-vue": { "version": "3.0.5", @@ -2956,6 +2962,7 @@ "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2969,6 +2976,7 @@ "integrity": "sha512-Y+X1B1j+/zupKDVJfkKc8uYMjQkGzfnd8lt7vK3y8x9Br6H5dBuhAfFrQ6ff7HAMm/1BwgecyEiRFkYCWPRxmA==", "dev": true, "license": "MIT", + "peer": true, "workspaces": [ "examples/*" ], @@ -3559,7 +3567,6 @@ "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -3696,6 +3703,39 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@jest/reporters/node_modules/jest-worker": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.5.tgz", + "integrity": "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.5", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/@jest/reporters/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -3726,7 +3766,6 @@ "version": "30.0.5", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.34.0" @@ -4378,7 +4417,6 @@ "version": "0.34.38", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", - "dev": true, "license": "MIT" }, "node_modules/@sinonjs/commons": { @@ -4407,6 +4445,7 @@ "integrity": "sha512-bE2DUjruqXlHYP3Q2Gpqiuj2bHq7/88FnuaS0FjeGGLCy+X6a07bGVuwtiOYnPSLHR6jmx5Bwdv+j7l8H+G97A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/types": "^8.37.0", @@ -4517,6 +4556,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -4544,14 +4584,12 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" @@ -4561,7 +4599,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" @@ -4608,8 +4645,8 @@ "version": "20.19.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz", "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==", - "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4660,7 +4697,6 @@ "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, "license": "MIT", "dependencies": { "@types/yargs-parser": "*" @@ -4670,7 +4706,6 @@ "version": "21.0.3", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -4679,6 +4714,7 @@ "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.38.0", @@ -4719,6 +4755,7 @@ "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", @@ -4948,7 +4985,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, "license": "ISC" }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { @@ -5401,6 +5437,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5460,6 +5497,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6077,6 +6115,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -6337,7 +6376,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", - "dev": true, "funding": [ { "type": "github", @@ -9340,6 +9378,7 @@ "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -9430,6 +9469,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9588,6 +9628,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9632,6 +9673,7 @@ "integrity": "sha512-EE44T0OSMCeXhDrrdsbKAhprobKkPtJTbQz5yEktysNpHeDZTAL1SfDTNKmcFfJkY6yrQLtTKZALrD3j/Gpmiw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/utils": "^8.0.0" }, @@ -9658,6 +9700,7 @@ "integrity": "sha512-KZjaoTWWUIml6K6zyPvwCYlLoMDQ69taSdTcdTIavBUoJCIWUfYcsRIw4n9dzllMouqdxiFfKW33EAbBLBu1HA==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@es-joy/jsdoccomment": "~0.52.0", "are-docs-informative": "^0.0.2", @@ -9696,6 +9739,7 @@ "integrity": "sha512-MtxYjDZhMQgsWRm/4xYLL0i2EhusWT7itDxlJ80l1NND2AL2Vi5Mvneqv/ikG9+zpran0VsVRXTEHrpLmUZRNw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.5.0", "enhanced-resolve": "^5.17.1", @@ -9749,6 +9793,7 @@ "integrity": "sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.7" @@ -9780,6 +9825,7 @@ "integrity": "sha512-QUzTefvP8stfSXsqKQ+vBQSEsXIlAiCduS/V1Em+FKgL9c21U/IIm20/e3MFy1jyCf14tHAhqC1sX8OTy6VUCg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "@eslint-community/eslint-utils": "^4.7.0", @@ -10368,6 +10414,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -11468,6 +11515,7 @@ "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -11549,7 +11597,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -11608,7 +11655,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12664,6 +12710,7 @@ "integrity": "sha512-y2mfcJywuTUkvLm2Lp1/pFX8kTgMO5yyQGq/Sk/n2mN7XWYp4JsCZ/QXW34M8YScgk8bPZlREH04f6blPnoHnQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.0.5", "@jest/types": "30.0.5", @@ -13239,6 +13286,39 @@ "fsevents": "^2.3.3" } }, + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.5.tgz", + "integrity": "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.5", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/jest-leak-detector": { "version": "30.0.5", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.5.tgz", @@ -13443,7 +13523,6 @@ "version": "30.0.1", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -13633,6 +13712,39 @@ "dev": true, "license": "MIT" }, + "node_modules/jest-runner/node_modules/jest-worker": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.5.tgz", + "integrity": "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.5", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner/node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/jest-runtime": { "version": "30.0.5", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.5.tgz", @@ -14105,15 +14217,14 @@ "license": "MIT" }, "node_modules/jest-worker": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.5.tgz", - "integrity": "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ==", - "dev": true, + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", "license": "MIT", "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -14121,11 +14232,106 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-worker/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-worker/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-worker/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-worker/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/jest-worker/node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -15283,7 +15489,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, "license": "MIT" }, "node_modules/merge2": { @@ -16703,7 +16908,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -16860,6 +17064,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -17517,6 +17722,7 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -19190,7 +19396,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/type-check": { "version": "0.4.0", @@ -19319,6 +19526,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -19333,6 +19541,7 @@ "integrity": "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/eslint-plugin": "8.38.0", "@typescript-eslint/parser": "8.38.0", @@ -19388,7 +19597,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -19712,6 +19920,7 @@ "integrity": "sha512-B4t+nJqytPeuZlHuIKTbalhljIFXeNRqrUGAQgTGlfOl2lXXKXw+yZu6bicycP+PUlM44CxBjCFD6aciKFT3LQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", diff --git a/package.json b/package.json index cb94cf1..85bd697 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "release": "standard-version" }, "dependencies": { + "jest-worker": "^30.2.0", "schema-utils": "^4.2.0", "serialize-javascript": "^6.0.2" }, diff --git a/src/index.js b/src/index.js index 12194de..195c389 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ */ const crypto = require("node:crypto"); +const os = require("node:os"); const path = require("node:path"); const { validate } = require("schema-utils"); @@ -20,6 +21,7 @@ const schema = require("./options.json"); /** @typedef {import("webpack").sources.Source} Source */ /** @typedef {import("webpack").Asset} Asset */ /** @typedef {import("webpack").WebpackError} WebpackError */ +/** @typedef {import("jest-worker").Worker} JestWorker */ /** * @template T @@ -36,6 +38,62 @@ const schema = require("./options.json"); * @typedef {{ [key: string]: EXPECTED_ANY }} CustomOptions */ +/** + * @typedef {JestWorker & { compress: (options: { inputBase64: string, algorithm: string, compressionOptions: CustomOptions }) => Promise<{ compressedBase64: string }> }} CompressionWorker + */ + +const notSettled = Symbol("not-settled"); + +/** + * @template T + * @typedef {() => Promise} Task + */ + +/** + * Run tasks with limited concurrency. + * @template T + * @param {number} limit Limit of tasks that run at once. + * @param {Task[]} tasks List of tasks to run. + * @returns {Promise} A promise that fulfills to an array of the results + */ +function throttleAll(limit, tasks) { + if (tasks.length === 0) { + return Promise.resolve(/** @type {T[]} */ ([])); + } + + return new Promise((resolve, reject) => { + const result = Array.from({ length: tasks.length }).fill(notSettled); + const entries = tasks.entries(); + const next = () => { + const { done, value } = entries.next(); + + if (done) { + const isLast = !result.includes(notSettled); + + if (isLast) resolve(/** @type {T[]} */ (result)); + + return; + } + + const [index, task] = value; + + /** + * @param {T} resultValue Result value + */ + const onFulfilled = (resultValue) => { + result[index] = resultValue; + next(); + }; + + task().then(onFulfilled, reject); + }; + + for (let i = 0; i < limit; i++) { + next(); + } + }); +} + /** * @template T * @typedef {T extends infer U ? U : CustomOptions} InferDefaultType @@ -62,6 +120,10 @@ const schema = require("./options.json"); * @typedef {boolean | "keep-source-map" | ((name: string) => boolean)} DeleteOriginalAssets */ +/** + * @typedef {undefined | boolean | number} Parallel + */ + /** * @template T * @typedef {object} BasePluginOptions @@ -72,6 +134,7 @@ const schema = require("./options.json"); * @property {number=} minRatio only assets that compress better than this ratio are processed (`minRatio = Compressed Size / Original Size`) * @property {DeleteOriginalAssets=} deleteOriginalAssets whether to delete the original assets or not * @property {Filename=} filename the target asset filename + * @property {Parallel=} parallel enables parallel compression */ /** @@ -114,6 +177,7 @@ class CompressionPlugin { threshold = 0, minRatio = 0.8, deleteOriginalAssets = false, + parallel = true, } = options || {}; /** @@ -130,6 +194,7 @@ class CompressionPlugin { threshold, minRatio, deleteOriginalAssets, + parallel, }; /** @@ -189,6 +254,26 @@ class CompressionPlugin { } } + /** + * @private + * @param {Parallel} parallel value of the `parallel` option + * @returns {number} number of cores for parallelism + */ + static getAvailableNumberOfCores(parallel) { + // In some cases cpus() returns undefined + // https://github.com/nodejs/node/issues/19022 + const cpus = + // eslint-disable-next-line n/no-unsupported-features/node-builtins + typeof os.availableParallelism === "function" + ? // eslint-disable-next-line n/no-unsupported-features/node-builtins + { length: os.availableParallelism() } + : os.cpus() || { length: 1 }; + + return parallel === true || typeof parallel === "undefined" + ? cpus.length - 1 + : Math.min(parallel || 0, cpus.length - 1); + } + /** * @private * @param {Buffer} input input @@ -221,10 +306,12 @@ class CompressionPlugin { * @param {Compiler} compiler compiler * @param {Compilation} compilation compilation * @param {Record} assets assets + * @param {{ availableNumberOfCores: number }} optimizeOptions optimize options * @returns {Promise} */ - async compress(compiler, compilation, assets) { + async compress(compiler, compilation, assets, optimizeOptions) { const cache = compilation.getCache("CompressionWebpackPlugin"); + let numberOfAssets = 0; /** * @typedef {object} AssetForCompression @@ -308,6 +395,8 @@ class CompressionPlugin { // No need original buffer for cached files if (!output.source) { + numberOfAssets++; + if (typeof source.buffer === "function") { buffer = source.buffer(); } @@ -331,85 +420,158 @@ class CompressionPlugin { ) ).filter(Boolean); + /** @type {undefined | (() => CompressionWorker)} */ + let getWorker; + /** @type {undefined | CompressionWorker} */ + let initializedWorker; + /** @type {undefined | number} */ + let numberOfWorkers; + + if (optimizeOptions.availableNumberOfCores > 0) { + // Do not create unnecessary workers when the number of files is less than the available cores, it saves memory + numberOfWorkers = Math.min( + numberOfAssets, + optimizeOptions.availableNumberOfCores, + ); + + getWorker = () => { + if (initializedWorker) { + return initializedWorker; + } + + const { Worker } = require("jest-worker"); + + initializedWorker = + /** @type {CompressionWorker} */ + ( + new Worker(require.resolve("./worker"), { + numWorkers: numberOfWorkers, + enableWorkerThreads: true, + }) + ); + + const workerStdout = initializedWorker.getStdout(); + + // https://github.com/facebook/jest/issues/8872#issuecomment-524822081 + if (workerStdout) { + workerStdout.on("data", (chunk) => process.stdout.write(chunk)); + } + + const workerStderr = initializedWorker.getStderr(); + + if (workerStderr) { + workerStderr.on("data", (chunk) => process.stderr.write(chunk)); + } + + return initializedWorker; + }; + } + const { RawSource } = compiler.webpack.sources; const scheduledTasks = []; for (const asset of assetsForCompression) { - scheduledTasks.push( - (async () => { - const { name, source, buffer, output, cacheItem, info, relatedName } = - /** @type {AssetForCompression} */ - (asset); - - if (!output.source) { - if (!output.compressed) { - try { + scheduledTasks.push(async () => { + const { name, source, buffer, output, cacheItem, info, relatedName } = + /** @type {AssetForCompression} */ + (asset); + + if (!output.source) { + if (!output.compressed) { + try { + if (typeof this.options.algorithm === "string") { + // Use worker pool if available, otherwise call compress directly + const compress = getWorker + ? getWorker().compress + : require("./worker").compress; + + const result = await compress({ + inputBase64: buffer.toString("base64"), + algorithm: this.options.algorithm, + compressionOptions: + /** @type {CustomOptions} */ + (this.options.compressionOptions), + }); + + output.compressed = Buffer.from( + result.compressedBase64, + "base64", + ); + } else { output.compressed = await this.runCompressionAlgorithm(buffer); - } catch (error) { - compilation.errors.push(/** @type {WebpackError} */ (error)); - - return; } - } - - if ( - output.compressed.length / buffer.length > - this.options.minRatio - ) { - await cacheItem.storePromise({ compressed: output.compressed }); + } catch (error) { + compilation.errors.push(/** @type {WebpackError} */ (error)); return; } - - output.source = new RawSource(output.compressed); - - await cacheItem.storePromise(output); } - const newFilename = compilation.getPath(this.options.filename, { - filename: name, - }); - /** @type {AssetInfo} */ - const newInfo = { compressed: true }; - - // TODO: possible problem when developer uses custom function, ideally we need to get parts of filename (i.e. name/base/ext/etc) in info - // otherwise we can't detect an asset as immutable if ( - info.immutable && - typeof this.options.filename === "string" && - /(\[name]|\[base]|\[file])/.test(this.options.filename) + output.compressed.length / buffer.length > + this.options.minRatio ) { - newInfo.immutable = true; + await cacheItem.storePromise({ compressed: output.compressed }); + + return; } - if (this.options.deleteOriginalAssets) { - if (this.options.deleteOriginalAssets === "keep-source-map") { - compilation.updateAsset(name, source, { - related: { sourceMap: null }, - }); + output.source = new RawSource(output.compressed); - compilation.deleteAsset(name); - } else if ( - typeof this.options.deleteOriginalAssets === "function" - ) { - if (this.options.deleteOriginalAssets(name)) { - compilation.deleteAsset(name); - } - } else { + await cacheItem.storePromise(output); + } + + const newFilename = compilation.getPath(this.options.filename, { + filename: name, + }); + /** @type {{ compressed: boolean, immutable?: boolean }} */ + const newInfo = { compressed: true }; + + /** @type {AssetInfo} */ + // TODO: possible problem when developer uses custom function, ideally we need to get parts of filename (i.e. name/base/ext/etc) in info + // otherwise we can't detect an asset as immutable + if ( + info.immutable && + typeof this.options.filename === "string" && + /(\[name]|\[base]|\[file])/.test(this.options.filename) + ) { + newInfo.immutable = true; + } + + if (this.options.deleteOriginalAssets) { + if (this.options.deleteOriginalAssets === "keep-source-map") { + compilation.updateAsset(name, source, { + related: { sourceMap: null }, + }); + + compilation.deleteAsset(name); + } else if (typeof this.options.deleteOriginalAssets === "function") { + if (this.options.deleteOriginalAssets(name)) { compilation.deleteAsset(name); } } else { - compilation.updateAsset(name, source, { - related: { [relatedName]: newFilename }, - }); + compilation.deleteAsset(name); } + } else { + compilation.updateAsset(name, source, { + related: { [relatedName]: newFilename }, + }); + } - compilation.emitAsset(newFilename, output.source, newInfo); - })(), - ); + compilation.emitAsset(newFilename, output.source, newInfo); + }); } - await Promise.all(scheduledTasks); + const limit = + getWorker && numberOfAssets > 0 + ? /** @type {number} */ (numberOfWorkers) + : scheduledTasks.length; + + await throttleAll(limit, scheduledTasks); + + if (initializedWorker) { + await initializedWorker.end(); + } } /** @@ -417,6 +579,9 @@ class CompressionPlugin { * @returns {void} */ apply(compiler) { + const availableNumberOfCores = CompressionPlugin.getAvailableNumberOfCores( + this.options.parallel, + ); const pluginName = this.constructor.name; compiler.hooks.thisCompilation.tap(pluginName, (compilation) => { @@ -427,7 +592,10 @@ class CompressionPlugin { compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER, additionalAssets: true, }, - (assets) => this.compress(compiler, compilation, assets), + (assets) => + this.compress(compiler, compilation, assets, { + availableNumberOfCores, + }), ); compilation.hooks.statsPrinter.tap(pluginName, (stats) => { diff --git a/src/options.json b/src/options.json index 26ac87d..a9e9e6f 100644 --- a/src/options.json +++ b/src/options.json @@ -117,6 +117,18 @@ "instanceof": "Function" } ] + }, + "parallel": { + "description": "Use multi-process parallel running to improve the build speed.", + "link": "https://github.com/webpack/compression-webpack-plugin#parallel", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "integer" + } + ] } } } diff --git a/src/worker.js b/src/worker.js new file mode 100644 index 0000000..7ea6761 --- /dev/null +++ b/src/worker.js @@ -0,0 +1,68 @@ +// eslint-disable-next-line jsdoc/no-restricted-syntax +/** @typedef {any} EXPECTED_ANY */ + +/** + * @typedef {{ [key: string]: EXPECTED_ANY }} CustomOptions + */ + +/** + * @typedef {object} CompressOptions + * @property {string} inputBase64 input buffer as base64 string + * @property {string} algorithm algorithm name + * @property {CustomOptions} compressionOptions compression options + */ + +/** + * @typedef {object} CompressResult + * @property {string} compressedBase64 compressed buffer as base64 string + */ + +/** + * @param {CompressOptions} options compress options + * @returns {Promise} compressed result + */ +async function compress(options) { + const { inputBase64, algorithm, compressionOptions } = options; + + /** + * @type {typeof import("node:zlib")} + */ + const zlib = require("node:zlib"); + + const input = Buffer.from(inputBase64, "base64"); + + return new Promise((resolve, reject) => { + /** + * @type {((buf: Buffer, options: CustomOptions, callback: (error: Error | null, result: Buffer) => void) => void)} + */ + const zlibFunction = + zlib[ + /** @type {"gzip" | "deflate" | "deflateRaw" | "brotliCompress"} */ ( + algorithm + ) + ]; + + zlibFunction( + input, + compressionOptions, + /** + * @param {Error | null} error error from zlib + * @param {Buffer} result compressed buffer + */ + (error, result) => { + if (error) { + reject(error); + return; + } + + const compressed = Buffer.isBuffer(result) + ? result + : Buffer.from(/** @type {string} */ (result)); + + resolve({ compressedBase64: compressed.toString("base64") }); + }, + ); + }); +} + +module.exports = { compress }; diff --git a/test/CompressionPlugin.test.js b/test/CompressionPlugin.test.js index 28c4ed7..f6fea2c 100644 --- a/test/CompressionPlugin.test.js +++ b/test/CompressionPlugin.test.js @@ -100,7 +100,7 @@ describe("CompressionPlugin", () => { }, ); - new CompressionPlugin().apply(compiler); + new CompressionPlugin({ parallel: false }).apply(compiler); const stats = await compile(compiler); diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index 0633bd4..9d694d7 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`validate options should throw an error on the "algorithm" option with "true" value 1`] = ` "Invalid options object. Compression Plugin has been initialized using an options object that does not match the API schema. @@ -155,6 +155,39 @@ exports[`validate options should throw an error on the "minRatio" option with "0 -> Read more at https://github.com/webpack/compression-webpack-plugin/#minratio" `; +exports[`validate options should throw an error on the "parallel" option with "{}" value 1`] = ` +"Invalid options object. Compression Plugin has been initialized using an options object that does not match the API schema. + - options.parallel should be one of these: + boolean | integer + -> Use multi-process parallel running to improve the build speed. + -> Read more at https://github.com/webpack/compression-webpack-plugin#parallel + Details: + * options.parallel should be a boolean. + * options.parallel should be an integer." +`; + +exports[`validate options should throw an error on the "parallel" option with "2" value 1`] = ` +"Invalid options object. Compression Plugin has been initialized using an options object that does not match the API schema. + - options.parallel should be one of these: + boolean | integer + -> Use multi-process parallel running to improve the build speed. + -> Read more at https://github.com/webpack/compression-webpack-plugin#parallel + Details: + * options.parallel should be a boolean. + * options.parallel should be an integer." +`; + +exports[`validate options should throw an error on the "parallel" option with "true" value 1`] = ` +"Invalid options object. Compression Plugin has been initialized using an options object that does not match the API schema. + - options.parallel should be one of these: + boolean | integer + -> Use multi-process parallel running to improve the build speed. + -> Read more at https://github.com/webpack/compression-webpack-plugin#parallel + Details: + * options.parallel should be a boolean. + * options.parallel should be an integer." +`; + exports[`validate options should throw an error on the "test" option with "[{},"foo",true]" value 1`] = ` "Invalid options object. Compression Plugin has been initialized using an options object that does not match the API schema. - options.test should be one of these: @@ -212,47 +245,47 @@ exports[`validate options should throw an error on the "threshold" option with " exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = ` "Invalid options object. Compression Plugin has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { test?, include?, exclude?, algorithm?, compressionOptions?, threshold?, minRatio?, deleteOriginalAssets?, filename? }" + object { test?, include?, exclude?, algorithm?, compressionOptions?, threshold?, minRatio?, deleteOriginalAssets?, filename?, parallel? }" `; exports[`validate options should throw an error on the "unknown" option with "[]" value 1`] = ` "Invalid options object. Compression Plugin has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { test?, include?, exclude?, algorithm?, compressionOptions?, threshold?, minRatio?, deleteOriginalAssets?, filename? }" + object { test?, include?, exclude?, algorithm?, compressionOptions?, threshold?, minRatio?, deleteOriginalAssets?, filename?, parallel? }" `; exports[`validate options should throw an error on the "unknown" option with "{"foo":"bar"}" value 1`] = ` "Invalid options object. Compression Plugin has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { test?, include?, exclude?, algorithm?, compressionOptions?, threshold?, minRatio?, deleteOriginalAssets?, filename? }" + object { test?, include?, exclude?, algorithm?, compressionOptions?, threshold?, minRatio?, deleteOriginalAssets?, filename?, parallel? }" `; exports[`validate options should throw an error on the "unknown" option with "{}" value 1`] = ` "Invalid options object. Compression Plugin has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { test?, include?, exclude?, algorithm?, compressionOptions?, threshold?, minRatio?, deleteOriginalAssets?, filename? }" + object { test?, include?, exclude?, algorithm?, compressionOptions?, threshold?, minRatio?, deleteOriginalAssets?, filename?, parallel? }" `; exports[`validate options should throw an error on the "unknown" option with "1" value 1`] = ` "Invalid options object. Compression Plugin has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { test?, include?, exclude?, algorithm?, compressionOptions?, threshold?, minRatio?, deleteOriginalAssets?, filename? }" + object { test?, include?, exclude?, algorithm?, compressionOptions?, threshold?, minRatio?, deleteOriginalAssets?, filename?, parallel? }" `; exports[`validate options should throw an error on the "unknown" option with "false" value 1`] = ` "Invalid options object. Compression Plugin has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { test?, include?, exclude?, algorithm?, compressionOptions?, threshold?, minRatio?, deleteOriginalAssets?, filename? }" + object { test?, include?, exclude?, algorithm?, compressionOptions?, threshold?, minRatio?, deleteOriginalAssets?, filename?, parallel? }" `; exports[`validate options should throw an error on the "unknown" option with "test" value 1`] = ` "Invalid options object. Compression Plugin has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { test?, include?, exclude?, algorithm?, compressionOptions?, threshold?, minRatio?, deleteOriginalAssets?, filename? }" + object { test?, include?, exclude?, algorithm?, compressionOptions?, threshold?, minRatio?, deleteOriginalAssets?, filename?, parallel? }" `; exports[`validate options should throw an error on the "unknown" option with "true" value 1`] = ` "Invalid options object. Compression Plugin has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { test?, include?, exclude?, algorithm?, compressionOptions?, threshold?, minRatio?, deleteOriginalAssets?, filename? }" + object { test?, include?, exclude?, algorithm?, compressionOptions?, threshold?, minRatio?, deleteOriginalAssets?, filename?, parallel? }" `; diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 27c7b91..133a66b 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -59,6 +59,10 @@ describe("validate options", () => { success: [true, false, "keep-source-map", () => true], failure: ["true", "unknown"], }, + parallel: { + success: [true, false, 2], + failure: ["true", "2", {}], + }, unknown: { success: [], failure: [1, true, false, "test", /test/, [], {}, { foo: "bar" }], diff --git a/types/index.d.ts b/types/index.d.ts index 77daa2c..a397aa1 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -8,6 +8,7 @@ export = CompressionPlugin; /** @typedef {import("webpack").sources.Source} Source */ /** @typedef {import("webpack").Asset} Asset */ /** @typedef {import("webpack").WebpackError} WebpackError */ +/** @typedef {import("jest-worker").Worker} JestWorker */ /** * @template T * @typedef {T | { valueOf(): T }} WithImplicitCoercion @@ -18,6 +19,9 @@ export = CompressionPlugin; /** * @typedef {{ [key: string]: EXPECTED_ANY }} CustomOptions */ +/** + * @typedef {JestWorker & { transform: (options: string) => Promise<{ compressedBase64: string }>, compress: (options: { inputBase64: string, algorithm: string, compressionOptions: object }) => Promise<{ compressedBase64: string }> }} CompressionWorker + */ /** * @template T * @typedef {T extends infer U ? U : CustomOptions} InferDefaultType @@ -39,6 +43,9 @@ export = CompressionPlugin; /** * @typedef {boolean | "keep-source-map" | ((name: string) => boolean)} DeleteOriginalAssets */ +/** + * @typedef {undefined | boolean | number} Parallel + */ /** * @template T * @typedef {object} BasePluginOptions @@ -49,6 +56,7 @@ export = CompressionPlugin; * @property {number=} minRatio only assets that compress better than this ratio are processed (`minRatio = Compressed Size / Original Size`) * @property {DeleteOriginalAssets=} deleteOriginalAssets whether to delete the original assets or not * @property {Filename=} filename the target asset filename + * @property {Parallel=} parallel enables parallel compression */ /** * @typedef {import("zlib").ZlibOptions} ZlibOptions @@ -68,6 +76,12 @@ export = CompressionPlugin; declare class CompressionPlugin implements WebpackPluginInstance { + /** + * @private + * @param {Parallel} parallel value of the `parallel` option + * @returns {number} number of cores for parallelism + */ + private static getAvailableNumberOfCores; /** * @param {(BasePluginOptions & DefinedDefaultAlgorithmAndOptions)=} options options */ @@ -97,6 +111,7 @@ declare class CompressionPlugin * @param {Compiler} compiler compiler * @param {Compilation} compilation compilation * @param {Record} assets assets + * @param {{ availableNumberOfCores: number }} optimizeOptions optimize options * @returns {Promise} */ private compress; @@ -117,16 +132,19 @@ declare namespace CompressionPlugin { Source, Asset, WebpackError, + JestWorker, WithImplicitCoercion, Rule, Rules, EXPECTED_ANY, CustomOptions, + CompressionWorker, InferDefaultType, CompressionOptions, AlgorithmFunction, Filename, DeleteOriginalAssets, + Parallel, BasePluginOptions, ZlibOptions, DefinedDefaultAlgorithmAndOptions, @@ -142,6 +160,7 @@ type Compilation = import("webpack").Compilation; type Source = import("webpack").sources.Source; type Asset = import("webpack").Asset; type WebpackError = import("webpack").WebpackError; +type JestWorker = import("jest-worker").Worker; type WithImplicitCoercion = | T | { @@ -153,6 +172,18 @@ type EXPECTED_ANY = any; type CustomOptions = { [key: string]: EXPECTED_ANY; }; +type CompressionWorker = JestWorker & { + transform: (options: string) => Promise<{ + compressedBase64: string; + }>; + compress: (options: { + inputBase64: string; + algorithm: string; + compressionOptions: CustomOptions; + }) => Promise<{ + compressedBase64: string; + }>; +}; type InferDefaultType = T extends infer U ? U : CustomOptions; type CompressionOptions = InferDefaultType; type AlgorithmFunction = ( @@ -176,6 +207,7 @@ type DeleteOriginalAssets = | boolean | "keep-source-map" | ((name: string) => boolean); +type Parallel = undefined | boolean | number; type BasePluginOptions = { /** * include all assets that pass test assertion @@ -205,6 +237,10 @@ type BasePluginOptions = { * the target asset filename */ filename?: Filename | undefined; + /** + * enables parallel compression + */ + parallel?: Parallel | undefined; }; type ZlibOptions = import("zlib").ZlibOptions; type DefinedDefaultAlgorithmAndOptions = T extends ZlibOptions