From 861fc3816fdb092e892b67d04130059ff0806452 Mon Sep 17 00:00:00 2001 From: sheo13666q Date: Sun, 18 Oct 2026 15:24:55 +0000 Subject: [PATCH] perf: pass a precompiled options check to getOptions this.getOptions(schema, check) skips compiling the options schema when the check passes (webpack/webpack#22745). Older webpack versions ignore the second argument and validate as before. src/options.check.js is generated by `schema-utils precompile`; `npm run lint` fails when it is out of date with options.json. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QYPkbvwyRWt2SHgXDoYRdo --- .cspell.json | 3 ++- .prettierignore | 1 + eslint.config.mjs | 2 ++ jest.config.js | 4 ++++ package-lock.json | 28 ++++++++++++++++------------ package.json | 5 ++++- src/index.js | 3 ++- src/options.check.js | 7 +++++++ test/validate-options.test.js | 23 +++++++++++++++++++++++ 9 files changed, 61 insertions(+), 15 deletions(-) create mode 100644 src/options.check.js diff --git a/.cspell.json b/.cspell.json index feea67a3..c4ea9edb 100644 --- a/.cspell.json +++ b/.cspell.json @@ -24,6 +24,7 @@ "package-lock.json", "node_modules", "coverage", - "*.log" + "*.log", + "src/options.check.js" ] } diff --git a/.prettierignore b/.prettierignore index acd33fcf..9abe94b2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,3 +4,4 @@ /test/fixtures CHANGELOG.md docs/LOADER.md +/src/options.check.js diff --git a/eslint.config.mjs b/eslint.config.mjs index c2279752..5438ea6b 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` + { ignores: ["src/options.check.js"] }, { extends: [configs["recommended-dirty"]], }, diff --git a/jest.config.js b/jest.config.js index 492b19de..013e1913 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,10 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { testTimeout: 10000, + coveragePathIgnorePatterns: [ + "/node_modules/", + "/src/options\\.check\\.js$", + ], transform: { "\\.[mc]?tsx?$": ["ts-jest", { rootDir: ".", esModuleInterop: true }], "\\.[mc]?jsx?$": "babel-jest", diff --git a/package-lock.json b/package-lock.json index 0e2d67d9..f7013d09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "prettier": "^3.3.2", "sass": "^1.77.6", "sass-loader": "^14.2.1", + "schema-utils": "^4.6.0", "standard-version": "^9.3.2", "strip-ansi": "^7.1.0", "sugarss": "^5.0.1", @@ -5904,9 +5905,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -5921,9 +5922,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==", "dev": true, "license": "MIT", "dependencies": { @@ -19049,17 +19050,20 @@ } }, "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==", "dev": true, "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 13ecbda2..f1b3ac99 100644 --- a/package.json +++ b/package.json @@ -32,10 +32,12 @@ "lint:prettier": "prettier --cache --list-different .", "lint:code": "eslint --cache .", "lint:spelling": "cspell --cache --no-must-find-files --quiet \"**/*.*\"", + "lint:schema": "schema-utils precompile --check src/options.json", "lint": "npm-run-all -l -p \"lint:**\"", "fix:code": "npm run lint:code -- --fix", "fmt": "npm run lint:prettier -- --write", - "fix": "npm run fix:code && npm run fmt", + "fix:schema": "schema-utils precompile src/options.json", + "fix": "npm run fix:schema && npm run fix:code && npm run fmt", "test:only": "cross-env NODE_ENV=test jest", "test:watch": "npm run test:only -- --watch", "test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage", @@ -81,6 +83,7 @@ "prettier": "^3.3.2", "sass": "^1.77.6", "sass-loader": "^14.2.1", + "schema-utils": "^4.6.0", "standard-version": "^9.3.2", "strip-ansi": "^7.1.0", "sugarss": "^5.0.1", diff --git a/src/index.js b/src/index.js index 77d221af..4d237c58 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ import path from "node:path"; import postcssPackage from "postcss/package.json"; +import check from "./options.check.js"; import schema from "./options.json"; import { exec, @@ -31,7 +32,7 @@ let hasExplicitDependencyOnPostCSS = false; * @return {callback} callback Result */ export default async function loader(content, sourceMap, meta) { - const options = this.getOptions(schema); + const options = this.getOptions(schema, check); const callback = this.async(); const configOption = typeof options.postcssOptions === "undefined" || diff --git a/src/options.check.js b/src/options.check.js new file mode 100644 index 00000000..c2331dd7 --- /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 = {"type":"object","properties":{"postcssOptions":{"anyOf":[{"type":"object","additionalProperties":true,"properties":{"config":{"anyOf":[{"type":"string"},{"type":"boolean"}]}}},{"instanceof":"Function"}]},"execute":{"type":"boolean"},"sourceMap":{"type":"boolean"},"implementation":{"anyOf":[{"type":"string"},{"instanceof":"Function"}]}},"additionalProperties":false,"$id":"options.json"};function validate11(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){/*# sourceURL="options.json" */;let vErrors = null;let errors = 0;if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){const _errs1 = errors;for(const key0 in data){if(!((((key0 === "postcssOptions") || (key0 === "execute")) || (key0 === "sourceMap")) || (key0 === "implementation"))){validate11.errors = [{instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0}}];return false;break;}}if(_errs1 === errors){if(data.postcssOptions !== undefined){let data0 = data.postcssOptions;const _errs2 = errors;const _errs3 = errors;let valid1 = false;const _errs4 = errors;if(errors === _errs4){if(data0 && typeof data0 == "object" && !Array.isArray(data0)){if(data0.config !== undefined){let data1 = data0.config;const _errs8 = errors;let valid3 = false;const _errs9 = errors;if(typeof data1 !== "string"){const err0 = {instancePath:instancePath+"/postcssOptions/config",schemaPath:"#/properties/postcssOptions/anyOf/0/properties/config/anyOf/0/type",keyword:"type",params:{type: "string"}};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}var _valid1 = _errs9 === errors;valid3 = valid3 || _valid1;if(!valid3){const _errs11 = errors;if(typeof data1 !== "boolean"){const err1 = {instancePath:instancePath+"/postcssOptions/config",schemaPath:"#/properties/postcssOptions/anyOf/0/properties/config/anyOf/1/type",keyword:"type",params:{type: "boolean"}};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}var _valid1 = _errs11 === errors;valid3 = valid3 || _valid1;}if(!valid3){const err2 = {instancePath:instancePath+"/postcssOptions/config",schemaPath:"#/properties/postcssOptions/anyOf/0/properties/config/anyOf",keyword:"anyOf",params:{}};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}else {errors = _errs8;if(vErrors !== null){if(_errs8){vErrors.length = _errs8;}else {vErrors = null;}}}}}else {const err3 = {instancePath:instancePath+"/postcssOptions",schemaPath:"#/properties/postcssOptions/anyOf/0/type",keyword:"type",params:{type: "object"}};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}}var _valid0 = _errs4 === errors;valid1 = valid1 || _valid0;if(!valid1){const _errs13 = errors;if(!(data0 instanceof Function)){const err4 = {instancePath:instancePath+"/postcssOptions",schemaPath:"#/properties/postcssOptions/anyOf/1/instanceof",keyword:"instanceof",params:{}};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}var _valid0 = _errs13 === errors;valid1 = valid1 || _valid0;}if(!valid1){const err5 = {instancePath:instancePath+"/postcssOptions",schemaPath:"#/properties/postcssOptions/anyOf",keyword:"anyOf",params:{}};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;validate11.errors = vErrors;return false;}else {errors = _errs3;if(vErrors !== null){if(_errs3){vErrors.length = _errs3;}else {vErrors = null;}}}var valid0 = _errs2 === errors;}else {var valid0 = true;}if(valid0){if(data.execute !== undefined){const _errs14 = errors;if(typeof data.execute !== "boolean"){validate11.errors = [{instancePath:instancePath+"/execute",schemaPath:"#/properties/execute/type",keyword:"type",params:{type: "boolean"}}];return false;}var valid0 = _errs14 === errors;}else {var valid0 = true;}if(valid0){if(data.sourceMap !== undefined){const _errs16 = errors;if(typeof data.sourceMap !== "boolean"){validate11.errors = [{instancePath:instancePath+"/sourceMap",schemaPath:"#/properties/sourceMap/type",keyword:"type",params:{type: "boolean"}}];return false;}var valid0 = _errs16 === errors;}else {var valid0 = true;}if(valid0){if(data.implementation !== undefined){let data4 = data.implementation;const _errs18 = errors;const _errs19 = errors;let valid4 = false;const _errs20 = errors;if(typeof data4 !== "string"){const err6 = {instancePath:instancePath+"/implementation",schemaPath:"#/properties/implementation/anyOf/0/type",keyword:"type",params:{type: "string"}};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}var _valid2 = _errs20 === errors;valid4 = valid4 || _valid2;if(!valid4){const _errs22 = errors;if(!(data4 instanceof Function)){const err7 = {instancePath:instancePath+"/implementation",schemaPath:"#/properties/implementation/anyOf/1/instanceof",keyword:"instanceof",params:{}};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}var _valid2 = _errs22 === errors;valid4 = valid4 || _valid2;}if(!valid4){const err8 = {instancePath:instancePath+"/implementation",schemaPath:"#/properties/implementation/anyOf",keyword:"anyOf",params:{}};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;validate11.errors = vErrors;return false;}else {errors = _errs19;if(vErrors !== null){if(_errs19){vErrors.length = _errs19;}else {vErrors = null;}}}var valid0 = _errs18 === errors;}else {var valid0 = true;}}}}}}else {validate11.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"}}];return false;}}validate11.errors = vErrors;return errors === 0;} diff --git a/test/validate-options.test.js b/test/validate-options.test.js index ad500fa5..0490c9c1 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -1,7 +1,26 @@ import path from "node:path"; +import { validate } from "schema-utils"; + +import check from "../src/options.check"; +import schema from "../src/options.json"; + import { compile, getCompiler } from "./helpers/index"; +/** + * @param {object} options loader options + * @returns {boolean} whether the schema accepts them + */ +function isValid(options) { + try { + validate(schema, options); + + return true; + } catch { + return false; + } +} + describe("validate options", () => { const tests = { execute: { @@ -70,6 +89,10 @@ describe("validate options", () => { it(`should ${ type === "success" ? "successfully validate" : "throw an error on" } the "${key}" option with "${stringifyValue(value)}" value`, async () => { + // webpack trusts a check that passes and skips the schema, so the two + // must agree on every value + expect(check({ [key]: value })).toBe(isValid({ [key]: value })); + let compiler; if (