diff --git a/.stylelintignore b/.stylelintignore new file mode 100644 index 0000000000000..c04e935624470 --- /dev/null +++ b/.stylelintignore @@ -0,0 +1,26 @@ +# Ignore WordPress plugin files. +src/wp-content/plugins/**/* + +# Ignore WordPress theme files. +# Note that the dependencies of some themes would require the "stylelint-a11y" +# package which is not used in Core and incompatible with latest Stylelint versions. +src/wp-content/themes/**/* + +# Ignore WordPress block files. +src/wp-includes/blocks/**/* + +# Ignore WordPress included dist files. +src/wp-includes/css/dist/**/* + +# Ignore WordPress TinyMCE files. +src/wp-includes/js/tinymce/**/* + +# Ignore WordPress JS vendor files. +src/js/_enqueues/vendor/**/* + +# Ignore minified CSS files. +**/*.min.css + +# Ignore RTL CSS files. +**/*-rtl.css +**/*-rtl.min.css diff --git a/.stylelintrc.js b/.stylelintrc.js new file mode 100644 index 0000000000000..bde6311a68888 --- /dev/null +++ b/.stylelintrc.js @@ -0,0 +1,60 @@ +/** @type {import('stylelint').Config} */ +module.exports = { + extends: '@wordpress/stylelint-config/scss-stylistic', + plugins: [ + 'stylelint-plugin-logical-css', + '@wordpress/theme/stylelint-plugins/no-token-fallback-values', + ], + reportNeedlessDisables: true, + rules: { + 'at-rule-empty-line-before': null, + 'at-rule-no-unknown': null, + 'comment-empty-line-before': null, + 'declaration-property-value-allowed-list': [ + { + 'flex-direction': '/^(?!(row|column)-reverse).*$/', + }, + { + message: ( property, value ) => + `Avoid "${ value }" value for the "${ property }" property. For accessibility reasons, visual, reading, and DOM order must match. Only use the reverse values when they do not affect reading order, meaning, and interaction.`, + }, + ], + 'font-weight-notation': null, + '@stylistic/max-line-length': null, + 'no-descending-specificity': null, + 'property-disallowed-list': [ + [ 'order' ], + { + message: + 'Avoid the order property. For accessibility reasons, visual, reading, and DOM order must match. Only use the order property when it does not affect reading order, meaning, and interaction.', + }, + ], + 'rule-empty-line-before': null, + 'selector-class-pattern': [ + '^[a-z][a-z0-9]*(?:(?:__|--|-)[a-z0-9]+)*$', + { + message: + 'Selector should use lowercase class segments separated with hyphens, double hyphens, or double underscores (selector-class-pattern)', + }, + ], + 'value-keyword-case': null, + 'scss/operator-no-unspaced': null, + 'scss/selector-no-redundant-nesting-selector': null, + 'scss/load-partial-extension': null, + 'scss/no-global-function-names': null, + 'scss/comment-no-empty': null, + 'scss/at-extend-no-missing-placeholder': null, + 'scss/operator-no-newline-after': null, + 'scss/at-if-closing-brace-newline-after': null, + 'scss/at-else-empty-line-before': null, + 'scss/at-if-closing-brace-space-after': null, + 'no-invalid-position-at-import-rule': null, + }, + reportDescriptionlessDisables: true, + // Using ignorePath instead of ignoreFiles helps avoiding configuration + // dependencies issues specifically helps ignoring some themes configurations + // that require the "stylelint-a11y" package. + ignorePath: '.stylelintignore', +}; + + diff --git a/Gruntfile.js b/Gruntfile.js index 61f18481e23a8..45a814612faa3 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1760,8 +1760,28 @@ module.exports = function(grunt) { 'qunit:compiled' ] ); + grunt.registerTask( 'lint:css', 'Runs Stylelint on core CSS.', function() { + var done = this.async(); + + grunt.util.spawn( { + cmd: 'npx', + args: [ + 'wp-scripts', + 'lint-style', + SOURCE_DIR + '**/*.{css,scss}', + // By default, the Stylelint CLI looks for files to ignore in + // .stylelintignore in process.cwd(). This way, the configuration + // is consistent with the one for the NPM script in .stylelintrc.js. + ], + opts: { stdio: 'inherit' } + }, function( error ) { + done( ! error ); + } ); + } ); + grunt.registerTask( 'precommit:css', [ - 'postcss:core' + 'postcss:core', + 'lint:css', ] ); grunt.registerTask( 'precommit:php', [ diff --git a/package-lock.json b/package-lock.json index 78985e8f955c0..e01f475a24944 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,6 +88,7 @@ "sinon": "16.1.3", "sinon-test": "~3.1.6", "source-map-loader": "5.0.0", + "stylelint-plugin-logical-css": "2.1.0", "typescript": "6.0.3", "uuid": "14.0.1", "wait-on": "9.0.10", @@ -30615,6 +30616,16 @@ } } }, + "node_modules/stylelint-plugin-logical-css": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/stylelint-plugin-logical-css/-/stylelint-plugin-logical-css-2.1.0.tgz", + "integrity": "sha512-625OT+p5y2kkGBaRV7uTYscuH0m1UueMXh0WcidrXgwF2DOnKov+un9tvuyNG+SUC07W0ibcn+fZvQs/keskww==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "stylelint": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/stylelint-scss": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.14.0.tgz", diff --git a/package.json b/package.json index 7b8cdfe8eb8c0..875937685fbfb 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "sinon": "16.1.3", "sinon-test": "~3.1.6", "source-map-loader": "5.0.0", + "stylelint-plugin-logical-css": "2.1.0", "typescript": "6.0.3", "uuid": "14.0.1", "wait-on": "9.0.10", @@ -125,6 +126,8 @@ "grunt": "grunt", "lint:jsdoc": "wp-scripts lint-js", "lint:jsdoc:fix": "wp-scripts lint-js --fix", + "lint:css": "wp-scripts lint-style \"src/**/*.{css,scss}\"", + "lint:css:fix": "npm run lint:css -- --fix", "typecheck:js": "tsc --build", "env:start": "node ./tools/local-env/scripts/start.js && node ./tools/local-env/scripts/docker.js run -T --rm php composer update -W", "env:stop": "node ./tools/local-env/scripts/docker.js down",