From 69ce2e43abcb56dc767ecbd536bc21e74f901971 Mon Sep 17 00:00:00 2001 From: sheo13666q Date: Sun, 18 Oct 2026 13:16:03 +0000 Subject: [PATCH 1/2] perf: validate options with a precompiled check webpack validates the plugin options with `compiler.validate`, which without a check loads `ajv` and compiles `options.json`: about 200ms of every production build start, since webpack adds this plugin as its default minimizer. `src/options.check.js` is that schema precompiled with `schema-utils precompile`, passed as the fourth argument to `compiler.validate` and to the `schema-utils` fallback. Valid options are only checked; `ajv` still reports what is wrong with invalid ones, so the errors stay the same. `npm run fix:schema` regenerates the check and `lint:schema` fails when it is out of date with the schema. Babel copies it as is, it is already written for Node.js 10.13.0 and the transforms made it three times the size. Every case of the validation tests now also asserts that the check agrees with the schema. `schema-utils` goes to 4.6.0 for the runtime helpers the check loads, which also changes how two error messages print the property types. --- .changeset/precompiled-options-check.md | 5 ++++ .cspell.json | 1 + .prettierignore | 1 + babel.config.js | 3 ++ eslint.config.mjs | 2 ++ jest.config.js | 10 ++++++- package-lock.json | 27 ++++++++++-------- package.json | 6 ++-- src/index.js | 4 +++ src/options.check.js | 7 +++++ .../validate-options.test.js.snap | 20 ++++++------- test/validate-options.test.js | 28 +++++++++++++++++-- types/options.check.d.ts | 16 +++++++++++ 13 files changed, 103 insertions(+), 27 deletions(-) create mode 100644 .changeset/precompiled-options-check.md create mode 100644 src/options.check.js create mode 100644 types/options.check.d.ts diff --git a/.changeset/precompiled-options-check.md b/.changeset/precompiled-options-check.md new file mode 100644 index 00000000..ccf2eeda --- /dev/null +++ b/.changeset/precompiled-options-check.md @@ -0,0 +1,5 @@ +--- +"minimizer-webpack-plugin": patch +--- + +perf: validate options with a check precompiled by `schema-utils precompile`, so valid options no longer load `ajv` and compile the schema, about 200ms off every production webpack start diff --git a/.cspell.json b/.cspell.json index f9015f19..2f3d2cb3 100644 --- a/.cspell.json +++ b/.cspell.json @@ -80,6 +80,7 @@ "CHANGELOG.md", "package.json", "src/serialize-javascript.js", + "src/options.check.js", "dist/**", "**/__snapshots__/**", "package-lock.json", diff --git a/.prettierignore b/.prettierignore index d704fb83..42f11332 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ src/serialize-javascript.js +src/options.check.js /coverage /dist /node_modules diff --git a/babel.config.js b/babel.config.js index 65000d45..e5de7f98 100644 --- a/babel.config.js +++ b/babel.config.js @@ -5,6 +5,9 @@ module.exports = (api) => { api.cache(true); return { + // Generated by `schema-utils precompile` for Node.js 10.13.0 already, so it + // is copied as is rather than grown three times by the transforms + ignore: ["src/options.check.js"], presets: [ [ "@babel/preset-env", diff --git a/eslint.config.mjs b/eslint.config.mjs index 04ac47ec..23b6ae49 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,6 +2,8 @@ import { defineConfig } from "eslint/config"; import configs from "eslint-config-webpack/configs.js"; export default defineConfig([ + // Generated by `schema-utils precompile`, `npm run fix:schema` updates it + { ignores: ["src/options.check.js"] }, { ignores: ["./src/serialize-javascript.js"], extends: [configs["recommended-dirty"]], diff --git a/jest.config.js b/jest.config.js index ff94a264..8a2e605c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -37,7 +37,15 @@ module.exports = { // older Node on macOS), where webpack + multiple minimizers per asset // routinely take longer than that. testTimeout: 60000, - coveragePathIgnorePatterns: ["src/serialize-javascript.js"], + coveragePathIgnorePatterns: [ + "src/serialize-javascript.js", + "src/options.check.js", + ], + // Babel ignores the generated check, see `babel.config.js` + transformIgnorePatterns: [ + "/node_modules/", + "/src/options\\.check\\.js$", + ], snapshotSerializers: ["/test/helpers/snapshotHashSerializer.js"], testPathIgnorePatterns, }; diff --git a/package-lock.json b/package-lock.json index 0a672192..08ca964c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "minimizer-webpack-plugin", - "version": "5.12.0", + "version": "5.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "minimizer-webpack-plugin", - "version": "5.12.0", + "version": "5.13.0", "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "jest-worker": "^27.4.5", - "schema-utils": "^4.3.3", + "schema-utils": "^4.6.0", "terser": "^5.51.0" }, "devDependencies": { @@ -7872,9 +7872,9 @@ } }, "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -19153,16 +19153,19 @@ } }, "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.6.0.tgz", + "integrity": "sha512-q8cvkEp4bRQdWcLvB3vmxpZAlS25Hxws10nCK+rUxUfuB7e430dYZZJ5z5yAoOGBbbDtve8cUg8DFfc3ue12TA==", "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", "ajv-keywords": "^5.1.0" }, + "bin": { + "schema-utils": "bin/schema-utils.js" + }, "engines": { "node": ">= 10.13.0" }, diff --git a/package.json b/package.json index e7d69e87..e17a0bc2 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "build": "npm-run-all -p \"build:**\"", "security": "npm audit --production", "lint:serialize-javascript": "node ./scripts/copy-serialize-javascript.js --check", + "lint:schema": "schema-utils precompile --check src/options.json", "lint:prettier": "prettier --list-different .", "lint:code": "eslint --cache .", "lint:spelling": "cspell \"**/*.*\"", @@ -70,7 +71,8 @@ "lint": "npm-run-all -l -p \"lint:**\"", "fix:code": "npm run lint:code -- --fix", "fix:prettier": "npm run lint:prettier -- --write", - "fix": "npm-run-all -l fix:code fix:prettier", + "fix:schema": "schema-utils precompile src/options.json", + "fix": "npm-run-all -l fix:schema fix:code fix:prettier", "test:base": "node ./scripts/jest.js", "test:watch": "npm run test:base -- --watch", "test:coverage": "npm run test:base -- --collectCoverageFrom=\"src/**/*.js\" --coverage", @@ -83,7 +85,7 @@ "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "jest-worker": "^27.4.5", - "schema-utils": "^4.3.3", + "schema-utils": "^4.6.0", "terser": "^5.51.0" }, "devDependencies": { diff --git a/src/index.js b/src/index.js index 77012d34..aa6a83ad 100644 --- a/src/index.js +++ b/src/index.js @@ -2413,10 +2413,13 @@ class MinimizerPlugin { */ validateOptions(compiler) { if (typeof compiler.validate === "function") { + // The precompiled check validates valid options without `ajv`, webpack + // only compiles the schema to report what is wrong with invalid ones compiler.validate( () => /** @type {Schema} */ (require("./options.json")), this.rawOptions, VALIDATION_CONFIGURATION, + require("./options.check.js"), ); this.validateMinimizers(); this.validateGenerators(); @@ -2432,6 +2435,7 @@ class MinimizerPlugin { /** @type {Schema} */ (require("./options.json")), this.rawOptions, VALIDATION_CONFIGURATION, + require("./options.check.js"), ); this.validateMinimizers(); this.validateGenerators(); diff --git a/src/options.check.js b/src/options.check.js new file mode 100644 index 00000000..0f1de412 --- /dev/null +++ b/src/options.check.js @@ -0,0 +1,7 @@ +/* eslint-disable */ +// @ts-nocheck +/* + * This file was automatically generated by "schema-utils". + * DO NOT MODIFY BY HAND. + */ +"use strict";module.exports = validate11;module.exports.default = validate11;const schema28 = {"definitions":{"Rule":{"anyOf":[{"instanceof":"RegExp"},{"type":"string","minLength":1}]},"Rules":{"anyOf":[{"type":"array","items":{"oneOf":[{"$ref":"#/definitions/Rule"}]}},{"$ref":"#/definitions/Rule"}]},"MinimizerImplementation":{"anyOf":[{"instanceof":"Function"},{"type":"string","minLength":1},{"type":"object","additionalProperties":false,"properties":{"path":{"type":"string","minLength":1},"export":{"type":"string","minLength":1}},"required":["path"]}]}},"type":"object","additionalProperties":false,"properties":{"test":{"oneOf":[{"$ref":"#/definitions/Rules"}]},"include":{"oneOf":[{"$ref":"#/definitions/Rules"}]},"exclude":{"oneOf":[{"$ref":"#/definitions/Rules"}]},"minimizerOptions":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"array","minItems":1,"items":{"additionalProperties":true,"type":"object"}}]},"terserOptions":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"array","minItems":1,"items":{"additionalProperties":true,"type":"object"}}]},"extractComments":{"anyOf":[{"type":"boolean"},{"type":"string","minLength":1},{"instanceof":"RegExp"},{"instanceof":"Function"},{"additionalProperties":false,"properties":{"condition":{"anyOf":[{"type":"boolean"},{"type":"string","minLength":1},{"instanceof":"RegExp"},{"instanceof":"Function"}]},"filename":{"anyOf":[{"type":"string","minLength":1},{"instanceof":"Function"}]},"banner":{"anyOf":[{"type":"boolean"},{"type":"string","minLength":1},{"instanceof":"Function"}]}},"type":"object"}]},"parallel":{"anyOf":[{"type":"boolean"},{"type":"integer"}]},"minify":{"anyOf":[{"$ref":"#/definitions/MinimizerImplementation"},{"type":"array","items":{"anyOf":[{"$ref":"#/definitions/MinimizerImplementation"},{"type":"object","additionalProperties":false,"properties":{"implementation":{"$ref":"#/definitions/MinimizerImplementation"},"options":{"type":"object","additionalProperties":true},"filter":{"instanceof":"Function"}},"required":["implementation"]}]}},{"type":"object","additionalProperties":false,"properties":{"implementation":{"$ref":"#/definitions/MinimizerImplementation"},"options":{"type":"object","additionalProperties":true},"filter":{"instanceof":"Function"}},"required":["implementation"]}]},"generate":{"anyOf":[{"instanceof":"Function"},{"type":"array","minItems":1,"items":{"instanceof":"Function"}},{"type":"object","additionalProperties":false,"properties":{"implementation":{"instanceof":"Function"},"options":{"type":"object","additionalProperties":true},"type":{"enum":["import","asset"]},"filename":{"anyOf":[{"type":"string","minLength":1},{"instanceof":"Function"}]},"filter":{"instanceof":"Function"},"deleteOriginalAssets":{"anyOf":[{"type":"boolean"},{"instanceof":"Function"}]},"threshold":{"type":"number","minimum":0},"minRatio":{"type":"number","exclusiveMinimum":0},"relatedName":{"anyOf":[{"type":"string","minLength":1},{"enum":[false]}]}},"required":["implementation"]},{"type":"object","minProperties":1,"additionalProperties":{"anyOf":[{"instanceof":"Function"},{"type":"array","minItems":1,"items":{"instanceof":"Function"}},{"type":"object","additionalProperties":false,"properties":{"implementation":{"instanceof":"Function"},"options":{"type":"object","additionalProperties":true},"type":{"enum":["import","asset"]},"filename":{"anyOf":[{"type":"string","minLength":1},{"instanceof":"Function"}]},"filter":{"instanceof":"Function"},"deleteOriginalAssets":{"anyOf":[{"type":"boolean"},{"instanceof":"Function"}]},"threshold":{"type":"number","minimum":0},"minRatio":{"type":"number","exclusiveMinimum":0},"relatedName":{"anyOf":[{"type":"string","minLength":1},{"enum":[false]}]}},"required":["implementation"]}]}}]},"generatorOptions":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"array","minItems":1,"items":{"additionalProperties":true,"type":"object"}}]}},"$id":"options.json"};const schema32 = {"anyOf":[{"instanceof":"Function"},{"type":"string","minLength":1},{"type":"object","additionalProperties":false,"properties":{"path":{"type":"string","minLength":1},"export":{"type":"string","minLength":1}},"required":["path"]}]};const func2 = Object.prototype.hasOwnProperty;const func3 = require("schema-utils/dist/runtime").ucs2length;const schema29 = {"anyOf":[{"type":"array","items":{"oneOf":[{"$ref":"#/definitions/Rule"}]}},{"$ref":"#/definitions/Rule"}]};const schema30 = {"anyOf":[{"instanceof":"RegExp"},{"type":"string","minLength":1}]};function validate12(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;const _errs0 = errors;let valid0 = false;const _errs1 = errors;if(errors === _errs1){if(Array.isArray(data)){var valid1 = true;const len0 = data.length;for(let i0=0; i0=", limit: 0}};if(vErrors === null){vErrors = [err113];}else {vErrors.push(err113);}errors++;}}else {const err114 = {instancePath:instancePath+"/generate/threshold",schemaPath:"#/properties/generate/anyOf/2/properties/threshold/type",keyword:"type",params:{type: "number"}};if(vErrors === null){vErrors = [err114];}else {vErrors.push(err114);}errors++;}}var valid33 = _errs165 === errors;}else {var valid33 = true;}if(valid33){if(data28.minRatio !== undefined){let data37 = data28.minRatio;const _errs167 = errors;if(errors === _errs167){if(typeof data37 == "number"){if(data37 <= 0 || isNaN(data37)){const err115 = {instancePath:instancePath+"/generate/minRatio",schemaPath:"#/properties/generate/anyOf/2/properties/minRatio/exclusiveMinimum",keyword:"exclusiveMinimum",params:{comparison: ">", limit: 0}};if(vErrors === null){vErrors = [err115];}else {vErrors.push(err115);}errors++;}}else {const err116 = {instancePath:instancePath+"/generate/minRatio",schemaPath:"#/properties/generate/anyOf/2/properties/minRatio/type",keyword:"type",params:{type: "number"}};if(vErrors === null){vErrors = [err116];}else {vErrors.push(err116);}errors++;}}var valid33 = _errs167 === errors;}else {var valid33 = true;}if(valid33){if(data28.relatedName !== undefined){let data38 = data28.relatedName;const _errs169 = errors;const _errs170 = errors;let valid36 = false;const _errs171 = errors;if(errors === _errs171){if(typeof data38 === "string"){if(func3(data38) < 1){const err117 = {instancePath:instancePath+"/generate/relatedName",schemaPath:"#/properties/generate/anyOf/2/properties/relatedName/anyOf/0/minLength",keyword:"minLength",params:{limit: 1}};if(vErrors === null){vErrors = [err117];}else {vErrors.push(err117);}errors++;}}else {const err118 = {instancePath:instancePath+"/generate/relatedName",schemaPath:"#/properties/generate/anyOf/2/properties/relatedName/anyOf/0/type",keyword:"type",params:{type: "string"}};if(vErrors === null){vErrors = [err118];}else {vErrors.push(err118);}errors++;}}var _valid19 = _errs171 === errors;valid36 = valid36 || _valid19;if(!valid36){const _errs173 = errors;if(!(data38 === false)){const err119 = {instancePath:instancePath+"/generate/relatedName",schemaPath:"#/properties/generate/anyOf/2/properties/relatedName/anyOf/1/enum",keyword:"enum",params:{allowedValues: schema28.properties.generate.anyOf[2].properties.relatedName.anyOf[1].enum}};if(vErrors === null){vErrors = [err119];}else {vErrors.push(err119);}errors++;}var _valid19 = _errs173 === errors;valid36 = valid36 || _valid19;}if(!valid36){const err120 = {instancePath:instancePath+"/generate/relatedName",schemaPath:"#/properties/generate/anyOf/2/properties/relatedName/anyOf",keyword:"anyOf",params:{}};if(vErrors === null){vErrors = [err120];}else {vErrors.push(err120);}errors++;}else {errors = _errs170;if(vErrors !== null){if(_errs170){vErrors.length = _errs170;}else {vErrors = null;}}}var valid33 = _errs169 === errors;}else {var valid33 = true;}}}}}}}}}}}}else {const err121 = {instancePath:instancePath+"/generate",schemaPath:"#/properties/generate/anyOf/2/type",keyword:"type",params:{type: "object"}};if(vErrors === null){vErrors = [err121];}else {vErrors.push(err121);}errors++;}}var _valid16 = _errs146 === errors;valid31 = valid31 || _valid16;if(!valid31){const _errs174 = errors;if(errors === _errs174){if(data28 && typeof data28 == "object" && !Array.isArray(data28)){if(Object.keys(data28).length < 1){const err122 = {instancePath:instancePath+"/generate",schemaPath:"#/properties/generate/anyOf/3/minProperties",keyword:"minProperties",params:{limit: 1}};if(vErrors === null){vErrors = [err122];}else {vErrors.push(err122);}errors++;}else {for(const key9 in data28){let data39 = data28[key9];const _errs177 = errors;const _errs178 = errors;let valid38 = false;const _errs179 = errors;if(!(data39 instanceof Function)){const err123 = {instancePath:instancePath+"/generate/" + key9.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/generate/anyOf/3/additionalProperties/anyOf/0/instanceof",keyword:"instanceof",params:{}};if(vErrors === null){vErrors = [err123];}else {vErrors.push(err123);}errors++;}var _valid20 = _errs179 === errors;valid38 = valid38 || _valid20;if(!valid38){const _errs180 = errors;if(errors === _errs180){if(Array.isArray(data39)){if(data39.length < 1){const err124 = {instancePath:instancePath+"/generate/" + key9.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/generate/anyOf/3/additionalProperties/anyOf/1/minItems",keyword:"minItems",params:{limit: 1}};if(vErrors === null){vErrors = [err124];}else {vErrors.push(err124);}errors++;}else {var valid39 = true;const len4 = data39.length;for(let i4=0; i4=", limit: 0}};if(vErrors === null){vErrors = [err140];}else {vErrors.push(err140);}errors++;}}else {const err141 = {instancePath:instancePath+"/generate/" + key9.replace(/~/g, "~0").replace(/\//g, "~1")+"/threshold",schemaPath:"#/properties/generate/anyOf/3/additionalProperties/anyOf/2/properties/threshold/type",keyword:"type",params:{type: "number"}};if(vErrors === null){vErrors = [err141];}else {vErrors.push(err141);}errors++;}}var valid40 = _errs202 === errors;}else {var valid40 = true;}if(valid40){if(data39.minRatio !== undefined){let data48 = data39.minRatio;const _errs204 = errors;if(errors === _errs204){if(typeof data48 == "number"){if(data48 <= 0 || isNaN(data48)){const err142 = {instancePath:instancePath+"/generate/" + key9.replace(/~/g, "~0").replace(/\//g, "~1")+"/minRatio",schemaPath:"#/properties/generate/anyOf/3/additionalProperties/anyOf/2/properties/minRatio/exclusiveMinimum",keyword:"exclusiveMinimum",params:{comparison: ">", limit: 0}};if(vErrors === null){vErrors = [err142];}else {vErrors.push(err142);}errors++;}}else {const err143 = {instancePath:instancePath+"/generate/" + key9.replace(/~/g, "~0").replace(/\//g, "~1")+"/minRatio",schemaPath:"#/properties/generate/anyOf/3/additionalProperties/anyOf/2/properties/minRatio/type",keyword:"type",params:{type: "number"}};if(vErrors === null){vErrors = [err143];}else {vErrors.push(err143);}errors++;}}var valid40 = _errs204 === errors;}else {var valid40 = true;}if(valid40){if(data39.relatedName !== undefined){let data49 = data39.relatedName;const _errs206 = errors;const _errs207 = errors;let valid43 = false;const _errs208 = errors;if(errors === _errs208){if(typeof data49 === "string"){if(func3(data49) < 1){const err144 = {instancePath:instancePath+"/generate/" + key9.replace(/~/g, "~0").replace(/\//g, "~1")+"/relatedName",schemaPath:"#/properties/generate/anyOf/3/additionalProperties/anyOf/2/properties/relatedName/anyOf/0/minLength",keyword:"minLength",params:{limit: 1}};if(vErrors === null){vErrors = [err144];}else {vErrors.push(err144);}errors++;}}else {const err145 = {instancePath:instancePath+"/generate/" + key9.replace(/~/g, "~0").replace(/\//g, "~1")+"/relatedName",schemaPath:"#/properties/generate/anyOf/3/additionalProperties/anyOf/2/properties/relatedName/anyOf/0/type",keyword:"type",params:{type: "string"}};if(vErrors === null){vErrors = [err145];}else {vErrors.push(err145);}errors++;}}var _valid23 = _errs208 === errors;valid43 = valid43 || _valid23;if(!valid43){const _errs210 = errors;if(!(data49 === false)){const err146 = {instancePath:instancePath+"/generate/" + key9.replace(/~/g, "~0").replace(/\//g, "~1")+"/relatedName",schemaPath:"#/properties/generate/anyOf/3/additionalProperties/anyOf/2/properties/relatedName/anyOf/1/enum",keyword:"enum",params:{allowedValues: schema28.properties.generate.anyOf[3].additionalProperties.anyOf[2].properties.relatedName.anyOf[1].enum}};if(vErrors === null){vErrors = [err146];}else {vErrors.push(err146);}errors++;}var _valid23 = _errs210 === errors;valid43 = valid43 || _valid23;}if(!valid43){const err147 = {instancePath:instancePath+"/generate/" + key9.replace(/~/g, "~0").replace(/\//g, "~1")+"/relatedName",schemaPath:"#/properties/generate/anyOf/3/additionalProperties/anyOf/2/properties/relatedName/anyOf",keyword:"anyOf",params:{}};if(vErrors === null){vErrors = [err147];}else {vErrors.push(err147);}errors++;}else {errors = _errs207;if(vErrors !== null){if(_errs207){vErrors.length = _errs207;}else {vErrors = null;}}}var valid40 = _errs206 === errors;}else {var valid40 = true;}}}}}}}}}}}}else {const err148 = {instancePath:instancePath+"/generate/" + key9.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/generate/anyOf/3/additionalProperties/anyOf/2/type",keyword:"type",params:{type: "object"}};if(vErrors === null){vErrors = [err148];}else {vErrors.push(err148);}errors++;}}var _valid20 = _errs183 === errors;valid38 = valid38 || _valid20;}}if(!valid38){const err149 = {instancePath:instancePath+"/generate/" + key9.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/generate/anyOf/3/additionalProperties/anyOf",keyword:"anyOf",params:{}};if(vErrors === null){vErrors = [err149];}else {vErrors.push(err149);}errors++;}else {errors = _errs178;if(vErrors !== null){if(_errs178){vErrors.length = _errs178;}else {vErrors = null;}}}var valid37 = _errs177 === errors;if(!valid37){break;}}}}else {const err150 = {instancePath:instancePath+"/generate",schemaPath:"#/properties/generate/anyOf/3/type",keyword:"type",params:{type: "object"}};if(vErrors === null){vErrors = [err150];}else {vErrors.push(err150);}errors++;}}var _valid16 = _errs174 === errors;valid31 = valid31 || _valid16;}}}if(!valid31){const err151 = {instancePath:instancePath+"/generate",schemaPath:"#/properties/generate/anyOf",keyword:"anyOf",params:{}};if(vErrors === null){vErrors = [err151];}else {vErrors.push(err151);}errors++;validate11.errors = vErrors;return false;}else {errors = _errs141;if(vErrors !== null){if(_errs141){vErrors.length = _errs141;}else {vErrors = null;}}}var valid0 = _errs140 === errors;}else {var valid0 = true;}if(valid0){if(data.generatorOptions !== undefined){let data50 = data.generatorOptions;const _errs211 = errors;const _errs212 = errors;let valid44 = false;const _errs213 = errors;if(errors === _errs213){if(data50 && typeof data50 == "object" && !Array.isArray(data50)){}else {const err152 = {instancePath:instancePath+"/generatorOptions",schemaPath:"#/properties/generatorOptions/anyOf/0/type",keyword:"type",params:{type: "object"}};if(vErrors === null){vErrors = [err152];}else {vErrors.push(err152);}errors++;}}var _valid24 = _errs213 === errors;valid44 = valid44 || _valid24;if(!valid44){const _errs216 = errors;if(errors === _errs216){if(Array.isArray(data50)){if(data50.length < 1){const err153 = {instancePath:instancePath+"/generatorOptions",schemaPath:"#/properties/generatorOptions/anyOf/1/minItems",keyword:"minItems",params:{limit: 1}};if(vErrors === null){vErrors = [err153];}else {vErrors.push(err153);}errors++;}else {var valid45 = true;const len5 = data50.length;for(let i5=0; i5 Allows you to override default minify function. Written as an object it states how to run one minimizer, options included. An empty array is no minimizers at all, for an instance whose whole job is its \`generate\`. A string or \`{ path, export }\` loads the minimizer by module path in workers (like sass-loader \`implementation\`). -> Read more at https://github.com/webpack/minimizer-webpack-plugin#number Details: * options.minify should be one of these: - function | non-empty string | object { path, export? } + function | non-empty string | object { path: non-empty string, export?: non-empty string } -> The minimizer itself: a function, a module path string (worker \`require\`s it), or \`{ path, export }\` for a named export. Details: * options.minify should be an instance of function. * options.minify should be a non-empty string. * options.minify should be an object: - object { path, export? } + object { path: non-empty string, export?: non-empty string } * options.minify should be an array: - [function | non-empty string | object { path, export? } | object { implementation, options?, filter? }, ...] + [function | non-empty string | object { path: non-empty string, export?: non-empty string } | object { implementation, options?, filter?: function }, ...] * options.minify should be an object: - object { implementation, options?, filter? }" + object { implementation, options?, filter?: function }" `; exports[`validation validate 10`] = ` @@ -247,20 +247,20 @@ exports[`validation validate 18`] = ` exports[`validation validate 19`] = ` "Invalid options object. Minimizer Plugin has been initialized using an options object that does not match the API schema. - options.minify should be one of these: - function | non-empty string | object { path, export? } | [function | non-empty string | object { path, export? } | object { implementation, options?, filter? }, ...] | object { implementation, options?, filter? } + function | non-empty string | object { path: non-empty string, export?: non-empty string } | [function | non-empty string | object { path: non-empty string, export?: non-empty string } | object { implementation, options?, filter?: function }, ...] | object { implementation, options?, filter?: function } -> Allows you to override default minify function. Written as an object it states how to run one minimizer, options included. An empty array is no minimizers at all, for an instance whose whole job is its \`generate\`. A string or \`{ path, export }\` loads the minimizer by module path in workers (like sass-loader \`implementation\`). -> Read more at https://github.com/webpack/minimizer-webpack-plugin#number Details: * options.minify should be one of these: - function | non-empty string | object { path, export? } + function | non-empty string | object { path: non-empty string, export?: non-empty string } -> The minimizer itself: a function, a module path string (worker \`require\`s it), or \`{ path, export }\` for a named export. Details: * options.minify should be an instance of function. * options.minify should be a non-empty string. * options.minify should be an object: - object { path, export? } + object { path: non-empty string, export?: non-empty string } * options.minify should be an array: - [function | non-empty string | object { path, export? } | object { implementation, options?, filter? }, ...] + [function | non-empty string | object { path: non-empty string, export?: non-empty string } | object { implementation, options?, filter?: function }, ...] * options.minify should be an object: - object { implementation, options?, filter? }" + object { implementation, options?, filter?: function }" `; diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 4bdbb05a..7339d2a9 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -1,13 +1,37 @@ +import { validate } from "schema-utils"; + import MinimizerPlugin from "../src"; +import check from "../src/options.check"; +import schema from "../src/options.json"; import { getCompiler } from "./helpers"; +/** + * Whether the full validation accepts the options. + * @param {EXPECTED_ANY} options plugin options + * @returns {boolean} true when valid + */ +const isValid = (options) => { + try { + validate(/** @type {EXPECTED_ANY} */ (schema), options); + } catch (_err) { + return false; + } + + return true; +}; + /** * @param {EXPECTED_ANY} options plugin options * @returns {import("webpack").Compiler} compiler */ -const createCompiler = (options) => - getCompiler({ plugins: [new MinimizerPlugin(options)] }); +const createCompiler = (options) => { + // The precompiled check has to agree with the schema it was compiled from, + // a valid value is never validated against the schema itself + expect(check(options === undefined ? {} : options)).toBe(isValid(options)); + + return getCompiler({ plugins: [new MinimizerPlugin(options)] }); +}; describe("validation", () => { it("validate", () => { diff --git a/types/options.check.d.ts b/types/options.check.d.ts new file mode 100644 index 00000000..4b1c1f8b --- /dev/null +++ b/types/options.check.d.ts @@ -0,0 +1,16 @@ +export = validate11; +declare function validate11( + data: any, + { + instancePath, + parentData, + parentDataProperty, + rootData, + }?: { + instancePath?: string | undefined; + rootData?: any; + }, +): boolean; +declare namespace validate11 { + export { validate11 as default }; +} From 5a1e8dad89b6c9e0fbc5ef919eb3ecf7e58fa338 Mon Sep 17 00:00:00 2001 From: sheo13666q Date: Sun, 18 Oct 2026 13:35:03 +0000 Subject: [PATCH 2/2] test: compare CSS declarations regardless of order webpack 5.112's CSS minimizer emits declarations in a different order, so the two embedded-source tests that compared the stylesheet verbatim failed on the rows that install webpack@latest (Node 10/12/14). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QYPkbvwyRWt2SHgXDoYRdo --- test/embedded-source.test.js | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/test/embedded-source.test.js b/test/embedded-source.test.js index cd2b15ac..f99173c1 100644 --- a/test/embedded-source.test.js +++ b/test/embedded-source.test.js @@ -26,6 +26,18 @@ const htmlMinify = require(WEBPACK_HTML_MINIFY); const fixture = (name) => path.resolve(__dirname, "./fixtures/embedded", name); +/** + * A one-rule stylesheet with its declarations sorted: webpack 5.112's CSS + * minimizer emits them in a different order than 5.110 and 5.111 did. + * @param {string} css minified stylesheet + * @returns {string} the stylesheet with sorted declarations + */ +const sortDeclarations = (css) => { + const [, selector, body] = /^([^{]+)\{(.*)\}$/.exec(css); + + return `${selector}{${body.split(";").sort().join(";")}}`; +}; + /** * The three minify functions webpack's own `optimization.minimize` wires into * one plugin instance, which is what makes this work out of the box. @@ -642,8 +654,10 @@ describe("embedded source", () => { const stats = await compile(compiler); expect(getErrors(stats)).toEqual([]); - expect(readAsset("sheet.css", compiler, stats)).toBe( - ".a{background:url(\"data:image/svg+xml, \");color:red}", + expect(sortDeclarations(readAsset("sheet.css", compiler, stats))).toBe( + sortDeclarations( + ".a{background:url(\"data:image/svg+xml, \");color:red}", + ), ); }); @@ -739,8 +753,10 @@ describe("embedded source", () => { const stats = await compile(compiler); expect(getErrors(stats)).toEqual([]); - expect(readAsset("sheet.css", compiler, stats)).toBe( - ".a{background:url(\"data:image/svg+xml, \");color:red}", + expect(sortDeclarations(readAsset("sheet.css", compiler, stats))).toBe( + sortDeclarations( + ".a{background:url(\"data:image/svg+xml, \");color:red}", + ), ); }); });