diff --git a/.changeset/stale-trainers-heal.md b/.changeset/stale-trainers-heal.md new file mode 100644 index 00000000000..9eae1fef749 --- /dev/null +++ b/.changeset/stale-trainers-heal.md @@ -0,0 +1,5 @@ +--- +'@shopify/stylelint-polaris': minor +--- + +Created `polaris/declaration-property-value-disallowed-list` rule to ignore failures in `@font-face` at-rules diff --git a/stylelint-polaris/index.js b/stylelint-polaris/index.js index d61617a8ebb..41dea5571e9 100644 --- a/stylelint-polaris/index.js +++ b/stylelint-polaris/index.js @@ -88,7 +88,7 @@ const stylelintPolarisCoverageOptions = { ], typography: [ { - 'declaration-property-value-disallowed-list': { + 'polaris/declaration-property-value-disallowed-list': { 'font-weight': [/(\$.*|[0-9]+)/], }, 'declaration-property-unit-disallowed-list': [ @@ -487,6 +487,7 @@ module.exports = { './plugins/at-rule-disallowed-list', './plugins/custom-property-allowed-list', './plugins/media-query-allowed-list', + './plugins/declaration-property-value-disallowed-list', ], rules: { 'polaris/coverage': stylelintPolarisCoverageOptions, diff --git a/stylelint-polaris/plugins/declaration-property-value-disallowed-list/index.js b/stylelint-polaris/plugins/declaration-property-value-disallowed-list/index.js new file mode 100644 index 00000000000..cfc2e0f8cbd --- /dev/null +++ b/stylelint-polaris/plugins/declaration-property-value-disallowed-list/index.js @@ -0,0 +1,63 @@ +const stylelint = require('stylelint'); + +const { + isRegExp, + isString, + validateObjectWithArrayProps, +} = require('../../utils'); + +const ruleName = 'polaris/declaration-property-value-disallowed-list'; + +/** + * @typedef {{ + * [property: string]: string | RegExp | (string | RegExp)[] + * }} PrimaryOptions + */ + +/** + * Wrapper for the Stylelint `declaration-property-value-disallowed-list` rule + * that ignores failures in `@font-face` at-rules. + */ +const {rule} = stylelint.createPlugin( + ruleName, + /** @param {PrimaryOptions} primary */ + (primary) => { + return (root, result) => { + const validOptions = stylelint.utils.validateOptions(result, ruleName, { + actual: primary, + possible: [validateObjectWithArrayProps(isString, isRegExp)], + }); + + if (!validOptions) return; + + stylelint.utils.checkAgainstRule( + { + ruleName: 'declaration-property-value-disallowed-list', + ruleSettings: primary, + root, + }, + (warning) => { + if ( + warning.node.type === 'decl' && + warning.node.parent.type === 'atrule' && + warning.node.parent.name === 'font-face' + ) { + return; + } + + stylelint.utils.report({ + ruleName, + result, + node: warning.node, + message: warning.text, + }); + }, + ); + }; + }, +); + +module.exports = { + rule, + ruleName, +}; diff --git a/stylelint-polaris/plugins/declaration-property-value-disallowed-list/index.test.js b/stylelint-polaris/plugins/declaration-property-value-disallowed-list/index.test.js new file mode 100644 index 00000000000..392d91e4fd2 --- /dev/null +++ b/stylelint-polaris/plugins/declaration-property-value-disallowed-list/index.test.js @@ -0,0 +1,27 @@ +const {ruleName} = require('.'); + +const config = { + 'font-weight': [/(\$.*|[0-9]+)/], +}; + +testRule({ + ruleName, + plugins: [__dirname], + config, + customSyntax: 'postcss-scss', + accept: [ + { + code: '@font-face { font-weight: 400; }', + description: '@font-face descriptors are ignored', + }, + ], + + reject: [ + { + code: '.class { font-weight: 400; }', + description: 'Not using a Polaris custom property', + message: + 'Unexpected value "400" for property "font-weight" (declaration-property-value-disallowed-list)', + }, + ], +}); diff --git a/stylelint-polaris/utils/index.js b/stylelint-polaris/utils/index.js index 8b2a385e99f..b9ac61ae62f 100644 --- a/stylelint-polaris/utils/index.js +++ b/stylelint-polaris/utils/index.js @@ -230,6 +230,34 @@ function isString(value) { return typeof value === 'string' || value instanceof String; } +/** + * Check whether the variable is an object and all its properties are one or more values + * that satisfy the specified validator(s): + * + * @example + * ignoreProperties = { + * value1: ["item11", "item12", "item13"], + * value2: "item2", + * }; + * validateObjectWithArrayProps(isString)(ignoreProperties); + * //=> true + * + * @typedef {(value: unknown) => boolean} Validator + * @param {...Validator} validators + * @returns {Validator} + */ +function validateObjectWithArrayProps(...validators) { + return (value) => { + if (!isPlainObject(value)) { + return false; + } + + return Object.values(value) + .flat() + .every((item) => validators.some((validator) => validator(item))); + }; +} + /** * Returns the arguments expected by Stylelint rules that support functional custom messages * @param {string} ruleName The category's default message @@ -259,6 +287,7 @@ module.exports.isPlainObject = isPlainObject; module.exports.isRegExp = isRegExp; module.exports.isScssInterpolation = isScssInterpolation; module.exports.isString = isString; +module.exports.validateObjectWithArrayProps = validateObjectWithArrayProps; module.exports.matchesStringOrRegExp = matchesStringOrRegExp; module.exports.scssInterpolationExpression = scssInterpolationExpression; module.exports.scssInterpolationRegExp = scssInterpolationRegExp;