From 96b3f2fa2292fe0be4debeca8a79f8a0eea7d9ec Mon Sep 17 00:00:00 2001 From: "oe.sonnh" Date: Thu, 27 Jul 2023 16:22:54 +0700 Subject: [PATCH] [stylelint-polaris] Custom stylelint messages (#7696) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #7506 - [x] Bring in changes from https://github.com/Shopify/polaris/pull/7814/files - [ ] ~Finish adding examples to coverage/README.md~ (handling in separate [PR](https://github.com/Shopify/polaris/pull/7878)) - [x] Update screenshots to reflect current approach - [x] Update/add comments to notable changes/fixes/open questions This pull request adds support for configuring `stylelint-polaris/coverage` rules with custom messages and metadata, so that error and warning messages tell admin builders how to resolve the problem and VS Code diagnostics link to the `@shopify/stylelint-polaris` documentation. For example: Screenshot 2022-12-09 at 1 41 40 PM This PR also includes a bit of polish: - Added typing and descriptions to `polaris/coverage` plugin so that primary options are clear in the config and in the plugin's code - Updated plugin names to be singular instead of plural ```diff - custom-properties-allowed-list + custom-property-allowed-list - media-queries-allowed-list + media-query-allowed-list ``` đŸ–Ĩ [Local development instructions](https://github.com/Shopify/polaris/blob/main/README.md#local-development) 🗒 [General tophatting guidelines](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting.md) 📄 [Changelog guidelines](https://github.com/Shopify/polaris/blob/main/.github/CONTRIBUTING.md#changelog)
Copy-paste this code in playground/Playground.tsx: ```jsx import React from 'react'; import {Page} from '../src'; export function Playground() { return ( {/* Add the code you want to test in here */} ); } ```
- [ ] Tested on [mobile](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting.md#cross-browser-testing) - [ ] Tested on [multiple browsers](https://help.shopify.com/en/manual/shopify-admin/supported-browsers) - [ ] Tested for [accessibility](https://github.com/Shopify/polaris/blob/main/documentation/Accessibility%20testing.md) - [ ] Updated the component's `README.md` with documentation changes - [ ] [Tophatted documentation](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting%20documentation.md) changes in the style guide --- .changeset/thirty-jobs-vanish.md | 5 + .stylelintrc.js | 4 +- .../src/components/Button/Button.scss | 7 +- .../src/components/DropZone/DropZone.scss | 3 +- .../ConnectedFilterControl.scss | 9 +- .../components/DualThumb/DualThumb.scss | 5 +- .../components/SingleThumb/SingleThumb.scss | 24 +- stylelint-polaris/index.js | 826 ++++++++++-------- .../plugins/at-rule-disallowed-list/README.md | 8 +- .../plugins/at-rule-disallowed-list/index.js | 18 +- stylelint-polaris/plugins/coverage/index.js | 170 +++- .../plugins/coverage/index.test.js | 24 + .../README.md | 16 +- .../index.js | 15 +- .../index.test.js | 19 +- .../global-disallowed-list/index.test.js | 14 +- .../README.md | 12 +- .../index.js | 4 +- .../index.test.js | 0 stylelint-polaris/utils/index.js | 22 + 20 files changed, 694 insertions(+), 511 deletions(-) create mode 100644 .changeset/thirty-jobs-vanish.md rename stylelint-polaris/plugins/{custom-properties-allowed-list => custom-property-allowed-list}/README.md (84%) rename stylelint-polaris/plugins/{custom-properties-allowed-list => custom-property-allowed-list}/index.js (91%) rename stylelint-polaris/plugins/{custom-properties-allowed-list => custom-property-allowed-list}/index.test.js (88%) rename stylelint-polaris/plugins/{media-queries-allowed-list => media-query-allowed-list}/README.md (90%) rename stylelint-polaris/plugins/{media-queries-allowed-list => media-query-allowed-list}/index.js (96%) rename stylelint-polaris/plugins/{media-queries-allowed-list => media-query-allowed-list}/index.test.js (100%) diff --git a/.changeset/thirty-jobs-vanish.md b/.changeset/thirty-jobs-vanish.md new file mode 100644 index 00000000000..d3c81f3fe30 --- /dev/null +++ b/.changeset/thirty-jobs-vanish.md @@ -0,0 +1,5 @@ +--- +'@shopify/stylelint-polaris': patch +--- + +Implemented custom message configuration support for polaris/coverage plugin diff --git a/.stylelintrc.js b/.stylelintrc.js index e498b0f9a52..bfec2a3e8be 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -1,9 +1,9 @@ /** @type {import('stylelint').Config} */ module.exports = { extends: ['@shopify/stylelint-plugin/prettier', './stylelint-polaris'], - // Disabling @shopify/stylelint-plugin/configs/core no-unknown-animations as stylelint - // is not aware of global Polaris keyframes + // Disabling @shopify/stylelint-plugin/configs/core no-unknown-animations as stylelint is not aware of global Polaris keyframes // TODO: create custom plugin to ensure animation-names match Polaris keyframe names + customSyntax: 'postcss-scss', rules: { 'no-unknown-animations': null, 'value-keyword-case': ['lower', {camelCaseSvgKeywords: true}], diff --git a/polaris-react/src/components/Button/Button.scss b/polaris-react/src/components/Button/Button.scss index 858606bd03a..0e2064949a1 100644 --- a/polaris-react/src/components/Button/Button.scss +++ b/polaris-react/src/components/Button/Button.scss @@ -251,7 +251,7 @@ .plain { // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY @include recolor-icon(var(--p-interactive)); - // stylelint-disable-next-line polaris/conventions/polaris/custom-properties-allowed-list -- generated by polaris-migrator DO NOT COPY + // stylelint-disable-next-line -- polaris/conventions/polaris/custom-property-allowed-list margin: calc(-1 * var(--pc-button-vertical-padding)) calc(-1 * var(--p-space-2)); padding-left: var(--p-space-2); @@ -357,10 +357,10 @@ } &.sizeLarge { - // stylelint-disable polaris/conventions/polaris/custom-properties-allowed-list -- generated by polaris-migrator DO NOT COPY + // stylelint-disable -- polaris/conventions/polaris/custom-property-allowed-list margin: calc(-1 * var(--pc-button-large-vertical-padding)) calc(-1 * var(--p-space-5)); - // stylelint-enable polaris/conventions/polaris/custom-properties-allowed-list + // stylelint-enable -- polaris/conventions/polaris/custom-property-allowed-list } &.iconOnly { @@ -411,7 +411,6 @@ margin-right: 0; } } -// stylelint-enable selector-max-specificity, selector-max-class .fullWidth { // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY diff --git a/polaris-react/src/components/DropZone/DropZone.scss b/polaris-react/src/components/DropZone/DropZone.scss index c2820c203b2..cd5d80cd6e4 100755 --- a/polaris-react/src/components/DropZone/DropZone.scss +++ b/polaris-react/src/components/DropZone/DropZone.scss @@ -94,7 +94,7 @@ $dropzone-stacking-order: ( &:not(.focused) { &::after { @include reset-after; - // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY + @include set-border-radius; border-color: var(--p-border-neutral-subdued); } @@ -145,7 +145,6 @@ $dropzone-stacking-order: ( } .Overlay { - // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY @include set-border-radius; // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY position: absolute; diff --git a/polaris-react/src/components/Filters/components/ConnectedFilterControl/ConnectedFilterControl.scss b/polaris-react/src/components/Filters/components/ConnectedFilterControl/ConnectedFilterControl.scss index 4360815b42b..f021116e76e 100644 --- a/polaris-react/src/components/Filters/components/ConnectedFilterControl/ConnectedFilterControl.scss +++ b/polaris-react/src/components/Filters/components/ConnectedFilterControl/ConnectedFilterControl.scss @@ -5,10 +5,10 @@ // stylelint-disable selector-max-compound-selectors -- generated by polaris-migrator DO NOT COPY // stylelint-disable selector-max-type -- generated by polaris-migrator DO NOT COPY .ConnectedFilterControl { - // stylelint-disable polaris/conventions/polaris/custom-properties-allowed-list -- generated by polaris-migrator DO NOT COPY + // stylelint-disable -- polaris/conventions/polaris/custom-property-allowed-list --pc-connceted-filter-control-item: 10; --pc-connceted-filter-control-focused: 20; - // stylelint-enable polaris/conventions/polaris/custom-properties-allowed-list + // stylelint-enable -- polaris/conventions/polaris/custom-property-allowed-list // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY display: flex; // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY @@ -125,8 +125,3 @@ // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY flex-grow: 0; } -// stylelint-enable selector-max-specificity -// stylelint-enable selector-max-combinators -// stylelint-enable selector-max-class -// stylelint-enable selector-max-type -// stylelint-enable selector-max-compound-selectors diff --git a/polaris-react/src/components/RangeSlider/components/DualThumb/DualThumb.scss b/polaris-react/src/components/RangeSlider/components/DualThumb/DualThumb.scss index c2751b31b0a..6be183d7424 100644 --- a/polaris-react/src/components/RangeSlider/components/DualThumb/DualThumb.scss +++ b/polaris-react/src/components/RangeSlider/components/DualThumb/DualThumb.scss @@ -205,14 +205,13 @@ $range-output-size: 32px; transform: translateY(calc(-1 * var(--pc-range-slider-output-spacing))); @media #{$p-breakpoints-md-up} { - // stylelint-disable polaris/conventions/polaris/custom-properties-allowed-list -- generated by polaris-migrator DO NOT COPY + // stylelint-disable-next-line -- polaris/conventions/polaris/custom-property-allowed-list transform: translateY( calc(-1 * (var(--pc-range-slider-output-spacing) * 0.5)) ); - // stylelint-enable polaris/conventions/polaris/custom-properties-allowed-list } } - // stylelint-enable selector-max-specificity, selector-max-combinators, selector-max-class + // stylelint-enable selector-max-specificity, selector-max-combinators, selector-max-class -- generated by polaris-migrator DO NOT COPY > :first-child { // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY diff --git a/polaris-react/src/components/RangeSlider/components/SingleThumb/SingleThumb.scss b/polaris-react/src/components/RangeSlider/components/SingleThumb/SingleThumb.scss index e22ad46ef32..281a3384b35 100644 --- a/polaris-react/src/components/RangeSlider/components/SingleThumb/SingleThumb.scss +++ b/polaris-react/src/components/RangeSlider/components/SingleThumb/SingleThumb.scss @@ -103,12 +103,12 @@ width: 100%; // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY height: var(--pc-range-slider-track-height); - // stylelint-disable polaris/conventions/polaris/custom-properties-allowed-list -- Polaris component custom properties + // stylelint-disable -- polaris/conventions/polaris/custom-property-allowed-list -- Polaris component custom properties background-image: linear-gradient( to right, var(--pc-single-thumb-gradient-colors) ); - // stylelint-enable polaris/conventions/polaris/custom-properties-allowed-list + // stylelint-enable -- polaris/conventions/polaris/custom-property-allowed-list border: none; // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY border-radius: var(--pc-range-slider-track-height); @@ -141,19 +141,19 @@ &::-ms-thumb { margin-top: 0; - // stylelint-disable polaris/conventions/polaris/custom-properties-allowed-list -- generated by polaris-migrator DO NOT COPY + // stylelint-disable -- polaris/conventions/polaris/custom-property-allowed-list -- generated by polaris-migrator DO NOT COPY transform: translateY(calc(var(--pc-range-slider-thumb-size) * 0.2)) scale(0.4); - // stylelint-enable polaris/conventions/polaris/custom-properties-allowed-list + // stylelint-enable -- polaris/conventions/polaris/custom-property-allowed-list } &::-webkit-slider-thumb { - // stylelint-disable polaris/conventions/polaris/custom-properties-allowed-list, function-calc-no-unspaced-operator -- generated by polaris-migrator DO NOT COPY + // stylelint-disable -- polaris/conventions/polaris/custom-property-allowed-list, function-calc-no-unspaced-operator margin-top: calc( (var(--pc-range-slider-thumb-size) - var(--pc-range-slider-track-height)) * -0.5 ); - // stylelint-enable polaris/conventions/polaris/custom-properties-allowed-list, function-calc-no-unspaced-operator + // stylelint-enable -- polaris/conventions/polaris/custom-property-allowed-list, function-calc-no-unspaced-operator } &:active { @@ -202,11 +202,11 @@ /// Output value indicator $range-output-size: 32px; -// stylelint-disable polaris/conventions/polaris/custom-properties-allowed-list -- generated by polaris-migrator DO NOT COPY +// stylelint-disable -- polaris/conventions/polaris/custom-property-allowed-list $range-output-translate-x: calc( -50% + var(--pc-range-slider-output-factor) * var(--pc-range-slider-thumb-size) ); -// stylelint-enable polaris/conventions/polaris/custom-properties-allowed-list +// stylelint-enable -- polaris/conventions/polaris/custom-property-allowed-list .Output { // stylelint-disable-next-line -- Polaris component custom properties @@ -228,14 +228,13 @@ $range-output-translate-x: calc( transition-timing-function: var(--p-ease); .Input:active + & { - // stylelint-enable polaris/layout/polaris/global-disallowed-list opacity: 1; visibility: visible; - // stylelint-disable polaris/layout/declaration-property-value-disallowed-list, polaris/conventions/polaris/custom-properties-allowed-list -- generated by polaris-migrator DO NOT COPY + // stylelint-disable -- polaris/layout/declaration-property-value-disallowed-list, polaris/conventions/polaris/custom-property-allowed-list -- generated by polaris-migrator DO NOT COPY bottom: calc( var(--pc-range-slider-thumb-size) + var(--p-range-slider-thumb-size-active) - var(--p-range-slider-thumb-size-base) ); - // stylelint-enable polaris/layout/declaration-property-value-disallowed-list, polaris/conventions/polaris/custom-properties-allowed-list + // stylelint-enable -- polaris/layout/declaration-property-value-disallowed-list, polaris/conventions/polaris/custom-property-allowed-list } } @@ -265,11 +264,10 @@ $range-output-translate-x: calc( transform: translateY(calc(-1 * var(--pc-range-slider-output-spacing))); @media #{$p-breakpoints-md-up} { - // stylelint-disable polaris/conventions/polaris/custom-properties-allowed-list -- generated by polaris-migrator DO NOT COPY + // stylelint-disable-next-line -- polaris/conventions/polaris/custom-property-allowed-list transform: translateY( calc(-1 * (var(--pc-range-slider-output-spacing) * 0.4)) ); - // stylelint-enable polaris/conventions/polaris/custom-properties-allowed-list } } // stylelint-enable selector-max-specificity, selector-max-combinators, selector-max-class diff --git a/stylelint-polaris/index.js b/stylelint-polaris/index.js index 8c41f1d3bbd..864dc8b90fe 100644 --- a/stylelint-polaris/index.js +++ b/stylelint-polaris/index.js @@ -4,414 +4,474 @@ const { tokens, } = require('@shopify/polaris-tokens'); -/** @type {import('./plugins/coverage').PrimaryOptions} */ +/** + * @type {import('./plugins/coverage').PrimaryOptions} The stylelint-polaris/coverage rule expects a 3-dimensional rule config that groups Stylelint rules by coverage categories. It reports problems with dynamic rule names by appending the category to the coverage plugin's rule name + +(e.g., Unexpected named color "blue" - Please use a Polaris color token Stylelint(polaris/colors/color-named)") +*/ const stylelintPolarisCoverageOptions = { - colors: { - 'color-named': 'never', - 'color-no-hex': true, - 'scss/function-color-relative': true, - 'declaration-property-value-disallowed-list': { - opacity: [/(?!0|1)\d$|^\d{2,}|^[1-9]+\.|^\d+\.\d+\.|^0\.\d{3,}/], - }, - 'function-disallowed-list': [ - // Include Sass namespace - // https://regex101.com/r/UdW0oV/1 - 'brightness', - 'contrast', - 'hue-rotate', - 'hsl', - 'hsla', - 'invert', - 'rgb', - 'rgba', - 'sepia', - /([\w-]+\.)?color-multiply/, - /([\w-]+\.)?color/, - /([\w-]+\.)?filter/, - ], - 'polaris/at-rule-disallowed-list': { - include: [ - // Legacy mixins - /([\w-]+\.)?color-icon($|\()/, - /([\w-]+\.)?recolor-icon($|\()/, - /([\w-]+\.)?control-backdrop($|\()/, - /([\w-]+\.)?ms-high-contrast-color/, + colors: [ + { + 'color-named': 'never', + 'color-no-hex': true, + 'scss/function-color-relative': true, + 'declaration-property-value-disallowed-list': { + opacity: [/(?!0|1)\d$|^\d{2,}|^[1-9]+\.|^\d+\.\d+\.|^0\.\d{3,}/], + }, + 'function-disallowed-list': [ + // Include Sass namespace + // https://regex101.com/r/UdW0oV/1 + 'brightness', + 'contrast', + 'hue-rotate', + 'hsl', + 'hsla', + 'invert', + 'rgb', + 'rgba', + 'sepia', + /([\w-]+\.)?color-multiply/, + /([\w-]+\.)?color/, + /([\w-]+\.)?filter/, ], - }, - 'polaris/global-disallowed-list': [ - // Legacy mixin map-get data - /\$polaris-colors/, - /\$color-filter-palette-data/, - /\$color-palette-data/, - // Legacy custom properties - /--p-override-transparent/, - /--p-badge-mix-blend-mode/, - ], - }, - motion: { - 'at-rule-disallowed-list': ['keyframes'], - 'function-disallowed-list': [ - /([\w-]+\.)?control-icon-transition/, - /([\w-]+\.)?duration/, - /([\w-]+\.)?easing/, - ], - 'declaration-property-unit-disallowed-list': [ - { - '/^animation/': ['ms', 's'], - '/^transition/': ['ms', 's'], + 'polaris/at-rule-disallowed-list': { + include: [ + // Legacy mixins + /([\w-]+\.)?color-icon($|\()/, + /([\w-]+\.)?recolor-icon($|\()/, + /([\w-]+\.)?ms-high-contrast-color/, + ], }, - ], - 'polaris/at-rule-disallowed-list': { - include: [/([\w-]+\.)?skeleton-shimmer($|\()/], + 'polaris/global-disallowed-list': [ + // Legacy mixin map-get data + /\$polaris-colors/, + /\$color-filter-palette-data/, + /\$color-palette-data/, + // Legacy custom properties + /--p-override-transparent/, + /--p-badge-mix-blend-mode/, + ], }, - 'polaris/global-disallowed-list': [ - // Legacy mixin map-get data - /\$duration-data/, - /\$polaris-duration-map/, - /\$skeleton-shimmer-duration/, - /\$easing-data/, - // Legacy custom properties - /--p-range-slider-thumb-scale/, - /--p-duration-1-0-0/, - /--p-duration-1-5-0/, - ], - }, - typography: { - 'declaration-property-value-disallowed-list': { - 'font-weight': [/(\$.*|[0-9]+)/], + { + message: 'Please use a Polaris color token', }, - 'declaration-property-unit-disallowed-list': [ - { - '/^font/': ['px', 'rem', 'em'], - 'line-height': ['px', 'rem', 'em'], + ], + motion: [ + { + 'function-disallowed-list': [ + /([\w-]+\.)?control-icon-transition/, + /([\w-]+\.)?duration/, + /([\w-]+\.)?easing/, + ], + 'declaration-property-unit-disallowed-list': [ + { + '/^animation/': ['ms', 's'], + '/^transition/': ['ms', 's'], + }, + ], + 'at-rule-disallowed-list': ['keyframes'], + 'polaris/at-rule-disallowed-list': { + include: [/([\w-]+\.)?skeleton-shimmer($|\()/], }, - ], - 'function-disallowed-list': [ - /([\w-]+\.)?font-family/, - /([\w-]+\.)?font-size/, - /([\w-]+\.)?line-height/, - ], - 'polaris/at-rule-disallowed-list': { - include: [ - /([\w-]+\.)?truncate($|\()/, - /([\w-]+\.)?text-breakword($|\()/, - /([\w-]+\.)?text-emphasis-normal($|\()/, - /([\w-]+\.)?text-emphasis-strong($|\()/, - /([\w-]+\.)?text-emphasis-subdued($|\()/, - /([\w-]+\.)?text-style-body($|\()/, - /([\w-]+\.)?text-style-button-large($|\()/, - /([\w-]+\.)?text-style-button($|\()/, - /([\w-]+\.)?text-style-caption($|\()/, - /([\w-]+\.)?text-style-display-large($|\()/, - /([\w-]+\.)?text-style-display-medium($|\()/, - /([\w-]+\.)?text-style-display-small($|\()/, - /([\w-]+\.)?text-style-display-x-large($|\()/, - /([\w-]+\.)?text-style-heading($|\()/, - /([\w-]+\.)?text-style-input($|\()/, - /([\w-]+\.)?text-style-subheading($|\()/, + 'polaris/global-disallowed-list': [ + // Legacy mixin map-get data + /\$duration-data/, + /\$polaris-duration-map/, + /\$skeleton-shimmer-duration/, + /\$easing-data/, + // Legacy custom properties + /--p-range-slider-thumb-scale/, + /--p-duration-1-0-0/, + /--p-duration-1-5-0/, ], }, - 'polaris/global-disallowed-list': [ - // Legacy mixin map-get data - /\$typography-condensed/, - /\$typography-condensed/, - /\$base-font-size/, - /\$line-height-data/, - /\$font-family-data/, - /\$font-size-data/, - /\$default-browser-font-size/, - // Legacy custom properties - /--p-button-font/, - /--p-badge-font/, - ], - }, - layout: { - 'declaration-property-value-disallowed-list': [ - { - top: [/(?!var\(--p-).+$/], - bottom: [/(?!var\(--p-).+$/], - left: [/(?!var\(--p-).+$/], - right: [/(?!var\(--p-).+$/], - '/^width/': [/(?!var\(--p-).+$/], - '/^height/': [/(?!var\(--p-).+$/], + { + message: 'Please use a Polaris motion token', + }, + ], + typography: [ + { + 'declaration-property-value-disallowed-list': { + 'font-weight': [/(\$.*|[0-9]+)/], }, - {severity: 'warning'}, - ], - 'property-disallowed-list': [ - [ - 'position', - 'grid', - 'flex', - 'flex-grow', - 'flex-shrink', - 'flex-basis', - 'justify-content', - 'align-items', - 'grid-row', - 'grid-row-start', - 'grid-row-end', - 'grid-column', - 'grid-column-start', - 'grid-column-end', - 'grid-template', - 'grid-template-areas', - 'grid-template-rows', - 'grid-template-columns', - 'grid-area', - 'display', + 'declaration-property-unit-disallowed-list': [ + { + '/^font/': ['px', 'rem', 'em'], + 'line-height': ['px', 'rem', 'em'], + }, ], - {severity: 'warning'}, - ], - 'function-disallowed-list': [ - /([\w-]+\.)?nav-min-window-corrected/, - /([\w-]+\.)?control-height/, - /([\w-]+\.)?control-slim-height/, - /([\w-]+\.)?mobile-nav-width/, - /([\w-]+\.)?thumbnail-size/, - /([\w-]+\.)?icon-size($|\()/, - /([\w-]+\.)?top-bar-height/, - /([\w-]+\.)?z-index/, - ], - 'polaris/at-rule-disallowed-list': { - include: [ - /([\w-]+\.)?hidden-when-printing($|\()/, - /([\w-]+\.)?print-hidden($|\()/, - /([\w-]+\.)?layout-flex-fix($|\()/, - /([\w-]+\.)?safe-area-for($|\()/, - /([\w-]+\.)?skeleton-page-header-layout($|\()/, - /([\w-]+\.)?skeleton-page-secondary-actions-layout($|\()/, + 'function-disallowed-list': [ + /([\w-]+\.)?font-family/, + /([\w-]+\.)?font-size/, + /([\w-]+\.)?line-height/, ], - }, - 'polaris/global-disallowed-list': [ - // Legacy mixin map-get data - /\$layout-width-data/, - /\$navigation-width/, - /\$small-thumbnail-size/, - /\$large-thumbnail-size/, - /\$medium-thumbnail-size/, - /\$thumbnail-sizes/, - // Legacy custom properties - /--p-range-slider-thumb-size-base/, - /--p-range-slider-thumb-size-active/, - /--p-override-visible/, - /--p-override-loading-z-index/, - /--p-icon-size/, - /--p-choice-size/, - ], - }, - spacing: { - 'function-disallowed-list': [ - /([\w-]+\.)?control-vertical-padding/, - /([\w-]+\.)?em/, - /([\w-]+\.)?px/, - /([\w-]+\.)?rem/, - /([\w-]+\.)?spacing/, - ], - 'declaration-property-unit-disallowed-list': [ - { - '/^padding/': ['px', 'rem', 'em'], - '/^margin/': ['px', 'rem', 'em'], - '/^gap/': ['px', 'rem', 'em'], + 'polaris/at-rule-disallowed-list': { + include: [ + /([\w-]+\.)?truncate($|\()/, + /([\w-]+\.)?text-breakword($|\()/, + /([\w-]+\.)?text-emphasis-normal($|\()/, + /([\w-]+\.)?text-emphasis-strong($|\()/, + /([\w-]+\.)?text-emphasis-subdued($|\()/, + /([\w-]+\.)?text-style-body($|\()/, + /([\w-]+\.)?text-style-button-large($|\()/, + /([\w-]+\.)?text-style-button($|\()/, + /([\w-]+\.)?text-style-caption($|\()/, + /([\w-]+\.)?text-style-display-large($|\()/, + /([\w-]+\.)?text-style-display-medium($|\()/, + /([\w-]+\.)?text-style-display-small($|\()/, + /([\w-]+\.)?text-style-display-x-large($|\()/, + /([\w-]+\.)?text-style-heading($|\()/, + /([\w-]+\.)?text-style-input($|\()/, + /([\w-]+\.)?text-style-subheading($|\()/, + ], }, - ], - 'polaris/global-disallowed-list': [ - // Legacy mixin map-get data - /\$polaris-spacing/, - /\$spacing-data/, - /\$actions-vertical-spacing/, - // Legacy custom properties - /--p-button-group-item-spacing/, - /--p-choice-margin/, - /--p-text-field-spinner-offset/, - ], - }, - shape: { - 'declaration-property-unit-disallowed-list': [ - { - 'border-width': ['px', 'rem', 'em'], - border: ['px', 'rem', 'em'], - 'border-radius': ['px', 'rem', 'em'], - 'outline-offset': ['px', 'rem', 'em'], - outline: ['px', 'rem', 'em'], + 'polaris/global-disallowed-list': [ + // Legacy mixin map-get data + /\$typography-condensed/, + /\$typography-condensed/, + /\$base-font-size/, + /\$line-height-data/, + /\$font-family-data/, + /\$font-size-data/, + /\$default-browser-font-size/, + // Legacy custom properties + /--p-button-font/, + /--p-badge-font/, + ], + }, + { + message: 'Please use a Polaris font token or typography component', + }, + ], + layout: [ + { + 'declaration-property-value-disallowed-list': [ + { + top: [/(?!var\(--p-).+$/], + bottom: [/(?!var\(--p-).+$/], + left: [/(?!var\(--p-).+$/], + right: [/(?!var\(--p-).+$/], + '/^width/': [/(?!var\(--p-).+$/], + '/^height/': [/(?!var\(--p-).+$/], + }, + {severity: 'warning'}, + ], + 'property-disallowed-list': [ + [ + 'position', + 'grid', + 'flex', + 'flex-grow', + 'flex-shrink', + 'flex-basis', + 'justify-content', + 'align-items', + 'grid-row', + 'grid-row-start', + 'grid-row-end', + 'grid-column', + 'grid-column-start', + 'grid-column-end', + 'grid-template', + 'grid-template-areas', + 'grid-template-rows', + 'grid-template-columns', + 'grid-area', + 'display', + ], + {severity: 'warning'}, + ], + 'function-disallowed-list': [ + /([\w-]+\.)?nav-min-window-corrected/, + /([\w-]+\.)?control-height/, + /([\w-]+\.)?control-slim-height/, + /([\w-]+\.)?mobile-nav-width/, + /([\w-]+\.)?thumbnail-size/, + /([\w-]+\.)?icon-size($|\()/, + /([\w-]+\.)?top-bar-height/, + /([\w-]+\.)?z-index/, + ], + 'polaris/at-rule-disallowed-list': { + include: [ + /([\w-]+\.)?hidden-when-printing($|\()/, + /([\w-]+\.)?print-hidden($|\()/, + /([\w-]+\.)?layout-flex-fix($|\()/, + /([\w-]+\.)?safe-area-for($|\()/, + /([\w-]+\.)?skeleton-page-header-layout($|\()/, + /([\w-]+\.)?skeleton-page-secondary-actions-layout($|\()/, + ], }, - ], - 'polaris/at-rule-disallowed-list': { - include: [ + 'polaris/global-disallowed-list': [ + // Legacy mixin map-get data + /\$layout-width-data/, + /\$navigation-width/, + /\$small-thumbnail-size/, + /\$large-thumbnail-size/, + /\$medium-thumbnail-size/, + /\$thumbnail-sizes/, + // Legacy custom properties + /--p-range-slider-thumb-size-base/, + /--p-range-slider-thumb-size-active/, + /--p-override-visible/, + /--p-override-loading-z-index/, + /--p-icon-size/, + /--p-choice-size/, + ], + }, + { + message: 'Please use a Polaris layout component', + }, + ], + spacing: [ + { + 'function-disallowed-list': [ + /([\w-]+\.)?control-vertical-padding/, + /([\w-]+\.)?em/, + /([\w-]+\.)?px/, + /([\w-]+\.)?rem/, + /([\w-]+\.)?spacing/, + ], + 'declaration-property-unit-disallowed-list': [ + { + '/^padding/': ['px', 'rem', 'em'], + '/^margin/': ['px', 'rem', 'em'], + '/^gap/': ['px', 'rem', 'em'], + }, + ], + 'polaris/global-disallowed-list': [ + // Legacy mixin map-get data + /\$polaris-spacing/, + /\$spacing-data/, + /\$actions-vertical-spacing/, + // Legacy custom properties + /--p-button-group-item-spacing/, + /--p-choice-margin/, + /--p-text-field-spinner-offset/, + ], + }, + { + message: 'Please use a Polaris spacing token', + }, + ], + shape: [ + { + 'function-disallowed-list': [ /([\w-]+\.)?border-radius/, /([\w-]+\.)?border-width/, /([\w-]+\.)?border/, - /([\w-]+\.)?high-contrast-border($|\()/, - /([\w-]+\.)?high-contrast-button-outline($|\()/, - /([\w-]+\.)?high-contrast-outline($|\()/, - /([\w-]+\.)?focus-ring($|\()/, - /([\w-]+\.)?no-focus-ring($|\()/, + ], + 'declaration-property-unit-disallowed-list': [ + { + 'border-width': ['px', 'rem', 'em'], + border: ['px', 'rem', 'em'], + 'border-radius': ['px', 'rem', 'em'], + 'outline-offset': ['px', 'rem', 'em'], + outline: ['px', 'rem', 'em'], + }, + ], + 'polaris/at-rule-disallowed-list': { + include: [ + /([\w-]+\.)?high-contrast-border($|\()/, + /([\w-]+\.)?high-contrast-button-outline($|\()/, + /([\w-]+\.)?high-contrast-outline($|\()/, + /([\w-]+\.)?focus-ring($|\()/, + /([\w-]+\.)?no-focus-ring($|\()/, + ], + }, + 'polaris/global-disallowed-list': [ + // Legacy mixin map-get data + /\$border-radius-data/, + /\$border-width-data/, + /\$borders-data/, + // Legacy custom properties + /--p-border-radius-base/, + /--p-border-radius-wide/, + /--p-border-radius-full/, + /--p-control-border-width/, + /--p-thin-border-subdued/, + /--p-banner-border-default/, + /--p-banner-border-success/, + /--p-banner-border-highlight/, + /--p-banner-border-warning/, + /--p-banner-border-critical/, + /--p-text-field-focus-ring-border-radius/, + /--p-text-field-focus-ring-offset/, ], }, - 'polaris/global-disallowed-list': [ - // Legacy mixin map-get data - /\$border-radius-data/, - /\$border-width-data/, - /\$borders-data/, - // Legacy custom properties - /--p-border-radius-base/, - /--p-border-radius-wide/, - /--p-border-radius-full/, - /--p-control-border-width/, - /--p-thin-border-subdued/, - /--p-banner-border-default/, - /--p-banner-border-success/, - /--p-banner-border-highlight/, - /--p-banner-border-warning/, - /--p-banner-border-critical/, - /--p-text-field-focus-ring-border-radius/, - /--p-text-field-focus-ring-offset/, - ], - }, - depth: { - 'function-disallowed-list': [/([\w-]+\.)?shadow/], - 'declaration-property-unit-disallowed-list': [ - { - 'box-shadow': ['px', 'rem', 'em'], + { + message: 'Please use a Polaris shape token', + }, + ], + depth: [ + { + 'function-disallowed-list': [/([\w-]+\.)?shadow/], + 'declaration-property-unit-disallowed-list': [ + { + 'box-shadow': ['px', 'rem', 'em'], + }, + ], + 'property-disallowed-list': ['text-shadow'], + 'polaris/global-disallowed-list': [ + // Legacy mixin map-get data + /\$shadows-data/, + /\$fixed-element-stacking-order/, + /\$global-elements/, + // Legacy custom properties + /--p-button-drop-shadow/, + /--p-button-inner-shadow/, + /--p-button-pressed-inner-shadow/, + /--p-card-shadow/, + /--p-popover-shadow/, + /--p-modal-shadow/, + /--p-top-bar-shadow/, + ], + }, + { + message: 'Please use a Polaris depth token', + }, + ], + 'z-index': [ + { + 'declaration-property-value-allowed-list': [ + { + 'z-index': Object.keys(tokens.zIndex).map(createVar), + }, + ], + 'function-disallowed-list': [/([\w-]+\.)?z-index/], + 'polaris/global-disallowed-list': [ + // Legacy mixin map-get data + /\$fixed-element-stacking-order/, + /\$global-elements/, + // Legacy custom properties + /--p-override-loading-z-index/, + ], + }, + { + message: 'Please use a Polaris z-index token', + }, + ], + conventions: [ + { + 'polaris/custom-property-allowed-list': { + // Allow any custom property not prefixed with `--p-`, `--pc-`, or `--polaris-version-` + allowedProperties: [/--(?!(p|pc|polaris-version)-).+/], + allowedValues: { + '/.+/': [ + // Note: Order is important. + // The first pattern validates `--p-*` + // custom properties are valid Polaris tokens + ...getCustomPropertyNames(tokens), + // and the second pattern flags unknown `--p-*` custom properties + // or usages of our "private" `--pc-*` custom properties + /--(?!(p|pc)-).+/, + ], + }, }, - ], - 'property-disallowed-list': ['text-shadow'], - 'polaris/global-disallowed-list': [ - // Legacy mixin map-get data - /\$shadows-data/, - /\$fixed-element-stacking-order/, - /\$global-elements/, - // Legacy custom properties - /--p-button-drop-shadow/, - /--p-button-inner-shadow/, - /--p-button-pressed-inner-shadow/, - /--p-card-shadow/, - /--p-popover-shadow/, - /--p-modal-shadow/, - /--p-top-bar-shadow/, - ], - }, - 'z-index': { - 'declaration-property-value-allowed-list': [ - { - 'z-index': Object.keys(tokens.zIndex).map(createVar), + }, + { + message: 'Please use a Polaris token or component', + }, + ], + 'media-queries': [ + { + 'polaris/media-query-allowed-list': { + // Allowed media types and media conditions + // https://www.w3.org/TR/mediaquery5/#media + allowedMediaTypes: ['print', 'screen'], + allowedMediaFeatureNames: ['forced-colors', '-ms-high-contrast'], + allowedScssInterpolations: [ + // TODO: Add utility to @shopify/polaris-tokens to getMediaConditionNames + /^\$p-breakpoints-(xs|sm|md|lg|xl)-(up|down|only)$/, + ], }, - ], - 'function-disallowed-list': [/([\w-]+\.)?z-index/], - 'polaris/global-disallowed-list': [ - // Legacy mixin map-get data - /\$fixed-element-stacking-order/, - /\$global-elements/, - // Legacy custom properties - /--p-override-loading-z-index/, - ], - }, - conventions: { - 'polaris/custom-properties-allowed-list': { - // Allow any custom property not prefixed with `--p-`, `--pc-`, or `--polaris-version-` - allowedProperties: [/--(?!(p|pc|polaris-version)-).+/], - allowedValues: { - '/.+/': [ - // Note: Order is important. - // The first pattern validates `--p-*` - // custom properties are valid Polaris tokens - ...getCustomPropertyNames(tokens), - // and the second pattern flags unknown `--p-*` custom properties - // or usages of our "private" `--pc-*` custom properties - /--(?!(p|pc)-).+/, + // Legacy functions + 'function-disallowed-list': [ + /([\w-]+\.)?breakpoint/, + /([\w-]+\.)?layout-width/, + ], + // Legacy mixins + 'polaris/at-rule-disallowed-list': { + include: [ + /([\w-]+\.)?after-topbar-sheet($|\()/, + /([\w-]+\.)?breakpoint-after($|\()/, + /([\w-]+\.)?breakpoint-before($|\()/, + /([\w-]+\.)?frame-when-nav-displayed($|\()/, + /([\w-]+\.)?frame-when-nav-hidden($|\()/, + /([\w-]+\.)?frame-with-nav-when-not-max-width($|\()/, + /([\w-]+\.)?page-actions-layout($|\()/, + /([\w-]+\.)?page-content-breakpoint-after($|\()/, + /([\w-]+\.)?page-content-breakpoint-before($|\()/, + /([\w-]+\.)?page-content-layout($|\()/, + /([\w-]+\.)?page-content-when-fully-condensed($|\()/, + /([\w-]+\.)?page-content-when-layout-not-stacked($|\()/, + /([\w-]+\.)?page-content-when-layout-stacked($|\()/, + /([\w-]+\.)?page-content-when-not-fully-condensed($|\()/, + /([\w-]+\.)?page-content-when-not-partially-condensed($|\()/, + /([\w-]+\.)?page-content-when-partially-condensed($|\()/, + /([\w-]+\.)?page-header-has-navigation($|\()/, + /([\w-]+\.)?page-header-has-secondary-actions($|\()/, + /([\w-]+\.)?page-header-layout($|\()/, + /([\w-]+\.)?page-header-without-navigation($|\()/, + /([\w-]+\.)?page-layout($|\()/, + /([\w-]+\.)?page-padding-not-fully-condensed($|\()/, + /([\w-]+\.)?page-padding-not-partially-condensed($|\()/, + /([\w-]+\.)?page-title-layout($|\()/, + /([\w-]+\.)?page-when-not-max-width($|\()/, + /([\w-]+\.)?when-typography-condensed($|\()/, + /([\w-]+\.)?when-typography-not-condensed($|\()/, + /([\w-]+\.)?when-not-printing($|\()/, + /([\w-]+\.)?when-printing($|\()/, ], }, }, - }, - 'media-queries': { - 'polaris/media-queries-allowed-list': { - // Allowed media types and media conditions - // https://www.w3.org/TR/mediaqueries-5/#media - allowedMediaTypes: ['print', 'screen'], - allowedMediaFeatureNames: ['forced-colors', '-ms-high-contrast'], - allowedScssInterpolations: [ - // TODO: Add utility to @shopify/polaris-tokens to getMediaConditionNames - /^\$p-breakpoints-(xs|sm|md|lg|xl)-(up|down|only)$/, - ], + { + message: 'Please use a Polaris breakpoint token', }, - // Legacy functions - 'function-disallowed-list': [ - /([\w-]+\.)?breakpoint/, - /([\w-]+\.)?layout-width/, - ], - // Legacy mixins - 'polaris/at-rule-disallowed-list': { - include: [ - /([\w-]+\.)?after-topbar-sheet($|\()/, - /([\w-]+\.)?breakpoint-after($|\()/, - /([\w-]+\.)?breakpoint-before($|\()/, - /([\w-]+\.)?frame-when-nav-displayed($|\()/, - /([\w-]+\.)?frame-when-nav-hidden($|\()/, - /([\w-]+\.)?frame-with-nav-when-not-max-width($|\()/, - /([\w-]+\.)?page-actions-layout($|\()/, - /([\w-]+\.)?page-content-breakpoint-after($|\()/, - /([\w-]+\.)?page-content-breakpoint-before($|\()/, - /([\w-]+\.)?page-content-layout($|\()/, - /([\w-]+\.)?page-content-when-fully-condensed($|\()/, - /([\w-]+\.)?page-content-when-layout-not-stacked($|\()/, - /([\w-]+\.)?page-content-when-layout-stacked($|\()/, - /([\w-]+\.)?page-content-when-not-fully-condensed($|\()/, - /([\w-]+\.)?page-content-when-not-partially-condensed($|\()/, - /([\w-]+\.)?page-content-when-partially-condensed($|\()/, - /([\w-]+\.)?page-header-has-navigation($|\()/, - /([\w-]+\.)?page-header-has-secondary-actions($|\()/, - /([\w-]+\.)?page-header-layout($|\()/, - /([\w-]+\.)?page-header-without-navigation($|\()/, - /([\w-]+\.)?page-layout($|\()/, - /([\w-]+\.)?page-padding-not-fully-condensed($|\()/, - /([\w-]+\.)?page-padding-not-partially-condensed($|\()/, - /([\w-]+\.)?page-title-layout($|\()/, - /([\w-]+\.)?page-when-not-max-width($|\()/, - /([\w-]+\.)?when-typography-condensed($|\()/, - /([\w-]+\.)?when-typography-not-condensed($|\()/, - /([\w-]+\.)?when-not-printing($|\()/, - /([\w-]+\.)?when-printing($|\()/, + ], + legacy: [ + { + // Legacy mixins + 'polaris/at-rule-disallowed-list': { + include: [ + /([\w-]+\.)?base-button-disabled($|\()/, + /([\w-]+\.)?button-base($|\()/, + /([\w-]+\.)?button-filled($|\()/, + /([\w-]+\.)?button-full-width($|\()/, + /([\w-]+\.)?button-outline-disabled($|\()/, + /([\w-]+\.)?button-outline($|\()/, + /([\w-]+\.)?control-backdrop($|\()/, + /([\w-]+\.)?list-selected-indicator($|\()/, + /([\w-]+\.)?plain-button-backdrop($|\()/, + /([\w-]+\.)?unstyled-button($|\()/, + /([\w-]+\.)?skeleton-content($|\()/, + /([\w-]+\.)?unstyled-input($|\()/, + /([\w-]+\.)?unstyled-link($|\()/, + /([\w-]+\.)?unstyled-list($|\()/, + /([\w-]+\.)?range-thumb-selectors($|\()/, + /([\w-]+\.)?range-track-selectors($|\()/, + /([\w-]+\.)?state($|\()/, + /([\w-]+\.)?visually-hidden($|\()/, + ], + }, + // Legacy functions + 'function-disallowed-list': [ + /([\w-]+\.)?available-names/, + /([\w-]+\.)?map-extend/, ], - }, - }, - legacy: { - // Legacy mixins - 'polaris/at-rule-disallowed-list': { - include: [ - /([\w-]+\.)?base-button-disabled($|\()/, - /([\w-]+\.)?button-base($|\()/, - /([\w-]+\.)?button-filled($|\()/, - /([\w-]+\.)?button-full-width($|\()/, - /([\w-]+\.)?button-outline-disabled($|\()/, - /([\w-]+\.)?button-outline($|\()/, - /([\w-]+\.)?control-backdrop($|\()/, - /([\w-]+\.)?list-selected-indicator($|\()/, - /([\w-]+\.)?plain-button-backdrop($|\()/, - /([\w-]+\.)?unstyled-button($|\()/, - /([\w-]+\.)?skeleton-content($|\()/, - /([\w-]+\.)?unstyled-input($|\()/, - /([\w-]+\.)?unstyled-link($|\()/, - /([\w-]+\.)?unstyled-list($|\()/, - /([\w-]+\.)?range-thumb-selectors($|\()/, - /([\w-]+\.)?range-track-selectors($|\()/, - /([\w-]+\.)?state($|\()/, - /([\w-]+\.)?visually-hidden($|\()/, + 'polaris/global-disallowed-list': [ + // Legacy variables + / \* \$/, + // Legacy custom properties + /--p-override-none/, + /--p-override-one/, + /--p-override-zero/, + /--p-non-null-content/, ], }, - // Legacy functions - 'function-disallowed-list': [ - /([\w-]+\.)?available-names/, - /([\w-]+\.)?map-extend/, - ], - 'polaris/global-disallowed-list': [ - // Legacy variables - / \* \$/, - // Legacy custom properties - /--p-override-none/, - /--p-override-one/, - /--p-override-zero/, - /--p-non-null-content/, - ], - }, + { + message: 'Please use a Polaris token or component', + }, + ], }; /** @type {import('stylelint').Config} */ @@ -437,8 +497,8 @@ module.exports = { './plugins/coverage', './plugins/global-disallowed-list', './plugins/at-rule-disallowed-list', - './plugins/custom-properties-allowed-list', - './plugins/media-queries-allowed-list', + './plugins/custom-property-allowed-list', + './plugins/media-query-allowed-list', ], rules: { 'polaris/coverage': stylelintPolarisCoverageOptions, diff --git a/stylelint-polaris/plugins/at-rule-disallowed-list/README.md b/stylelint-polaris/plugins/at-rule-disallowed-list/README.md index 8827aa1083c..1775a255b1b 100644 --- a/stylelint-polaris/plugins/at-rule-disallowed-list/README.md +++ b/stylelint-polaris/plugins/at-rule-disallowed-list/README.md @@ -4,7 +4,7 @@ The purpose of this plugin is to disallow usages of the defined at-rules. ## How to use -### Options: +### Options ```ts interface PrimaryOptions { @@ -16,12 +16,12 @@ interface PrimaryOptions { } ``` -### How to configure: +### How to configure ```js const stylelintConfig = { rules: { - 'stylelint-polaris/at-rule-disallowed-list': { + 'polaris/at-rule-disallowed-list': { // Using a RegExp ensures we disallow `@mixin id` and `@mixin id()` // https://regex101.com/r/PJYwuP/1 mixin: [/^disallowed-mixin($|\()/], @@ -31,7 +31,7 @@ const stylelintConfig = { }; ``` -### How to run: +### How to run All files: diff --git a/stylelint-polaris/plugins/at-rule-disallowed-list/index.js b/stylelint-polaris/plugins/at-rule-disallowed-list/index.js index ca79369fc2c..85ee5245a8c 100644 --- a/stylelint-polaris/plugins/at-rule-disallowed-list/index.js +++ b/stylelint-polaris/plugins/at-rule-disallowed-list/index.js @@ -8,10 +8,10 @@ const messages = stylelint.utils.ruleMessages(ruleName, { /** * @type {stylelint.RuleMessageFunc} */ - rejected: (atRuleName, atRuleId, disallowedPattern) => { - return `Invalid @${atRuleName} rule${ - atRuleId ? ` [${atRuleId}].` : '' - } Disallowed pattern [${disallowedPattern}]`; + rejected: (atRuleName, atRuleParams) => { + return `Unexpected @${atRuleName} rule${ + atRuleParams ? ` "${atRuleParams}"` : '' + }`; }, }); @@ -51,18 +51,12 @@ const {rule} = stylelint.createPlugin( if (!found || !found.length) return; - found.forEach((disallowedPattern) => { + found.forEach(() => { stylelint.utils.report({ ruleName, result, node: atRule, - message: messages.rejected( - atRuleName, - atRuleId, - isString(disallowedPattern) - ? disallowedPattern - : disallowedPattern.source, - ), + message: messages.rejected(atRuleName, atRuleId), }); }); }); diff --git a/stylelint-polaris/plugins/coverage/index.js b/stylelint-polaris/plugins/coverage/index.js index ea724c242ea..b96a6c44167 100644 --- a/stylelint-polaris/plugins/coverage/index.js +++ b/stylelint-polaris/plugins/coverage/index.js @@ -2,12 +2,36 @@ const stylelint = require('stylelint'); const {isPlainObject} = require('../../utils'); -const ruleName = 'polaris/coverage'; +const coverageRuleName = 'polaris/coverage'; + +/** + * @typedef {import('stylelint').ConfigRules} StylelintConfigRules + * @property {string} message - Message appended to the warning in place of the default category message + */ + +/** + * @typedef {object} CategorySettings + * @property {import('stylelint').RuleMessage} [message] - Message appended to the warning if no custom message is set on a rule's secondary options + * @property {(stylelintRuleName: string) => import('stylelint').RuleMeta} [meta] - Category documentation URL hyperlinked to the reported rule in the VS Code diagnostic + */ /** * @typedef {{ - * [category: string]: import('stylelint').ConfigRules - * }} PrimaryOptions + * [category: string]: StylelintConfigRules | [ + * StylelintConfigRules, CategorySettings + * ] + * }} PrimaryOptions - Configured Stylelint rules grouped by Polaris coverage category + */ + +/** + * @typedef {object} CoverageRule + * @property { string } ruleName - The dynamic coverage rule name + * @property { string } stylelintRuleName - The Stylelint rule name + * @property { import('stylelint').ConfigRuleSettings } ruleSettings - The Stylelint rule settings + * @property { import('stylelint').RuleSeverity } [severity] - The severity of the rule + * @property { boolean } fix - Whether the rule should be autofixed + * @property { string } [appendedMessage] - Message appended to the warning text + * @property { (stylelintRuleName: string) => import('stylelint').RuleMeta } [meta] - Category documentation URL hyperlinked to the reported rule in the VS Code diagnostic */ // Setting `line` to an invalid line number forces the warning to be reported @@ -16,65 +40,86 @@ const ruleName = 'polaris/coverage'; const forceReport = {line: -1}; module.exports = stylelint.createPlugin( - ruleName, - /** @param {PrimaryOptions} primaryOptions */ - (primaryOptions, secondaryOptions, context) => { - const isPrimaryOptionsValid = validatePrimaryOptions(primaryOptions); - - const rules = []; - - for (const [categoryName, categoryConfigRules] of Object.entries( - primaryOptions, - )) { - for (const [categoryRuleName, categoryRuleSettings] of Object.entries( - categoryConfigRules, + coverageRuleName, + /** + * @param {PrimaryOptions} categorizedRules + * @param {import('stylelint').RuleContext} context + */ + (categorizedRules, _, context) => { + const isPrimaryOptionsValid = validatePrimaryOptions(categorizedRules); + + /** @type {CoverageRule[]} */ + const coverageRules = []; + + for (const [category, categoryConfig] of Object.entries(categorizedRules)) { + const [stylelintRules, categorySettings] = + normalizeConfig(categoryConfig); + + for (const [stylelintRuleName, stylelintRuleSettings] of Object.entries( + stylelintRules, )) { - rules.push({ - coverageRuleName: `polaris/${categoryName}/${categoryRuleName}`, - categoryRuleName, - categoryRuleSettings, - categoryRuleSeverity: categoryRuleSettings?.[1]?.severity, - categoryRuleFix: - context.fix && !categoryRuleSettings?.[1]?.disableFix, + coverageRules.push({ + ruleName: `polaris/${category}/${stylelintRuleName}`, + stylelintRuleName, + ruleSettings: stylelintRuleSettings, + severity: stylelintRuleSettings?.[1]?.severity, + fix: context.fix && !stylelintRuleSettings?.[1]?.disableFix, + appendedMessage: + stylelintRuleSettings?.[1]?.message || categorySettings?.message, + meta: getMeta(category, stylelintRuleName), }); } } return (root, result) => { - const validOptions = stylelint.utils.validateOptions(result, ruleName, { - actual: isPrimaryOptionsValid, - }); + const validOptions = stylelint.utils.validateOptions( + result, + coverageRuleName, + { + actual: isPrimaryOptionsValid, + }, + ); if (!validOptions) return; - for (const rule of rules) { + for (const rule of coverageRules) { const { - coverageRuleName, - categoryRuleName, - categoryRuleSettings, - categoryRuleSeverity, - categoryRuleFix, + ruleName, + stylelintRuleName, + ruleSettings, + fix, + meta, + appendedMessage = '', + severity = result.stylelint.config?.defaultSeverity, } = rule; stylelint.utils.checkAgainstRule( { - ruleName: categoryRuleName, - ruleSettings: categoryRuleSettings, - fix: categoryRuleFix, + ruleName: stylelintRuleName, + ruleSettings, + fix, root, result, }, (warning) => { + const warningText = warning.text.replace( + ` (${stylelintRuleName})`, + '', + ); + + // We insert the meta for the rules on the stylelint result, because the rules are reported with dynamic rule names instead of each category being its own plugin. See Stylelint issue for context: https://github.com/stylelint/stylelint/issues/6513 + result.stylelint.ruleMetadata[ruleName] = meta; + + const message = appendedMessage + ? `${warningText} - ${appendedMessage}` + : warningText; + stylelint.utils.report({ result, - ruleName: coverageRuleName, - message: warning.text.replace(` (${categoryRuleName})`, ''), - severity: - categoryRuleSeverity ?? - result.stylelint.config?.defaultSeverity ?? - 'error', - // If `warning.node` is NOT present, the warning is - // referring to a misconfigured rule + ruleName, + message, + severity: severity || 'error', + // If `warning.node` is NOT present, the warning is referring to a misconfigured rule ...(warning.node ? {node: warning.node} : forceReport), }); }, @@ -84,11 +129,44 @@ module.exports = stylelint.createPlugin( }, ); -function validatePrimaryOptions(primaryOptions) { - if (!isPlainObject(primaryOptions)) return false; +/** + * @param {string} category + * @param {string} stylelintRuleName + * @returns {import('stylelint').RuleMeta} + */ +function getMeta(category, stylelintRuleName) { + const baseMetaUrl = + 'https://github.com/Shopify/polaris/tree/main/stylelint-polaris/README.md'; + + return { + url: `${baseMetaUrl}#${category}-${stylelintRuleName.replace('/', '-')}`, + }; +} + +/** + * @param {PrimaryOptions} categoryConfigRules + * @returns {[StylelintConfigRules, CategorySettings]} + */ +function normalizeConfig(categoryConfigRules) { + return Array.isArray(categoryConfigRules) + ? categoryConfigRules + : [categoryConfigRules, {}]; +} + +function validatePrimaryOptions(categorizedRules) { + if (!isPlainObject(categorizedRules)) return false; - for (const categoryConfigRules of Object.values(primaryOptions)) { - if (!isPlainObject(categoryConfigRules)) return false; + for (const categoryConfig of Object.values(categorizedRules)) { + if ( + !( + isPlainObject(categoryConfig) || + (Array.isArray(categoryConfig) && + categoryConfig.length === 2 && + categoryConfig.every(isPlainObject)) + ) + ) { + return false; + } } return true; diff --git a/stylelint-polaris/plugins/coverage/index.test.js b/stylelint-polaris/plugins/coverage/index.test.js index d75100e6ee1..c380faf5cd1 100644 --- a/stylelint-polaris/plugins/coverage/index.test.js +++ b/stylelint-polaris/plugins/coverage/index.test.js @@ -3,6 +3,18 @@ const path = require('path'); const {ruleName} = require('.'); const config = { + colors: [ + { + 'color-named': 'never', + 'color-no-hex': [ + true, + {message: 'Appended Stylelint rule config message'}, + ], + }, + { + message: 'Appended category rule config message', + }, + ], motion: { 'at-rule-disallowed-list': [['keyframes'], {severity: 'warning'}], }, @@ -25,6 +37,18 @@ testRule({ ], reject: [ + { + code: '.class {color: #bad;}', + description: 'Overrides appended category rule warning text (string)', + message: + 'Unexpected hex color "#bad" - Appended Stylelint rule config message', + }, + { + code: '.class {color: red;}', + description: 'Appends message to category rule warning text', + message: + 'Unexpected named color "red" - Appended category rule config message', + }, { code: '@keyframes foo {}', description: 'Uses disallowed at-rule (built-in rule)', diff --git a/stylelint-polaris/plugins/custom-properties-allowed-list/README.md b/stylelint-polaris/plugins/custom-property-allowed-list/README.md similarity index 84% rename from stylelint-polaris/plugins/custom-properties-allowed-list/README.md rename to stylelint-polaris/plugins/custom-property-allowed-list/README.md index 1dc9f4d3074..a91c43d7e3c 100644 --- a/stylelint-polaris/plugins/custom-properties-allowed-list/README.md +++ b/stylelint-polaris/plugins/custom-property-allowed-list/README.md @@ -1,4 +1,4 @@ -## Allowed Custom Properties plugin +## Custom property allowed list plugin The purpose of this plugin is to ensure that we're following our established conventions for Polaris custom properties, and only using custom properties that are generated Polaris tokens. @@ -26,12 +26,12 @@ interface PrimaryOptions { } ``` -### How to configure: +### How to configure ```js const stylelintConfig = { rules: { - 'stylelint-polaris/custom-properties-allowed-list': { + 'polaris/custom-property-allowed-list': { allowedProperties: ['/--pc-.+/'], allowedValues: { width: ['--p-space-0', '--p-space-1' /* etc... */], @@ -75,10 +75,10 @@ e.x. output ``` src/components/TextContainer/TextContainer.scss - 4:3 ✖ Unexpected custom property [--p-text-container-spacing]. @shopify/custom-properties-allowed-list - 6:5 ✖ Invalid custom properties [--p-text-container-spacing]. @shopify/custom-properties-allowed-list - 15:3 ✖ Unexpected custom property [--p-text-container-spacing]. @shopify/custom-properties-allowed-list - 19:3 ✖ Unexpected custom property [--p-text-container-spacing]. @shopify/custom-properties-allowed-list + 4:3 ✖ Unexpected custom property [--p-text-container-spacing]. polaris/custom-property-allowed-list + 6:5 ✖ Invalid custom properties [--p-text-container-spacing]. polaris/custom-property-allowed-list + 15:3 ✖ Unexpected custom property [--p-text-container-spacing]. polaris/custom-property-allowed-list + 19:3 ✖ Unexpected custom property [--p-text-container-spacing]. polaris/custom-property-allowed-list ``` > Note: `--p-text-container-spacing` is not a valid Polaris custom property from [`tokens.ts`](../../../../src/tokens/tokens.ts). This custom property should use the local component prefix `--pc-` instead. @@ -86,6 +86,6 @@ src/components/TextContainer/TextContainer.scss ## FUTURE - Think about how to keep polaris tokens in sync in both plugin and `polaris-react` - (e.g. If `@shopify/custom-properties-allowed-list` plugin is separated from `polaris-react`) + (e.g. If `@shopify/custom-property-allowed-list` plugin is separated from `polaris-react`) - Share token generator functions? (e.g. `getPolarisCustomProperty`) - Validate color-scheme tokens have the same key value pairs: https://github.com/Shopify/polaris-react/issues/4803 diff --git a/stylelint-polaris/plugins/custom-properties-allowed-list/index.js b/stylelint-polaris/plugins/custom-property-allowed-list/index.js similarity index 91% rename from stylelint-polaris/plugins/custom-properties-allowed-list/index.js rename to stylelint-polaris/plugins/custom-property-allowed-list/index.js index e236254200f..2a6cfd2c3de 100644 --- a/stylelint-polaris/plugins/custom-properties-allowed-list/index.js +++ b/stylelint-polaris/plugins/custom-property-allowed-list/index.js @@ -9,22 +9,22 @@ const { isString, } = require('../../utils'); -const ruleName = 'polaris/custom-properties-allowed-list'; +const ruleName = 'polaris/custom-property-allowed-list'; const messages = stylelint.utils.ruleMessages(ruleName, { /** * @type {stylelint.RuleMessageFunc} */ - rejected: (invalidProperty, invalidValues) => { + rejected: (prop, value, invalidProperty, invalidValue) => { const invalidPropertyMessage = invalidProperty - ? `Unexpected custom property [${invalidProperty}].` + ? `Unexpected custom property "${prop}"` : null; - const invalidValuesMessage = invalidValues - ? `Invalid custom properties [${invalidValues}].` + const invalidValueMessage = invalidValue + ? `Unexpected value "var(${invalidValue})" for property "${prop}"` : null; - return [invalidPropertyMessage, invalidValuesMessage] + return [invalidPropertyMessage, invalidValueMessage] .filter(Boolean) .join(' '); }, @@ -74,6 +74,7 @@ const {rule} = stylelint.createPlugin( allowedProperties, prop, ); + const invalidValues = validateCustomPropertyValues( allowedValues, prop, @@ -84,6 +85,8 @@ const {rule} = stylelint.createPlugin( stylelint.utils.report({ message: messages.rejected( + prop, + value, /** @type {string} */ (invalidProperty), /** @type {string} */ (invalidValues?.join(', ')), ), diff --git a/stylelint-polaris/plugins/custom-properties-allowed-list/index.test.js b/stylelint-polaris/plugins/custom-property-allowed-list/index.test.js similarity index 88% rename from stylelint-polaris/plugins/custom-properties-allowed-list/index.test.js rename to stylelint-polaris/plugins/custom-property-allowed-list/index.test.js index c219aa0dd6b..962156dc1f5 100644 --- a/stylelint-polaris/plugins/custom-properties-allowed-list/index.test.js +++ b/stylelint-polaris/plugins/custom-property-allowed-list/index.test.js @@ -78,7 +78,7 @@ testRule({ code: '.a { --p-test: red; }', description: 'Defining custom-properties that start with --p- is disallowed', - message: messages.rejected('--p-test'), + message: messages.rejected('--p-test', 'red', true, undefined), line: 1, column: 6, endLine: 1, @@ -88,7 +88,7 @@ testRule({ code: '.a { --pc-test: red; }', description: 'Defining custom-properties that start with --pc- is disallowed', - message: messages.rejected('--pc-test'), + message: messages.rejected('--pc-test', 'red', true, undefined), line: 1, column: 6, endLine: 1, @@ -98,7 +98,12 @@ testRule({ code: '.a { color: var(--p-unknown); }', description: 'Using --p- prefixed tokens that do not exist in polaris-tokens is disallowed', - message: messages.rejected(undefined, '--p-unknown'), + message: messages.rejected( + 'color', + 'var(--p-unknown)', + false, + '--p-unknown', + ), line: 1, column: 6, endLine: 1, @@ -107,7 +112,7 @@ testRule({ { code: '.a { color: var(--pc-test); }', description: 'Using --pc- prefixed tokens is disallowed', - message: messages.rejected(undefined, '--pc-test'), + message: messages.rejected('color', 'var(--pc-test)', false, '--pc-test'), line: 1, column: 6, endLine: 1, @@ -139,7 +144,7 @@ testRule({ code: '.a { --p-test: red; }', description: 'Defining custom-properties that start with --p- is disallowed', - message: messages.rejected('--p-test'), + message: messages.rejected('--p-test', 'red', true, undefined), line: 1, column: 6, endLine: 1, @@ -149,7 +154,7 @@ testRule({ code: '.a { --test: red; }', description: "Defining custom-properties that don't start with --p- or --pc- is disallowed", - message: messages.rejected('--test'), + message: messages.rejected('--test', 'red', true, undefined), line: 1, column: 6, endLine: 1, @@ -158,7 +163,7 @@ testRule({ { code: '.a { color: var(--test); }', description: 'Using other custom-properties', - message: messages.rejected(undefined, '--test'), + message: messages.rejected('color', 'var(--test)', false, '--test'), line: 1, column: 6, endLine: 1, diff --git a/stylelint-polaris/plugins/global-disallowed-list/index.test.js b/stylelint-polaris/plugins/global-disallowed-list/index.test.js index 7c04109d438..23ac74c7a3a 100644 --- a/stylelint-polaris/plugins/global-disallowed-list/index.test.js +++ b/stylelint-polaris/plugins/global-disallowed-list/index.test.js @@ -1,6 +1,6 @@ -const {messages, ruleName} = require('.'); +const {ruleName} = require('.'); -const config = [[/rem\(/, /--p-button-font/]]; +const config = [[/\$font-size-data/, /--p-button-font/]]; testRule({ ruleName, @@ -16,18 +16,20 @@ testRule({ reject: [ { - code: '.a { font-size: rem(20px); }', + code: '.a { font-size: $font-size-data; }', description: 'Uses something on the disallowed list', - message: messages.rejected('rem('), + message: + 'Unexpected disallowed value "$font-size-data" (polaris/global-disallowed-list)', line: 1, column: 17, endLine: 1, - endColumn: 21, + endColumn: 32, }, { code: '.a { color: var(--p-button-font); }', description: 'Uses something on the disallowed list', - message: messages.rejected('--p-button-font'), + message: + 'Unexpected disallowed value "--p-button-font" (polaris/global-disallowed-list)', line: 1, column: 17, endLine: 1, diff --git a/stylelint-polaris/plugins/media-queries-allowed-list/README.md b/stylelint-polaris/plugins/media-query-allowed-list/README.md similarity index 90% rename from stylelint-polaris/plugins/media-queries-allowed-list/README.md rename to stylelint-polaris/plugins/media-query-allowed-list/README.md index c7bdd2b9a7d..50b33e4fa15 100644 --- a/stylelint-polaris/plugins/media-queries-allowed-list/README.md +++ b/stylelint-polaris/plugins/media-query-allowed-list/README.md @@ -1,4 +1,4 @@ -## Media queries allowed list plugin +## Media query allowed list plugin The purpose of this plugin is to ensure we're following our established conventions for Polaris breakpoints, and only using breakpoints aliases generated from Polaris tokens. @@ -11,7 +11,7 @@ The purpose of this plugin is to ensure we're following our established conventi ## How to use -### Options: +### Options ```ts interface PrimaryOptions { @@ -33,12 +33,12 @@ interface PrimaryOptions { } ``` -### How to configure: +### How to configure ```js const stylelintConfig = { rules: { - 'stylelint-polaris/media-queries-allowed-list': { + 'polaris/media-query-allowed-list': { allowedMediaTypes: ['print'], allowedMediaFeatureNames: ['forced-colors', 'reduced-motion'], allowedScssInterpolations: [ @@ -80,6 +80,6 @@ e.x. output ``` src/components/TextContainer/TextContainer.scss - 4:3 ✖ Invalid media query [(min-width: 0px)]. @shopify/media-queries-allowed-list - 6:5 ✖ Invalid media query [print and (min-width: 0px)]. @shopify/media-queries-allowed-list + 4:3 ✖ Invalid media query [(min-width: 0px)]. polaris/media-query-allowed-list + 6:5 ✖ Invalid media query [print and (min-width: 0px)]. polaris/media-query-allowed-list ``` diff --git a/stylelint-polaris/plugins/media-queries-allowed-list/index.js b/stylelint-polaris/plugins/media-query-allowed-list/index.js similarity index 96% rename from stylelint-polaris/plugins/media-queries-allowed-list/index.js rename to stylelint-polaris/plugins/media-query-allowed-list/index.js index 56be3258d6d..fc59ff3ced6 100644 --- a/stylelint-polaris/plugins/media-queries-allowed-list/index.js +++ b/stylelint-polaris/plugins/media-query-allowed-list/index.js @@ -10,13 +10,13 @@ const { matchesStringOrRegExp, } = require('../../utils'); -const ruleName = 'polaris/media-queries-allowed-list'; +const ruleName = 'polaris/media-query-allowed-list'; const messages = stylelint.utils.ruleMessages(ruleName, { /** * @type {stylelint.RuleMessageFunc} */ - rejected: (invalidMedia) => `Invalid media query [${invalidMedia}].`, + rejected: (invalidMedia) => `Unexpected media query "${invalidMedia}"`, }); /** @typedef {(string | RegExp)[]} AllowedPatterns */ diff --git a/stylelint-polaris/plugins/media-queries-allowed-list/index.test.js b/stylelint-polaris/plugins/media-query-allowed-list/index.test.js similarity index 100% rename from stylelint-polaris/plugins/media-queries-allowed-list/index.test.js rename to stylelint-polaris/plugins/media-query-allowed-list/index.test.js diff --git a/stylelint-polaris/utils/index.js b/stylelint-polaris/utils/index.js index d6392145880..8b2a385e99f 100644 --- a/stylelint-polaris/utils/index.js +++ b/stylelint-polaris/utils/index.js @@ -230,6 +230,27 @@ function isString(value) { return typeof value === 'string' || value instanceof String; } +/** + * Returns the arguments expected by Stylelint rules that support functional custom messages + * @param {string} ruleName The category's default message + * @param {import('postcss').Node} node The node being reported as a problem + * @returns {Parameters | undefined} An array of arguments for stylelint.report to invoke the functional message with + */ +function getMessageArgs(ruleName, node) { + if (!node) return undefined; + + const stylelintRuleMessageArgs = { + 'color-no-hex': [node.value], + 'function-disallowed-list': [node.value], + 'at-rule-disallowed-list': [node.name, node.params], + 'property-disallowed-list': [node.prop], + 'declaration-property-value-disallowed-list': [node.prop, node.value], + 'declaration-property-unit-disallowed-list': [node.prop, node.value], + }; + + return stylelintRuleMessageArgs[ruleName]; +} + module.exports.hasScssInterpolation = hasScssInterpolation; module.exports.isBoolean = isBoolean; module.exports.isCustomProperty = isCustomProperty; @@ -243,3 +264,4 @@ module.exports.scssInterpolationExpression = scssInterpolationExpression; module.exports.scssInterpolationRegExp = scssInterpolationRegExp; module.exports.vendorPrefix = vendorPrefix; module.exports.vendorUnprefixed = vendorUnprefixed; +module.exports.getMessageArgs = getMessageArgs;