diff --git a/code-pushup.config.ts b/code-pushup.config.ts index bd089d884..37046a09e 100644 --- a/code-pushup.config.ts +++ b/code-pushup.config.ts @@ -1,4 +1,5 @@ import 'dotenv/config'; +import path from 'node:path'; import { z } from 'zod'; import { coverageCoreConfigNx, @@ -7,6 +8,8 @@ import { lighthouseCoreConfig, } from './code-pushup.preset.js'; import type { CoreConfig } from './packages/models/src/index.js'; +import { stylelintPlugin } from './packages/plugin-stylelint/src/lib/stylelint-plugin'; +import { getCategoryRefsFromGroups } from './packages/plugin-stylelint/src/lib/utils'; import { mergeConfigs } from './packages/utils/src/index.js'; // load upload configuration from environment @@ -30,7 +33,9 @@ const config: CoreConfig = { plugins: [], }; - +const fixturesDir = 'packages/plugin-stylelint/mocks/fixtures'; +const stylelintrc = path.join(fixturesDir, 'scss', '.stylelintrc.extends.json'); +const patterns = `${fixturesDir}/scss/**/*.scss`; export default mergeConfigs( config, await coverageCoreConfigNx(), @@ -39,4 +44,43 @@ export default mergeConfigs( 'https://github.com/code-pushup/cli?tab=readme-ov-file#code-pushup-cli/', ), await eslintCoreConfigNx(), + { + plugins: [ + await stylelintPlugin([ + { + stylelintrc, + patterns, + }, + ]), + ], + categories: [ + { + slug: 'code-style', + title: 'Code style', + description: + 'Lint rules that promote **good practices** and consistency in your code.', + refs: ( + await getCategoryRefsFromGroups([ + { + stylelintrc, + patterns, + }, + ]) + ).filter(ref => ref.slug === 'suggestions'), + }, + { + slug: 'bug-prevention', + title: 'Bug Prevention', + description: 'Lint rules that help **prevent bugs** in your code.', + refs: ( + await getCategoryRefsFromGroups([ + { + stylelintrc, + patterns, // Adjust the path to your CSS files + }, + ]) + ).filter(ref => ref.slug === 'problems'), + }, + ], + }, ); diff --git a/e2e/plugin-stylelint-e2e/eslint.config.js b/e2e/plugin-stylelint-e2e/eslint.config.js new file mode 100644 index 000000000..2656b27cb --- /dev/null +++ b/e2e/plugin-stylelint-e2e/eslint.config.js @@ -0,0 +1,12 @@ +import tseslint from 'typescript-eslint'; +import baseConfig from '../../eslint.config.js'; + +export default tseslint.config(...baseConfig, { + files: ['**/*.ts'], + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, +}); diff --git a/e2e/plugin-stylelint-e2e/mocks/fixtures/default-setup/.stylelintrc.json b/e2e/plugin-stylelint-e2e/mocks/fixtures/default-setup/.stylelintrc.json new file mode 100644 index 000000000..c576b1144 --- /dev/null +++ b/e2e/plugin-stylelint-e2e/mocks/fixtures/default-setup/.stylelintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "stylelint-config-standard", + "rules": { + "color-no-invalid-hex": null + } +} diff --git a/e2e/plugin-stylelint-e2e/mocks/fixtures/default-setup/.stylelintrc.next.json b/e2e/plugin-stylelint-e2e/mocks/fixtures/default-setup/.stylelintrc.next.json new file mode 100644 index 000000000..1acbbd48a --- /dev/null +++ b/e2e/plugin-stylelint-e2e/mocks/fixtures/default-setup/.stylelintrc.next.json @@ -0,0 +1,6 @@ +{ + "extends": "stylelint-config-standard", + "rules": { + "color-no-invalid-hex": true + } +} diff --git a/e2e/plugin-stylelint-e2e/mocks/fixtures/default-setup/code-pushup.config.next.ts b/e2e/plugin-stylelint-e2e/mocks/fixtures/default-setup/code-pushup.config.next.ts new file mode 100644 index 000000000..0d297c7ba --- /dev/null +++ b/e2e/plugin-stylelint-e2e/mocks/fixtures/default-setup/code-pushup.config.next.ts @@ -0,0 +1,10 @@ +import stylelintPlugin from '@code-pushup/stylelint-plugin'; + +export default { + plugins: [ + await stylelintPlugin({ + stylelintrc: '.stylelintrc.next.json', + patterns: ['styles/*.css'], + }), + ], +}; diff --git a/e2e/plugin-stylelint-e2e/mocks/fixtures/default-setup/code-pushup.config.ts b/e2e/plugin-stylelint-e2e/mocks/fixtures/default-setup/code-pushup.config.ts new file mode 100644 index 000000000..7e65d795d --- /dev/null +++ b/e2e/plugin-stylelint-e2e/mocks/fixtures/default-setup/code-pushup.config.ts @@ -0,0 +1,10 @@ +import stylelintPlugin from '@code-pushup/stylelint-plugin'; + +export default { + plugins: [ + await stylelintPlugin({ + stylelintrc: '.stylelintrc.json', + patterns: ['./styles/*.css'], + }), + ], +}; diff --git a/e2e/plugin-stylelint-e2e/mocks/fixtures/default-setup/styles/styles.css b/e2e/plugin-stylelint-e2e/mocks/fixtures/default-setup/styles/styles.css new file mode 100644 index 000000000..ed63685b5 --- /dev/null +++ b/e2e/plugin-stylelint-e2e/mocks/fixtures/default-setup/styles/styles.css @@ -0,0 +1,8 @@ +.passing-always { + color: #000; +} + +.failing-in-next { + color: #00; +} + diff --git a/e2e/plugin-stylelint-e2e/project.json b/e2e/plugin-stylelint-e2e/project.json new file mode 100644 index 000000000..24d049279 --- /dev/null +++ b/e2e/plugin-stylelint-e2e/project.json @@ -0,0 +1,23 @@ +{ + "name": "plugin-stylelint-e2e", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "e2e/plugin-stylelint-e2e/src", + "projectType": "application", + "targets": { + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["e2e/plugin-stylelint-e2e/**/*.ts"] + } + }, + "e2e": { + "executor": "@nx/vite:test", + "options": { + "configFile": "e2e/plugin-stylelint-e2e/vite.config.e2e.ts" + } + } + }, + "implicitDependencies": ["cli", "plugin-stylelint"], + "tags": ["scope:plugin", "type:e2e"] +} diff --git a/e2e/plugin-stylelint-e2e/tests/__snapshots__/report.json b/e2e/plugin-stylelint-e2e/tests/__snapshots__/report.json new file mode 100644 index 000000000..cd0eef827 --- /dev/null +++ b/e2e/plugin-stylelint-e2e/tests/__snapshots__/report.json @@ -0,0 +1,715 @@ +{ + "packageName": "@code-pushup/core", + "plugins": [ + { + "packageName": "@code-pushup/stylelint-plugin", + "title": "Code stylelint", + "slug": "stylelint", + "icon": "folder-css", + "description": "Official Code PushUp code stylelint plugin.", + "docsUrl": "https://www.npmjs.com/package/@code-pushup/stylelint-plugin/", + "audits": [ + { + "slug": "color-no-invalid-hex", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "color-no-invalid-hex", + "docsUrl": "https://stylelint.io/user-guide/rules/color-no-invalid-hex" + }, + { + "slug": "alpha-value-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "alpha-value-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/alpha-value-notation" + }, + { + "slug": "at-rule-empty-line-before", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "at-rule-empty-line-before", + "docsUrl": "https://stylelint.io/user-guide/rules/at-rule-empty-line-before" + }, + { + "slug": "at-rule-no-vendor-prefix", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "at-rule-no-vendor-prefix", + "docsUrl": "https://stylelint.io/user-guide/rules/at-rule-no-vendor-prefix" + }, + { + "slug": "color-function-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "color-function-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/color-function-notation" + }, + { + "slug": "color-hex-length", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "color-hex-length", + "docsUrl": "https://stylelint.io/user-guide/rules/color-hex-length" + }, + { + "slug": "comment-empty-line-before", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "comment-empty-line-before", + "docsUrl": "https://stylelint.io/user-guide/rules/comment-empty-line-before" + }, + { + "slug": "comment-whitespace-inside", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "comment-whitespace-inside", + "docsUrl": "https://stylelint.io/user-guide/rules/comment-whitespace-inside" + }, + { + "slug": "custom-property-empty-line-before", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "custom-property-empty-line-before", + "docsUrl": "https://stylelint.io/user-guide/rules/custom-property-empty-line-before" + }, + { + "slug": "custom-media-pattern", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "custom-media-pattern", + "docsUrl": "https://stylelint.io/user-guide/rules/custom-media-pattern" + }, + { + "slug": "custom-property-pattern", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "custom-property-pattern", + "docsUrl": "https://stylelint.io/user-guide/rules/custom-property-pattern" + }, + { + "slug": "declaration-block-no-redundant-longhand-properties", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "declaration-block-no-redundant-longhand-properties", + "docsUrl": "https://stylelint.io/user-guide/rules/declaration-block-no-redundant-longhand-properties" + }, + { + "slug": "declaration-block-single-line-max-declarations", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "declaration-block-single-line-max-declarations", + "docsUrl": "https://stylelint.io/user-guide/rules/declaration-block-single-line-max-declarations" + }, + { + "slug": "declaration-empty-line-before", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "declaration-empty-line-before", + "docsUrl": "https://stylelint.io/user-guide/rules/declaration-empty-line-before" + }, + { + "slug": "font-family-name-quotes", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "font-family-name-quotes", + "docsUrl": "https://stylelint.io/user-guide/rules/font-family-name-quotes" + }, + { + "slug": "function-name-case", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "function-name-case", + "docsUrl": "https://stylelint.io/user-guide/rules/function-name-case" + }, + { + "slug": "function-url-quotes", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "function-url-quotes", + "docsUrl": "https://stylelint.io/user-guide/rules/function-url-quotes" + }, + { + "slug": "hue-degree-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "hue-degree-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/hue-degree-notation" + }, + { + "slug": "import-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "import-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/import-notation" + }, + { + "slug": "keyframe-selector-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "keyframe-selector-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/keyframe-selector-notation" + }, + { + "slug": "keyframes-name-pattern", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "keyframes-name-pattern", + "docsUrl": "https://stylelint.io/user-guide/rules/keyframes-name-pattern" + }, + { + "slug": "length-zero-no-unit", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "length-zero-no-unit", + "docsUrl": "https://stylelint.io/user-guide/rules/length-zero-no-unit" + }, + { + "slug": "lightness-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "lightness-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/lightness-notation" + }, + { + "slug": "media-feature-name-no-vendor-prefix", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "media-feature-name-no-vendor-prefix", + "docsUrl": "https://stylelint.io/user-guide/rules/media-feature-name-no-vendor-prefix" + }, + { + "slug": "media-feature-range-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "media-feature-range-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/media-feature-range-notation" + }, + { + "slug": "number-max-precision", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "number-max-precision", + "docsUrl": "https://stylelint.io/user-guide/rules/number-max-precision" + }, + { + "slug": "property-no-vendor-prefix", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "property-no-vendor-prefix", + "docsUrl": "https://stylelint.io/user-guide/rules/property-no-vendor-prefix" + }, + { + "slug": "rule-empty-line-before", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "rule-empty-line-before", + "docsUrl": "https://stylelint.io/user-guide/rules/rule-empty-line-before" + }, + { + "slug": "selector-attribute-quotes", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-attribute-quotes", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-attribute-quotes" + }, + { + "slug": "selector-class-pattern", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-class-pattern", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-class-pattern" + }, + { + "slug": "selector-id-pattern", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-id-pattern", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-id-pattern" + }, + { + "slug": "selector-no-vendor-prefix", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-no-vendor-prefix", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-no-vendor-prefix" + }, + { + "slug": "selector-not-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-not-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-not-notation" + }, + { + "slug": "selector-pseudo-element-colon-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-pseudo-element-colon-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-pseudo-element-colon-notation" + }, + { + "slug": "selector-type-case", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-type-case", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-type-case" + }, + { + "slug": "shorthand-property-no-redundant-values", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "shorthand-property-no-redundant-values", + "docsUrl": "https://stylelint.io/user-guide/rules/shorthand-property-no-redundant-values" + }, + { + "slug": "value-keyword-case", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "value-keyword-case", + "docsUrl": "https://stylelint.io/user-guide/rules/value-keyword-case" + }, + { + "slug": "value-no-vendor-prefix", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "value-no-vendor-prefix", + "docsUrl": "https://stylelint.io/user-guide/rules/value-no-vendor-prefix" + }, + { + "slug": "annotation-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "annotation-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/annotation-no-unknown" + }, + { + "slug": "at-rule-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "at-rule-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/at-rule-no-unknown" + }, + { + "slug": "block-no-empty", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "block-no-empty", + "docsUrl": "https://stylelint.io/user-guide/rules/block-no-empty" + }, + { + "slug": "comment-no-empty", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "comment-no-empty", + "docsUrl": "https://stylelint.io/user-guide/rules/comment-no-empty" + }, + { + "slug": "custom-property-no-missing-var-function", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "custom-property-no-missing-var-function", + "docsUrl": "https://stylelint.io/user-guide/rules/custom-property-no-missing-var-function" + }, + { + "slug": "declaration-block-no-duplicate-custom-properties", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "declaration-block-no-duplicate-custom-properties", + "docsUrl": "https://stylelint.io/user-guide/rules/declaration-block-no-duplicate-custom-properties" + }, + { + "slug": "declaration-block-no-duplicate-properties", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "declaration-block-no-duplicate-properties", + "docsUrl": "https://stylelint.io/user-guide/rules/declaration-block-no-duplicate-properties" + }, + { + "slug": "declaration-block-no-shorthand-property-overrides", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "declaration-block-no-shorthand-property-overrides", + "docsUrl": "https://stylelint.io/user-guide/rules/declaration-block-no-shorthand-property-overrides" + }, + { + "slug": "font-family-no-duplicate-names", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "font-family-no-duplicate-names", + "docsUrl": "https://stylelint.io/user-guide/rules/font-family-no-duplicate-names" + }, + { + "slug": "font-family-no-missing-generic-family-keyword", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "font-family-no-missing-generic-family-keyword", + "docsUrl": "https://stylelint.io/user-guide/rules/font-family-no-missing-generic-family-keyword" + }, + { + "slug": "function-calc-no-unspaced-operator", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "function-calc-no-unspaced-operator", + "docsUrl": "https://stylelint.io/user-guide/rules/function-calc-no-unspaced-operator" + }, + { + "slug": "function-linear-gradient-no-nonstandard-direction", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "function-linear-gradient-no-nonstandard-direction", + "docsUrl": "https://stylelint.io/user-guide/rules/function-linear-gradient-no-nonstandard-direction" + }, + { + "slug": "function-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "function-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/function-no-unknown" + }, + { + "slug": "keyframe-block-no-duplicate-selectors", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "keyframe-block-no-duplicate-selectors", + "docsUrl": "https://stylelint.io/user-guide/rules/keyframe-block-no-duplicate-selectors" + }, + { + "slug": "keyframe-declaration-no-important", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "keyframe-declaration-no-important", + "docsUrl": "https://stylelint.io/user-guide/rules/keyframe-declaration-no-important" + }, + { + "slug": "media-feature-name-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "media-feature-name-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/media-feature-name-no-unknown" + }, + { + "slug": "media-query-no-invalid", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "media-query-no-invalid", + "docsUrl": "https://stylelint.io/user-guide/rules/media-query-no-invalid" + }, + { + "slug": "named-grid-areas-no-invalid", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "named-grid-areas-no-invalid", + "docsUrl": "https://stylelint.io/user-guide/rules/named-grid-areas-no-invalid" + }, + { + "slug": "no-descending-specificity", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "no-descending-specificity", + "docsUrl": "https://stylelint.io/user-guide/rules/no-descending-specificity" + }, + { + "slug": "no-duplicate-at-import-rules", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "no-duplicate-at-import-rules", + "docsUrl": "https://stylelint.io/user-guide/rules/no-duplicate-at-import-rules" + }, + { + "slug": "no-duplicate-selectors", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "no-duplicate-selectors", + "docsUrl": "https://stylelint.io/user-guide/rules/no-duplicate-selectors" + }, + { + "slug": "no-empty-source", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "no-empty-source", + "docsUrl": "https://stylelint.io/user-guide/rules/no-empty-source" + }, + { + "slug": "no-invalid-double-slash-comments", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "no-invalid-double-slash-comments", + "docsUrl": "https://stylelint.io/user-guide/rules/no-invalid-double-slash-comments" + }, + { + "slug": "no-invalid-position-at-import-rule", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "no-invalid-position-at-import-rule", + "docsUrl": "https://stylelint.io/user-guide/rules/no-invalid-position-at-import-rule" + }, + { + "slug": "no-irregular-whitespace", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "no-irregular-whitespace", + "docsUrl": "https://stylelint.io/user-guide/rules/no-irregular-whitespace" + }, + { + "slug": "property-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "property-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/property-no-unknown" + }, + { + "slug": "selector-anb-no-unmatchable", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-anb-no-unmatchable", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-anb-no-unmatchable" + }, + { + "slug": "selector-pseudo-class-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-pseudo-class-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-pseudo-class-no-unknown" + }, + { + "slug": "selector-pseudo-element-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-pseudo-element-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-pseudo-element-no-unknown" + }, + { + "slug": "selector-type-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-type-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-type-no-unknown" + }, + { + "slug": "string-no-newline", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "string-no-newline", + "docsUrl": "https://stylelint.io/user-guide/rules/string-no-newline" + }, + { + "slug": "unit-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "unit-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/unit-no-unknown" + } + ] + } + ] +} \ No newline at end of file diff --git a/e2e/plugin-stylelint-e2e/tests/__snapshots__/report.next.json b/e2e/plugin-stylelint-e2e/tests/__snapshots__/report.next.json new file mode 100644 index 000000000..0ade20af8 --- /dev/null +++ b/e2e/plugin-stylelint-e2e/tests/__snapshots__/report.next.json @@ -0,0 +1,726 @@ +{ + "packageName": "@code-pushup/core", + "plugins": [ + { + "packageName": "@code-pushup/stylelint-plugin", + "title": "Code stylelint", + "slug": "stylelint", + "icon": "folder-css", + "description": "Official Code PushUp code stylelint plugin.", + "docsUrl": "https://www.npmjs.com/package/@code-pushup/stylelint-plugin/", + "audits": [ + { + "slug": "color-no-invalid-hex", + "value": 1, + "score": 0, + "details": { + "issues": [ + { + "message": "Unexpected invalid hex color \"#00\" (color-no-invalid-hex)", + "severity": "error", + "source": { + "file": "tmp/e2e/plugin-stylelint-e2e/__test__/default-setup/styles/styles.css", + "position": { + "startLine": 6 + } + } + } + ] + }, + "title": "color-no-invalid-hex", + "docsUrl": "https://stylelint.io/user-guide/rules/color-no-invalid-hex" + }, + { + "slug": "alpha-value-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "alpha-value-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/alpha-value-notation" + }, + { + "slug": "at-rule-empty-line-before", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "at-rule-empty-line-before", + "docsUrl": "https://stylelint.io/user-guide/rules/at-rule-empty-line-before" + }, + { + "slug": "at-rule-no-vendor-prefix", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "at-rule-no-vendor-prefix", + "docsUrl": "https://stylelint.io/user-guide/rules/at-rule-no-vendor-prefix" + }, + { + "slug": "color-function-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "color-function-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/color-function-notation" + }, + { + "slug": "color-hex-length", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "color-hex-length", + "docsUrl": "https://stylelint.io/user-guide/rules/color-hex-length" + }, + { + "slug": "comment-empty-line-before", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "comment-empty-line-before", + "docsUrl": "https://stylelint.io/user-guide/rules/comment-empty-line-before" + }, + { + "slug": "comment-whitespace-inside", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "comment-whitespace-inside", + "docsUrl": "https://stylelint.io/user-guide/rules/comment-whitespace-inside" + }, + { + "slug": "custom-property-empty-line-before", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "custom-property-empty-line-before", + "docsUrl": "https://stylelint.io/user-guide/rules/custom-property-empty-line-before" + }, + { + "slug": "custom-media-pattern", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "custom-media-pattern", + "docsUrl": "https://stylelint.io/user-guide/rules/custom-media-pattern" + }, + { + "slug": "custom-property-pattern", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "custom-property-pattern", + "docsUrl": "https://stylelint.io/user-guide/rules/custom-property-pattern" + }, + { + "slug": "declaration-block-no-redundant-longhand-properties", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "declaration-block-no-redundant-longhand-properties", + "docsUrl": "https://stylelint.io/user-guide/rules/declaration-block-no-redundant-longhand-properties" + }, + { + "slug": "declaration-block-single-line-max-declarations", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "declaration-block-single-line-max-declarations", + "docsUrl": "https://stylelint.io/user-guide/rules/declaration-block-single-line-max-declarations" + }, + { + "slug": "declaration-empty-line-before", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "declaration-empty-line-before", + "docsUrl": "https://stylelint.io/user-guide/rules/declaration-empty-line-before" + }, + { + "slug": "font-family-name-quotes", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "font-family-name-quotes", + "docsUrl": "https://stylelint.io/user-guide/rules/font-family-name-quotes" + }, + { + "slug": "function-name-case", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "function-name-case", + "docsUrl": "https://stylelint.io/user-guide/rules/function-name-case" + }, + { + "slug": "function-url-quotes", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "function-url-quotes", + "docsUrl": "https://stylelint.io/user-guide/rules/function-url-quotes" + }, + { + "slug": "hue-degree-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "hue-degree-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/hue-degree-notation" + }, + { + "slug": "import-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "import-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/import-notation" + }, + { + "slug": "keyframe-selector-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "keyframe-selector-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/keyframe-selector-notation" + }, + { + "slug": "keyframes-name-pattern", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "keyframes-name-pattern", + "docsUrl": "https://stylelint.io/user-guide/rules/keyframes-name-pattern" + }, + { + "slug": "length-zero-no-unit", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "length-zero-no-unit", + "docsUrl": "https://stylelint.io/user-guide/rules/length-zero-no-unit" + }, + { + "slug": "lightness-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "lightness-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/lightness-notation" + }, + { + "slug": "media-feature-name-no-vendor-prefix", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "media-feature-name-no-vendor-prefix", + "docsUrl": "https://stylelint.io/user-guide/rules/media-feature-name-no-vendor-prefix" + }, + { + "slug": "media-feature-range-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "media-feature-range-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/media-feature-range-notation" + }, + { + "slug": "number-max-precision", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "number-max-precision", + "docsUrl": "https://stylelint.io/user-guide/rules/number-max-precision" + }, + { + "slug": "property-no-vendor-prefix", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "property-no-vendor-prefix", + "docsUrl": "https://stylelint.io/user-guide/rules/property-no-vendor-prefix" + }, + { + "slug": "rule-empty-line-before", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "rule-empty-line-before", + "docsUrl": "https://stylelint.io/user-guide/rules/rule-empty-line-before" + }, + { + "slug": "selector-attribute-quotes", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-attribute-quotes", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-attribute-quotes" + }, + { + "slug": "selector-class-pattern", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-class-pattern", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-class-pattern" + }, + { + "slug": "selector-id-pattern", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-id-pattern", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-id-pattern" + }, + { + "slug": "selector-no-vendor-prefix", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-no-vendor-prefix", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-no-vendor-prefix" + }, + { + "slug": "selector-not-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-not-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-not-notation" + }, + { + "slug": "selector-pseudo-element-colon-notation", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-pseudo-element-colon-notation", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-pseudo-element-colon-notation" + }, + { + "slug": "selector-type-case", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-type-case", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-type-case" + }, + { + "slug": "shorthand-property-no-redundant-values", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "shorthand-property-no-redundant-values", + "docsUrl": "https://stylelint.io/user-guide/rules/shorthand-property-no-redundant-values" + }, + { + "slug": "value-keyword-case", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "value-keyword-case", + "docsUrl": "https://stylelint.io/user-guide/rules/value-keyword-case" + }, + { + "slug": "value-no-vendor-prefix", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "value-no-vendor-prefix", + "docsUrl": "https://stylelint.io/user-guide/rules/value-no-vendor-prefix" + }, + { + "slug": "annotation-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "annotation-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/annotation-no-unknown" + }, + { + "slug": "at-rule-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "at-rule-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/at-rule-no-unknown" + }, + { + "slug": "block-no-empty", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "block-no-empty", + "docsUrl": "https://stylelint.io/user-guide/rules/block-no-empty" + }, + { + "slug": "comment-no-empty", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "comment-no-empty", + "docsUrl": "https://stylelint.io/user-guide/rules/comment-no-empty" + }, + { + "slug": "custom-property-no-missing-var-function", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "custom-property-no-missing-var-function", + "docsUrl": "https://stylelint.io/user-guide/rules/custom-property-no-missing-var-function" + }, + { + "slug": "declaration-block-no-duplicate-custom-properties", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "declaration-block-no-duplicate-custom-properties", + "docsUrl": "https://stylelint.io/user-guide/rules/declaration-block-no-duplicate-custom-properties" + }, + { + "slug": "declaration-block-no-duplicate-properties", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "declaration-block-no-duplicate-properties", + "docsUrl": "https://stylelint.io/user-guide/rules/declaration-block-no-duplicate-properties" + }, + { + "slug": "declaration-block-no-shorthand-property-overrides", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "declaration-block-no-shorthand-property-overrides", + "docsUrl": "https://stylelint.io/user-guide/rules/declaration-block-no-shorthand-property-overrides" + }, + { + "slug": "font-family-no-duplicate-names", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "font-family-no-duplicate-names", + "docsUrl": "https://stylelint.io/user-guide/rules/font-family-no-duplicate-names" + }, + { + "slug": "font-family-no-missing-generic-family-keyword", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "font-family-no-missing-generic-family-keyword", + "docsUrl": "https://stylelint.io/user-guide/rules/font-family-no-missing-generic-family-keyword" + }, + { + "slug": "function-calc-no-unspaced-operator", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "function-calc-no-unspaced-operator", + "docsUrl": "https://stylelint.io/user-guide/rules/function-calc-no-unspaced-operator" + }, + { + "slug": "function-linear-gradient-no-nonstandard-direction", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "function-linear-gradient-no-nonstandard-direction", + "docsUrl": "https://stylelint.io/user-guide/rules/function-linear-gradient-no-nonstandard-direction" + }, + { + "slug": "function-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "function-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/function-no-unknown" + }, + { + "slug": "keyframe-block-no-duplicate-selectors", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "keyframe-block-no-duplicate-selectors", + "docsUrl": "https://stylelint.io/user-guide/rules/keyframe-block-no-duplicate-selectors" + }, + { + "slug": "keyframe-declaration-no-important", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "keyframe-declaration-no-important", + "docsUrl": "https://stylelint.io/user-guide/rules/keyframe-declaration-no-important" + }, + { + "slug": "media-feature-name-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "media-feature-name-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/media-feature-name-no-unknown" + }, + { + "slug": "media-query-no-invalid", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "media-query-no-invalid", + "docsUrl": "https://stylelint.io/user-guide/rules/media-query-no-invalid" + }, + { + "slug": "named-grid-areas-no-invalid", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "named-grid-areas-no-invalid", + "docsUrl": "https://stylelint.io/user-guide/rules/named-grid-areas-no-invalid" + }, + { + "slug": "no-descending-specificity", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "no-descending-specificity", + "docsUrl": "https://stylelint.io/user-guide/rules/no-descending-specificity" + }, + { + "slug": "no-duplicate-at-import-rules", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "no-duplicate-at-import-rules", + "docsUrl": "https://stylelint.io/user-guide/rules/no-duplicate-at-import-rules" + }, + { + "slug": "no-duplicate-selectors", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "no-duplicate-selectors", + "docsUrl": "https://stylelint.io/user-guide/rules/no-duplicate-selectors" + }, + { + "slug": "no-empty-source", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "no-empty-source", + "docsUrl": "https://stylelint.io/user-guide/rules/no-empty-source" + }, + { + "slug": "no-invalid-double-slash-comments", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "no-invalid-double-slash-comments", + "docsUrl": "https://stylelint.io/user-guide/rules/no-invalid-double-slash-comments" + }, + { + "slug": "no-invalid-position-at-import-rule", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "no-invalid-position-at-import-rule", + "docsUrl": "https://stylelint.io/user-guide/rules/no-invalid-position-at-import-rule" + }, + { + "slug": "no-irregular-whitespace", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "no-irregular-whitespace", + "docsUrl": "https://stylelint.io/user-guide/rules/no-irregular-whitespace" + }, + { + "slug": "property-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "property-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/property-no-unknown" + }, + { + "slug": "selector-anb-no-unmatchable", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-anb-no-unmatchable", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-anb-no-unmatchable" + }, + { + "slug": "selector-pseudo-class-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-pseudo-class-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-pseudo-class-no-unknown" + }, + { + "slug": "selector-pseudo-element-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-pseudo-element-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-pseudo-element-no-unknown" + }, + { + "slug": "selector-type-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "selector-type-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/selector-type-no-unknown" + }, + { + "slug": "string-no-newline", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "string-no-newline", + "docsUrl": "https://stylelint.io/user-guide/rules/string-no-newline" + }, + { + "slug": "unit-no-unknown", + "value": 0, + "score": 1, + "details": { + "issues": [] + }, + "title": "unit-no-unknown", + "docsUrl": "https://stylelint.io/user-guide/rules/unit-no-unknown" + } + ] + } + ] +} \ No newline at end of file diff --git a/e2e/plugin-stylelint-e2e/tests/__snapshots__/terminal.next.txt b/e2e/plugin-stylelint-e2e/tests/__snapshots__/terminal.next.txt new file mode 100644 index 000000000..d67e6cd62 --- /dev/null +++ b/e2e/plugin-stylelint-e2e/tests/__snapshots__/terminal.next.txt @@ -0,0 +1,24 @@ +Code PushUp CLI +[ info ] Run collect... +Code PushUp Report - @code-pushup/core@0.57.0 + + +Code stylelint audits + +● color-no-invalid-hex 1 +● ... 69 audits with perfect scores omitted for brevity ... + +Made with ❤ by code-pushup.dev + +[ success ] Collecting report successful! +[ info ] 💡 Configure categories to see the scores in an overview table. See: https://github.com/code-pushup/cli/blob/main/packages/cli/README.md +╭────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ 💡 Visualize your reports │ +│ │ +│ ❯ npx code-pushup upload - Run upload to upload the created report to the server │ +│ https://github.com/code-pushup/cli/tree/main/packages/cli#upload-command │ +│ ❯ npx code-pushup autorun - Run collect & upload │ +│ https://github.com/code-pushup/cli/tree/main/packages/cli#autorun-command │ +│ │ +╰────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/e2e/plugin-stylelint-e2e/tests/__snapshots__/terminal.txt b/e2e/plugin-stylelint-e2e/tests/__snapshots__/terminal.txt new file mode 100644 index 000000000..60a340e3b --- /dev/null +++ b/e2e/plugin-stylelint-e2e/tests/__snapshots__/terminal.txt @@ -0,0 +1,23 @@ +Code PushUp CLI +[ info ] Run collect... +Code PushUp Report - @code-pushup/core@0.57.0 + + +Code stylelint audits + +● ... All 70 audits have perfect scores ... + +Made with ❤ by code-pushup.dev + +[ success ] Collecting report successful! +[ info ] 💡 Configure categories to see the scores in an overview table. See: https://github.com/code-pushup/cli/blob/main/packages/cli/README.md +╭────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ 💡 Visualize your reports │ +│ │ +│ ❯ npx code-pushup upload - Run upload to upload the created report to the server │ +│ https://github.com/code-pushup/cli/tree/main/packages/cli#upload-command │ +│ ❯ npx code-pushup autorun - Run collect & upload │ +│ https://github.com/code-pushup/cli/tree/main/packages/cli#autorun-command │ +│ │ +╰────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/e2e/plugin-stylelint-e2e/tests/collect.e2e.test.ts b/e2e/plugin-stylelint-e2e/tests/collect.e2e.test.ts new file mode 100644 index 000000000..ee028f81c --- /dev/null +++ b/e2e/plugin-stylelint-e2e/tests/collect.e2e.test.ts @@ -0,0 +1,92 @@ +import { cp } from 'node:fs/promises'; +import path from 'node:path'; +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; +import { type Report, reportSchema } from '@code-pushup/models'; +import { nxTargetProject } from '@code-pushup/test-nx-utils'; +import { teardownTestFolder } from '@code-pushup/test-setup'; +import { + E2E_ENVIRONMENTS_DIR, + TEST_OUTPUT_DIR, + omitVariableReportData, + removeColorCodes, +} from '@code-pushup/test-utils'; +import { executeProcess, readJsonFile } from '@code-pushup/utils'; + +describe('PLUGIN collect report with eslint-plugin NPM package', () => { + const fixturesDir = path.join( + 'e2e', + 'plugin-stylelint-e2e', + 'mocks', + 'fixtures', + ); + const fixturesDefaultSetupDir = path.join(fixturesDir, 'default-setup'); + + const envRoot = path.join( + E2E_ENVIRONMENTS_DIR, + nxTargetProject(), + TEST_OUTPUT_DIR, + ); + const testFileDir = path.join(envRoot, 'default-setup'); + const flatConfigOutputDir = path.join(testFileDir, '.code-pushup'); + + beforeAll(async () => { + await cp(fixturesDefaultSetupDir, testFileDir, { recursive: true }); + }); + + afterAll(async () => { + await teardownTestFolder(testFileDir); + }); + + afterEach(async () => { + await teardownTestFolder(flatConfigOutputDir); + }); + + it('should run StyleLint plugin for prod example and create report.json', async () => { + const { code, stdout } = await executeProcess({ + command: 'npx', + args: ['@code-pushup/cli', 'collect', '--no-progress'], + cwd: testFileDir, + }); + + expect(code).toBe(0); + + const report = await readJsonFile( + path.join(flatConfigOutputDir, 'report.json'), + ); + expect(removeColorCodes(stdout)).toMatchFileSnapshot( + '__snapshots__/terminal.txt', + ); + + expect(() => reportSchema.parse(report)).not.toThrow(); + expect( + JSON.stringify(omitVariableReportData(report as Report), null, 2), + ).toMatchFileSnapshot('__snapshots__/report.json'); + }); + + it('should run StyleLint plugin for next example and create report.json', async () => { + const { code, stdout } = await executeProcess({ + command: 'npx', + args: [ + '@code-pushup/cli', + 'collect', + '--config=code-pushup.config.next.ts', + '--no-progress', + ], + cwd: testFileDir, + }); + + expect(code).toBe(0); + + const report = await readJsonFile( + path.join(flatConfigOutputDir, 'report.json'), + ); + expect(removeColorCodes(stdout)).toMatchFileSnapshot( + '__snapshots__/terminal.next.txt', + ); + + expect(() => reportSchema.parse(report)).not.toThrow(); + expect( + JSON.stringify(omitVariableReportData(report as Report), null, 2), + ).toMatchFileSnapshot('__snapshots__/report.next.json'); + }); +}); diff --git a/e2e/plugin-stylelint-e2e/tsconfig.json b/e2e/plugin-stylelint-e2e/tsconfig.json new file mode 100644 index 000000000..f5a2f890a --- /dev/null +++ b/e2e/plugin-stylelint-e2e/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "types": ["vitest"] + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.test.json" + } + ] +} diff --git a/e2e/plugin-stylelint-e2e/tsconfig.test.json b/e2e/plugin-stylelint-e2e/tsconfig.test.json new file mode 100644 index 000000000..34f35e30f --- /dev/null +++ b/e2e/plugin-stylelint-e2e/tsconfig.test.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"], + "target": "ES2020" + }, + "exclude": ["__test-env__/**"], + "include": [ + "vite.config.e2e.ts", + "tests/**/*.e2e.test.ts", + "tests/**/*.d.ts", + "mocks/**/*.ts" + ] +} diff --git a/e2e/plugin-stylelint-e2e/vite.config.e2e.ts b/e2e/plugin-stylelint-e2e/vite.config.e2e.ts new file mode 100644 index 000000000..4f715aa2c --- /dev/null +++ b/e2e/plugin-stylelint-e2e/vite.config.e2e.ts @@ -0,0 +1,21 @@ +/// <reference types="vitest" /> +import { defineConfig } from 'vite'; +import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js'; + +export default defineConfig({ + cacheDir: '../../node_modules/.vite/plugin-lighthouse-e2e', + test: { + reporters: ['basic'], + testTimeout: 120_000, + globals: true, + alias: tsconfigPathAliases(), + pool: 'threads', + poolOptions: { threads: { singleThread: true } }, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'node', + include: ['tests/**/*.e2e.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + setupFiles: ['../../testing/test-setup/src/lib/reset.mocks.ts'], + }, +}); diff --git a/package-lock.json b/package-lock.json index 0459e1180..29fd52b49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,8 +25,13 @@ "multi-progress-bars": "^5.0.3", "nx": "19.8.13", "parse-lcov": "^1.0.4", + "postcss-less": "^6.0.0", + "postcss-scss": "^4.0.9", "semver": "^7.6.3", "simple-git": "^3.26.0", + "stylelint-config-recommended": "^14.0.1", + "stylelint-config-standard": "^36.0.1", + "stylelint-config-standard-scss": "^14.0.0", "tslib": "^2.6.2", "vscode-material-icons": "^0.1.1", "yaml": "^2.5.1", @@ -94,6 +99,9 @@ "prettier": "^3.4.1", "react": "18.3.1", "react-dom": "18.3.1", + "stylelint": "^16.12.0", + "stylelint-order": "^6.0.4", + "stylelint-scss": "^6.10.0", "tsconfig-paths": "^4.2.0", "tsx": "^4.19.0", "type-fest": "^4.26.1", @@ -141,7 +149,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "dev": true, "dependencies": { "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" @@ -523,7 +530,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -569,7 +575,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", @@ -584,7 +589,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -596,7 +600,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -610,7 +613,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "engines": { "node": ">=0.8.0" } @@ -619,7 +621,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -2633,6 +2634,88 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", + "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", + "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.2.tgz", + "integrity": "sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, "node_modules/@cypress/request": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.6.tgz", @@ -2689,6 +2772,15 @@ "node": ">=16" } }, + "node_modules/@dual-bundle/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/@emnapi/core": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.2.0.tgz", @@ -8750,7 +8842,6 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8972,7 +9063,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, "engines": { "node": ">=8" } @@ -9129,6 +9219,14 @@ "node": ">=4" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -10016,7 +10114,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -10808,7 +10905,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -10816,8 +10912,12 @@ "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" }, "node_modules/colorette": { "version": "2.0.20", @@ -11565,7 +11665,6 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dev": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -11607,14 +11706,12 @@ "node_modules/cosmiconfig/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/cosmiconfig/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -11674,6 +11771,14 @@ "resolved": "https://registry.npmjs.org/csp_evaluator/-/csp_evaluator-1.1.1.tgz", "integrity": "sha512-N3ASg0C4kNPUaNxt1XAvzHIVuzdtr8KLgfk1O8WDyimp1GisPAHESupArO2ieHk9QWbrJ/WkQODyh21Ps/xhxw==" }, + "node_modules/css-functions-list": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz", + "integrity": "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==", + "engines": { + "node": ">=12 || >=16" + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -11721,6 +11826,17 @@ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csso": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", @@ -12238,7 +12354,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, "dependencies": { "path-type": "^4.0.0" }, @@ -12535,7 +12650,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, "engines": { "node": ">=6" } @@ -12567,7 +12681,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -14602,7 +14715,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -14618,7 +14730,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -14631,7 +14742,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -14640,7 +14750,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -14653,7 +14762,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -14698,14 +14806,20 @@ "node_modules/fast-uri": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "dev": true + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "engines": { + "node": ">= 4.9.1" + } }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -14855,7 +14969,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -15488,7 +15601,6 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -15504,6 +15616,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -15652,7 +15769,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "engines": { "node": ">=4" } @@ -15777,6 +15893,17 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/http-assert": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", @@ -16474,8 +16601,7 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-async-function": { "version": "2.0.0", @@ -16754,7 +16880,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -16796,7 +16921,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -19026,8 +19150,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "3.14.1", @@ -19142,8 +19265,7 @@ "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema": { "version": "0.4.0", @@ -19154,8 +19276,7 @@ "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -19331,6 +19452,14 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -19474,6 +19603,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/known-css-properties": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", + "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==" + }, "node_modules/koa": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", @@ -19832,6 +19966,11 @@ "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", "dev": true }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==" + }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -20228,6 +20367,15 @@ "node": ">= 0.4" } }, + "node_modules/mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -20299,7 +20447,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -20322,7 +20469,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -20613,7 +20759,6 @@ "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, "funding": [ { "type": "github", @@ -20774,7 +20919,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -21607,7 +21751,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -21624,8 +21767,7 @@ "node_modules/parse-json/node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/parse-lcov": { "version": "1.0.4", @@ -21736,7 +21878,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -21792,16 +21933,14 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -22006,10 +22145,9 @@ } }, "node_modules/postcss": { - "version": "8.4.45", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz", - "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==", - "dev": true, + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -22026,13 +22164,110 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-less": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-6.0.0.tgz", + "integrity": "sha512-FPX16mQLyEjLzEuuJtxA8X3ejDLNGGEG503d2YGZR5Ask1SpDN8KmZUMpzCvyalWRywAn1n1VOA5dcqfCLo5rg==", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "postcss": "^8.3.5" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==" + }, + "node_modules/postcss-resolve-nested-selector": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", + "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==" + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-sorting": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-8.0.2.tgz", + "integrity": "sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==", + "dev": true, + "peerDependencies": { + "postcss": "^8.4.20" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -22299,7 +22534,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -22780,7 +23014,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -22831,7 +23064,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, "engines": { "node": ">=8" } @@ -22899,7 +23131,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -23068,7 +23299,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -23514,7 +23744,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "engines": { "node": ">=8" } @@ -23651,10 +23880,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true, + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } @@ -24214,23 +24442,415 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/summary": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/summary/-/summary-2.1.0.tgz", - "integrity": "sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw==", - "dev": true - }, - "node_modules/supports-color": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", - "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", - "engines": { - "node": ">=12" + "node_modules/stylelint": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.12.0.tgz", + "integrity": "sha512-F8zZ3L/rBpuoBZRvI4JVT20ZanPLXfQLzMOZg1tzPflRVh9mKpOZ8qcSIhh1my3FjAjZWG4T2POwGnmn6a6hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/media-query-list-parser": "^4.0.2", + "@csstools/selector-specificity": "^5.0.0", + "@dual-bundle/import-meta-resolve": "^4.1.0", + "balanced-match": "^2.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^9.0.0", + "css-functions-list": "^3.2.3", + "css-tree": "^3.0.1", + "debug": "^4.3.7", + "fast-glob": "^3.3.2", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^9.1.0", + "global-modules": "^2.0.0", + "globby": "^11.1.0", + "globjoin": "^0.1.4", + "html-tags": "^3.3.1", + "ignore": "^6.0.2", + "imurmurhash": "^0.1.4", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.35.0", + "mathml-tag-names": "^2.1.3", + "meow": "^13.2.0", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-resolve-nested-selector": "^0.1.6", + "postcss-safe-parser": "^7.0.1", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0", + "resolve-from": "^5.0.0", + "string-width": "^4.2.3", + "supports-hyperlinks": "^3.1.0", + "svg-tags": "^1.0.0", + "table": "^6.9.0", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/stylelint-config-recommended": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-14.0.1.tgz", + "integrity": "sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.1.0" + } + }, + "node_modules/stylelint-config-recommended-scss": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-14.1.0.tgz", + "integrity": "sha512-bhaMhh1u5dQqSsf6ri2GVWWQW5iUjBYgcHkh7SgDDn92ijoItC/cfO/W+fpXshgTQWhwFkP1rVcewcv4jaftRg==", + "dependencies": { + "postcss-scss": "^4.0.9", + "stylelint-config-recommended": "^14.0.1", + "stylelint-scss": "^6.4.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "postcss": "^8.3.3", + "stylelint": "^16.6.1" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + } + } + }, + "node_modules/stylelint-config-standard": { + "version": "36.0.1", + "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-36.0.1.tgz", + "integrity": "sha512-8aX8mTzJ6cuO8mmD5yon61CWuIM4UD8Q5aBcWKGSf6kg+EC3uhB+iOywpTK4ca6ZL7B49en8yanOFtUW0qNzyw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "dependencies": { + "stylelint-config-recommended": "^14.0.1" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.1.0" + } + }, + "node_modules/stylelint-config-standard-scss": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-standard-scss/-/stylelint-config-standard-scss-14.0.0.tgz", + "integrity": "sha512-6Pa26D9mHyi4LauJ83ls3ELqCglU6VfCXchovbEqQUiEkezvKdv6VgsIoMy58i00c854wVmOw0k8W5FTpuaVqg==", + "dependencies": { + "stylelint-config-recommended-scss": "^14.1.0", + "stylelint-config-standard": "^36.0.1" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "postcss": "^8.3.3", + "stylelint": "^16.11.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + } + } + }, + "node_modules/stylelint-order": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/stylelint-order/-/stylelint-order-6.0.4.tgz", + "integrity": "sha512-0UuKo4+s1hgQ/uAxlYU4h0o0HS4NiQDud0NAUNI0aa8FJdmYHA5ZZTFHiV5FpmE3071e9pZx5j0QpVJW5zOCUA==", + "dev": true, + "dependencies": { + "postcss": "^8.4.32", + "postcss-sorting": "^8.0.2" + }, + "peerDependencies": { + "stylelint": "^14.0.0 || ^15.0.0 || ^16.0.1" + } + }, + "node_modules/stylelint-scss": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.10.0.tgz", + "integrity": "sha512-y03if6Qw9xBMoVaf7tzp5BbnYhYvudIKzURkhSHzcHG0bW0fAYvQpTUVJOe7DyhHaxeThBil4ObEMvGbV7+M+w==", + "dependencies": { + "css-tree": "^3.0.1", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.35.0", + "mdn-data": "^2.12.2", + "postcss-media-query-parser": "^0.2.3", + "postcss-resolve-nested-selector": "^0.1.6", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.0.2" + } + }, + "node_modules/stylelint-scss/node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/stylelint-scss/node_modules/css-tree/node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==" + }, + "node_modules/stylelint-scss/node_modules/mdn-data": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.14.0.tgz", + "integrity": "sha512-QjcSiIvUHjmXp5wNLClRjQeU0Zp+I2Dag+AhtQto0nyKYZ3IF/pUzCuHe7Bv77EC92XE5t3EXeEiEv/to2Bwig==" + }, + "node_modules/stylelint/node_modules/balanced-match": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==" + }, + "node_modules/stylelint/node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/stylelint/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/stylelint/node_modules/file-entry-cache": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.1.0.tgz", + "integrity": "sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==", + "dependencies": { + "flat-cache": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/stylelint/node_modules/flat-cache": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-5.0.0.tgz", + "integrity": "sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ==", + "dependencies": { + "flatted": "^3.3.1", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/stylelint/node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/stylelint/node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/stylelint/node_modules/ignore": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", + "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/stylelint/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/stylelint/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==" + }, + "node_modules/stylelint/node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/stylelint/node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/summary": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/summary/-/summary-2.1.0.tgz", + "integrity": "sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw==", + "dev": true + }, + "node_modules/supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "engines": { + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/supports-hyperlinks": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.1.0.tgz", + "integrity": "sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A==", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-hyperlinks/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -24249,6 +24869,11 @@ "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "dev": true }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==" + }, "node_modules/svgo": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", @@ -24289,6 +24914,104 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/table/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/table/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/table/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/table/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -24703,7 +25426,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, diff --git a/package.json b/package.json index 2782021cf..179837356 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,13 @@ "multi-progress-bars": "^5.0.3", "nx": "19.8.13", "parse-lcov": "^1.0.4", + "postcss-less": "^6.0.0", + "postcss-scss": "^4.0.9", "semver": "^7.6.3", "simple-git": "^3.26.0", + "stylelint-config-recommended": "^14.0.1", + "stylelint-config-standard": "^36.0.1", + "stylelint-config-standard-scss": "^14.0.0", "tslib": "^2.6.2", "vscode-material-icons": "^0.1.1", "yaml": "^2.5.1", @@ -107,6 +112,9 @@ "prettier": "^3.4.1", "react": "18.3.1", "react-dom": "18.3.1", + "stylelint": "^16.12.0", + "stylelint-order": "^6.0.4", + "stylelint-scss": "^6.10.0", "tsconfig-paths": "^4.2.0", "tsx": "^4.19.0", "type-fest": "^4.26.1", diff --git a/packages/plugin-stylelint/README.md b/packages/plugin-stylelint/README.md new file mode 100644 index 000000000..3185c8397 --- /dev/null +++ b/packages/plugin-stylelint/README.md @@ -0,0 +1,214 @@ +# @code-pushup/stylelint-plugin + +[](https://www.npmjs.com/package/@code-pushup/stylelint-plugin) +[](https://npmtrends.com/@code-pushup/stylelint-plugin) +[](https://www.npmjs.com/package/@code-pushup/stylelint-plugin?activeTab=dependencies) + +🧪 **Code PushUp plugin for tracking code stylelint.** ☂️ + +> [!IMPORTANT] +> In order to successfully run your stylelint tool and gather stylelint results directly within the plugin, all your tests need to pass! + +## Getting started + +1. If you haven't already, install [@code-pushup/cli](../cli/README.md) and create a configuration file. + +2. Prepare either existing code stylelint result files or a command for a stylelint tool of your choice that will generate the results. Set lcov as the reporter to the configuration (example for Jest [here](https://jestjs.io/docs/configuration#stylelintreporters-arraystring--string-options)). + +3. Add this plugin to the `plugins` array in your Code PushUp CLI config file (e.g. `code-pushup.config.js`). + + ```js + import stylelintPlugin from '@code-pushup/stylelint-plugin'; + + export default { + // ... + plugins: [ + // ... + await stylelintPlugin({ + reports: ['stylelint/lcov.info'], + stylelintToolCommand: { + command: 'npx', + args: ['jest', '--stylelint', '--stylelintReporters=lcov'], + }, + }), + ], + }; + ``` + +4. (Optional) Reference individual audits or the provided plugin group which you wish to include in custom categories (use `npx code-pushup print-config` to list audits and groups). + + 💡 Assign weights based on what influence each stylelint type should have on the overall category score (assign weight 0 to only include as extra info, without influencing category score). + + ```js + export default { + // ... + categories: [ + { + slug: 'code-stylelint', + title: 'Code stylelint', + refs: [ + { + type: 'group', + plugin: 'stylelint', + slug: 'stylelint', + weight: 1, + }, + // ... + ], + }, + // ... + ], + }; + ``` + +5. Run the CLI with `npx code-pushup collect` and view or upload report (refer to [CLI docs](../cli/README.md)). + +## About code stylelint + +Code stylelint is a metric that indicates what percentage of source code is executed by unit tests. It can give insights into test effectiveness and uncover parts of source code that would otherwise go untested. + +- **Statement stylelint**: Measures how many statements are executed in at least one test. +- **Line stylelint**: Measures how many lines are executed in at least one test. Unlike statement stylelint, any partially executed line counts towards line stylelint. +- **Condition stylelint**: Measures all condition values (`true`/`false`) evaluated for a conditional statement in at least one test. +- **Branch stylelint**: Measures how many branches are executed as a result of conditional statements (`if`/`else` and other) in at least one test. In case of short-circuit logic, only executed paths are counted in. Unlike condition stylelint, it does not ensure all combinations of condition values are tested. +- **Function stylelint**: Measures how many functions are called in at least one test. Argument values, usage of optional arguments or default values is irrelevant for this metric. + +> [!IMPORTANT] +> Please note that code stylelint is not the same as test stylelint. Test stylelint measures the amount of acceptance criteria covered by tests and is hard to formally verify. This means that code stylelint cannot guarantee that the designed software caters to the business requirements. + +If you want to know more code stylelint and how each type of stylelint is measured, go to [Software Testing Help](https://www.softwaretestinghelp.com/code-stylelint-tutorial/). + +### LCOV format + +The LCOV format was originally used by [GCOV](https://gcc.gnu.org/onlinedocs/gcc/gcov/introduction-to-gcov.html) tool for stylelint results in C/C++ projects. +It recognises the following entities: + +- TN [test name] +- SF [source file] +- FN [line number] [function name] +- FNF [number of functions found] +- FNH [number of functions hit] +- FNDA [number of hits] [function name] +- BRDA [line number] [block number] [branch name] [number of hits] +- BRF [number of branches found] +- BRH [number of branches taken] +- DA [line number] [number of hits] +- LF [lines found] +- LH [lines hit] + +[Here](https://github.com/linux-test-project/lcov/issues/113#issuecomment-762335134) is the source of the information above. + +> [!NOTE] +> Branch name is usually a number indexed from 0, indicating either truthy/falsy condition or loop conditions. + +## Plugin architecture + +### Plugin configuration specification + +The plugin accepts the following parameters: + +- `stylelintTypes`: An array of types of stylelint that you wish to track. Supported values: `function`, `branch`, `line`. Defaults to all available types. +- `reports`: Array of information about files with code stylelint results. LCOV format is supported for now. + - For a single project, providing paths to results as strings is enough. + - If you have a monorepo, both path to results (`resultsPath`) and path from the root to project the results belong to (`pathToProject`) need to be provided for the LCOV format. For Nx monorepos, you can use our helper function `getNxCoveragePaths` to get the path information automatically. +- (optional) `stylelintToolCommand`: If you wish to run your stylelint tool to generate the results first, you may define it here. +- (optional) `perfectScoreThreshold`: If your stylelint goal is not 100%, you may define it here in range 0-1. Any score above the defined threshold will be given the perfect score. The value will stay unaffected. + +### Audits and group + +This plugin provides a group for convenient declaration in your config. When defined this way, all measured stylelint type audits have the same weight. + +```ts + // ... + categories: [ + { + slug: 'code-stylelint', + title: 'Code stylelint', + refs: [ + { + type: 'group', + plugin: 'stylelint', + slug: 'stylelint', + weight: 1, + }, + // ... + ], + }, + // ... + ], +``` + +Each stylelint type still has its own audit. So when you want to include a subset of stylelint types or assign different weights to them, you can do so in the following way: + +```ts + // ... + categories: [ + { + slug: 'code-stylelint', + title: 'Code stylelint', + refs: [ + { + type: 'audit', + plugin: 'stylelint', + slug: 'function-stylelint', + weight: 2, + }, + { + type: 'audit', + plugin: 'stylelint', + slug: 'branch-stylelint', + weight: 1, + }, + // ... + ], + }, + // ... + ], +``` + +### Audit output + +An audit is an aggregation of all results for one stylelint type passed to the plugin. + +For functions and branches, an issue points to a single instance of a branch or function not covered in any test and counts as an error. In line stylelint, one issue groups any amount of consecutive lines together to reduce the total amount of issues and counts as a warning. + +For instance, the following can be an audit output for line stylelint. + +```json +{ + "slug": "line-stylelint", + "displayValue": "95 %", + "score": 0.95, + "value": 95, + "details": { + "issues": [ + { + "message": "Lines 7-9 are not covered in any test case.", + "severity": "warning", + "source": { + "file": "packages/cli/src/lib/utils.ts", + "position": { + "startLine": 7, + "endLine": 9 + } + } + } + ] + } +} +``` + +### Coverage results alteration + +At the moment, the LCOV results include `(empty-report)` functions with missing stylelint. These point to various imports or exports, not actual functions. For that reason, they are omitted from the results. + +## Providing stylelint results in Nx monorepo + +As a part of the plugin, there is a `getNxCoveragePaths` helper for setting up paths to stylelint results if you are using Nx. The helper accepts all relevant targets (e.g. `test` or `unit-test`) and searches for a stylelint path option. +Jest and Vitest configuration options are currently supported: + +- For `@nx/jest` executor it looks for the `stylelintDirectory` option. +- For `@nx/vite` executor it looks for the `reportsDirectory` option. + +> [!IMPORTANT] +> Please note that you need to set up the stylelint directory option in your `project.json` target options. Test configuration files are not searched. diff --git a/packages/plugin-stylelint/eslint.config.js b/packages/plugin-stylelint/eslint.config.js new file mode 100644 index 000000000..40165321a --- /dev/null +++ b/packages/plugin-stylelint/eslint.config.js @@ -0,0 +1,21 @@ +import tseslint from 'typescript-eslint'; +import baseConfig from '../../eslint.config.js'; + +export default tseslint.config( + ...baseConfig, + { + files: ['**/*.ts'], + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + files: ['**/*.json'], + rules: { + '@nx/dependency-checks': 'error', + }, + }, +); diff --git a/packages/plugin-stylelint/mocks/fixtures/config-format/.stylelintrc.json b/packages/plugin-stylelint/mocks/fixtures/config-format/.stylelintrc.json new file mode 100644 index 000000000..fb8b10ea3 --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/config-format/.stylelintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "../stylelint-config/index.js", + "rules": { + "color-no-invalid-hex": true + } +} diff --git a/packages/plugin-stylelint/mocks/fixtures/config-format/.stylelintrc.yml b/packages/plugin-stylelint/mocks/fixtures/config-format/.stylelintrc.yml new file mode 100644 index 000000000..5a5f0f8db --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/config-format/.stylelintrc.yml @@ -0,0 +1,3 @@ +extends: ../stylelint-config/index.js +rules: + color-no-invalid-hex: true diff --git a/packages/plugin-stylelint/mocks/fixtures/config-format/color-no-invalid-hex.css b/packages/plugin-stylelint/mocks/fixtures/config-format/color-no-invalid-hex.css new file mode 100644 index 000000000..0fabe7294 --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/config-format/color-no-invalid-hex.css @@ -0,0 +1,4 @@ +p { + + color: #34; /* 👈 Invalid hex color */ +} diff --git a/packages/plugin-stylelint/mocks/fixtures/config-format/stylelint.config.cjs b/packages/plugin-stylelint/mocks/fixtures/config-format/stylelint.config.cjs new file mode 100644 index 000000000..6d6c9ba07 --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/config-format/stylelint.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + extends: "../stylelint-config/index.js", + rules: { + "color-no-invalid-hex": true, + }, +}; diff --git a/packages/plugin-stylelint/mocks/fixtures/config-format/stylelint.config.js b/packages/plugin-stylelint/mocks/fixtures/config-format/stylelint.config.js new file mode 100644 index 000000000..36edcc20e --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/config-format/stylelint.config.js @@ -0,0 +1,6 @@ +export default { + extends: "../stylelint-config/index.js", + rules: { + "color-no-invalid-hex": true, + }, +}; diff --git a/packages/plugin-stylelint/mocks/fixtures/config-format/stylelint.config.mjs b/packages/plugin-stylelint/mocks/fixtures/config-format/stylelint.config.mjs new file mode 100644 index 000000000..36edcc20e --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/config-format/stylelint.config.mjs @@ -0,0 +1,6 @@ +export default { + extends: "../stylelint-config/index.js", + rules: { + "color-no-invalid-hex": true, + }, +}; diff --git a/packages/plugin-stylelint/mocks/fixtures/css/.stylelintrc.color-no-invalid-hex.json b/packages/plugin-stylelint/mocks/fixtures/css/.stylelintrc.color-no-invalid-hex.json new file mode 100644 index 000000000..e3b05ced2 --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/css/.stylelintrc.color-no-invalid-hex.json @@ -0,0 +1,5 @@ +{ + "rules": { + "color-no-invalid-hex": true + } +} diff --git a/packages/plugin-stylelint/mocks/fixtures/css/.stylelintrc.extends.json b/packages/plugin-stylelint/mocks/fixtures/css/.stylelintrc.extends.json new file mode 100644 index 000000000..36b32e218 --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/css/.stylelintrc.extends.json @@ -0,0 +1,6 @@ +{ + "extends": "../stylelint-config/index", + "rules": { + "color-no-invalid-hex": true + } +} diff --git a/packages/plugin-stylelint/mocks/fixtures/css/all-core-rules-fail-error.css b/packages/plugin-stylelint/mocks/fixtures/css/all-core-rules-fail-error.css new file mode 100644 index 000000000..e73393dff --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/css/all-core-rules-fail-error.css @@ -0,0 +1,46 @@ +/* annotation-no-unknown */ +@unknown-annotation; /* Invalid: unknown annotation */ + +/* at-rule-no-unknown */ +@unknown-rule { color: red; } /* Invalid: unknown at-rule */ + +/* block-no-empty */ +.empty-block { } /* Invalid: empty block */ + +/* color-no-invalid-hex */ +.invalid-hex { color: #12345; } /* Invalid: invalid hex color */ + +/* custom-property-no-missing-var-function */ +:root { --custom-var: 100px; } +.invalid-var { width: var(custom-var); } /* Invalid: missing var function */ + +/* declaration-block-no-duplicate-properties */ +.duplicate-properties { + color: red; + color: blue; /* Invalid: duplicate property */ +} + +/* function-no-unknown */ +.unknown-function { color: unknown(255, 0, 0); } /* Invalid: unknown function */ + +/* keyframe-block-no-duplicate-selectors */ +@keyframes duplicate-keyframe { + 0% { opacity: 0; } + 0% { opacity: 1; } /* Invalid: duplicate keyframe selector */ +} + +/* media-feature-name-no-unknown */ +@media (unknown-feature: 100px) { color: red; } /* Invalid: unknown media feature */ + +/* no-duplicate-selectors */ +.duplicate-selectors { color: red; } +.duplicate-selectors { color: blue; } /* Invalid: duplicate selector */ + +/* property-no-unknown */ +.unknown-property { unknown: red; } /* Invalid: unknown property */ + +/* selector-pseudo-class-no-unknown */ +:unknown-class { color: red; } /* Invalid: unknown pseudo-class */ + +/* unit-no-unknown */ +.invalid-unit { width: 10pixels; } /* Invalid: unknown unit */ diff --git a/packages/plugin-stylelint/mocks/fixtures/css/all-core-rules-fail-warning.css b/packages/plugin-stylelint/mocks/fixtures/css/all-core-rules-fail-warning.css new file mode 100644 index 000000000..ecddb87f9 --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/css/all-core-rules-fail-warning.css @@ -0,0 +1,101 @@ +/* indentation */ +.invalid-indentation { + background-color: red; /* Invalid: no indentation */ +} + +/* max-line-length */ +.invalid-line-length { + /* Invalid: This is a very long comment that exceeds the maximum line length of 80 characters */ +} + +/* max-empty-lines */ + +.invalid-max-empty-lines { /* Invalid: More than 1 empty line */ } + + + +/* no-eol-whitespace */ +.invalid-eol-whitespace { color: red; } /* Invalid: trailing whitespace */ + +/* declaration-block-no-redundant-longhand-properties */ +.invalid-longhand { + margin-top: 10px; + margin-right: 20px; + margin-bottom: 10px; + margin-left: 20px; /* Invalid: redundant longhand */ +} + +/* declaration-block-semicolon-space-after */ +.invalid-semicolon-space { + color: red ;background: blue; /* Invalid: no space after semicolon */ +} + +/* declaration-block-semicolon-space-before */ +.invalid-semicolon-before { + color : red; /* Invalid: space before semicolon */ +} + +/* block-closing-brace-space-before */ +.invalid-brace-space { + color: red;} /* Invalid: no space before closing brace */ + +/* string-quotes */ +.invalid-string-quotes { + content: 'single quotes'; /* Invalid: single quotes */ +} + +/* font-family-name-quotes */ +.invalid-font-family-quotes { + font-family: Times New Roman; /* Invalid: missing quotes */ +} + +/* function-url-quotes */ +.invalid-url-quotes { + background: url(image.png); /* Invalid: missing quotes */ +} + +/* color-hex-case */ +.invalid-hex-case { + color: #ABC123; /* Invalid: uppercase hex */ +} + +/* color-hex-length */ +.invalid-hex-length { + color: #ffccaa; /* Invalid: long hex */ +} + +/* color-function-notation */ +.invalid-color-function { + color: rgb(255, 0, 0); /* Invalid: legacy notation */ +} + +/* lightness-notation */ +.invalid-lightness-notation { + color: hsl(0, 100%, 50); /* Invalid: missing percentage */ +} + +/* value-list-comma-space-after */ +.invalid-value-list-comma-space { + padding: 10px,20px; /* Invalid: no space after comma */ +} + +/* selector-list-comma-newline-after */ +.invalid-selector-list-comma-newline { + .class1, .class2 { color: red; } /* Invalid: no newline after comma */ +} + +/* comment-whitespace-inside */ +.invalid-comment-whitespace { + /*Invalid: no whitespace inside comments*/ +} + +/* keyframes-name-pattern */ +@keyframes InvalidKeyframe { /* Invalid: not kebab-case */ + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +/* alpha-value-notation */ +.invalid-alpha-value { + color: rgba(255, 0, 0, 0.5); /* Invalid: legacy alpha value */ +} diff --git a/packages/plugin-stylelint/mocks/fixtures/css/color-no-invalid-hex.css b/packages/plugin-stylelint/mocks/fixtures/css/color-no-invalid-hex.css new file mode 100644 index 000000000..0fabe7294 --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/css/color-no-invalid-hex.css @@ -0,0 +1,4 @@ +p { + + color: #34; /* 👈 Invalid hex color */ +} diff --git a/packages/plugin-stylelint/mocks/fixtures/less/.stylelintrc.color-no-invalid-hex.json b/packages/plugin-stylelint/mocks/fixtures/less/.stylelintrc.color-no-invalid-hex.json new file mode 100644 index 000000000..b1f03489c --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/less/.stylelintrc.color-no-invalid-hex.json @@ -0,0 +1,6 @@ +{ + "customSyntax": "postcss-less", + "rules": { + "color-no-invalid-hex": true + } +} diff --git a/packages/plugin-stylelint/mocks/fixtures/less/.stylelintrc.extends.json b/packages/plugin-stylelint/mocks/fixtures/less/.stylelintrc.extends.json new file mode 100644 index 000000000..4b62f5cb0 --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/less/.stylelintrc.extends.json @@ -0,0 +1,7 @@ +{ + "extends": "../stylelint-config/index", + "customSyntax": "postcss-less", + "rules": { + "color-no-invalid-hex": true + } +} diff --git a/packages/plugin-stylelint/mocks/fixtures/less/color-no-invalid-hex.less b/packages/plugin-stylelint/mocks/fixtures/less/color-no-invalid-hex.less new file mode 100644 index 000000000..0fabe7294 --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/less/color-no-invalid-hex.less @@ -0,0 +1,4 @@ +p { + + color: #34; /* 👈 Invalid hex color */ +} diff --git a/packages/plugin-stylelint/mocks/fixtures/less/stylelint.scss.js b/packages/plugin-stylelint/mocks/fixtures/less/stylelint.scss.js new file mode 100644 index 000000000..bddde67e6 --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/less/stylelint.scss.js @@ -0,0 +1,13 @@ +module.exports = { + extends: [ + "stylelint-config-standard", // Optional: Base configuration + "stylelint-config-standard-scss", // Optional: SCSS-specific rules + ], + plugins: ["stylelint-scss"], // Include the SCSS plugin + customSyntax: "postcss-scss", // Use the SCSS parser + rules: { + // Add your custom rules here + "at-rule-no-unknown": null, // Disable the core rule for unknown at-rules + "scss/at-rule-no-unknown": true, // Enable the SCSS-specific rule + }, +}; diff --git a/packages/plugin-stylelint/mocks/fixtures/scss/.stylelintrc.color-no-invalid-hex.json b/packages/plugin-stylelint/mocks/fixtures/scss/.stylelintrc.color-no-invalid-hex.json new file mode 100644 index 000000000..e3b05ced2 --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/scss/.stylelintrc.color-no-invalid-hex.json @@ -0,0 +1,5 @@ +{ + "rules": { + "color-no-invalid-hex": true + } +} diff --git a/packages/plugin-stylelint/mocks/fixtures/scss/.stylelintrc.extends.json b/packages/plugin-stylelint/mocks/fixtures/scss/.stylelintrc.extends.json new file mode 100644 index 000000000..36b32e218 --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/scss/.stylelintrc.extends.json @@ -0,0 +1,6 @@ +{ + "extends": "../stylelint-config/index", + "rules": { + "color-no-invalid-hex": true + } +} diff --git a/packages/plugin-stylelint/mocks/fixtures/scss/color-no-invalid-hex.scss b/packages/plugin-stylelint/mocks/fixtures/scss/color-no-invalid-hex.scss new file mode 100644 index 000000000..0fabe7294 --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/scss/color-no-invalid-hex.scss @@ -0,0 +1,4 @@ +p { + + color: #34; /* 👈 Invalid hex color */ +} diff --git a/packages/plugin-stylelint/mocks/fixtures/scss/stylelint.scss.js b/packages/plugin-stylelint/mocks/fixtures/scss/stylelint.scss.js new file mode 100644 index 000000000..bddde67e6 --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/scss/stylelint.scss.js @@ -0,0 +1,13 @@ +module.exports = { + extends: [ + "stylelint-config-standard", // Optional: Base configuration + "stylelint-config-standard-scss", // Optional: SCSS-specific rules + ], + plugins: ["stylelint-scss"], // Include the SCSS plugin + customSyntax: "postcss-scss", // Use the SCSS parser + rules: { + // Add your custom rules here + "at-rule-no-unknown": null, // Disable the core rule for unknown at-rules + "scss/at-rule-no-unknown": true, // Enable the SCSS-specific rule + }, +}; diff --git a/packages/plugin-stylelint/mocks/fixtures/stylelint-config/color-no-invalid-hex.css b/packages/plugin-stylelint/mocks/fixtures/stylelint-config/color-no-invalid-hex.css new file mode 100644 index 000000000..788153b39 --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/stylelint-config/color-no-invalid-hex.css @@ -0,0 +1,9 @@ +$primary-color: #34; + +body { + color: $primary-color; + + .container { + margin: 0 auto; + } +} diff --git a/packages/plugin-stylelint/mocks/fixtures/stylelint-config/index.js b/packages/plugin-stylelint/mocks/fixtures/stylelint-config/index.js new file mode 100644 index 000000000..99f67c2ef --- /dev/null +++ b/packages/plugin-stylelint/mocks/fixtures/stylelint-config/index.js @@ -0,0 +1,165 @@ +/** + * Standard Stylelint configuration that extends the stylelint-config-standard. + * "Avoid errors" rules are set to "error" severity. + * "Enforce conventions" rules are set to "warning" severity. + */ + +const stylelintConfig = { + extends: ['stylelint-config-standard'], + rules: { + // = Avoid errors - set as errors + + // == Descending + 'no-descending-specificity': [true, { severity: 'error' }], + + // == Duplicate + 'declaration-block-no-duplicate-custom-properties': [true, { severity: 'error' }], + 'declaration-block-no-duplicate-properties': [ + true, + { severity: 'error', ignore: ['consecutive-duplicates-with-different-syntaxes'] }, + ], + 'font-family-no-duplicate-names': [true, { severity: 'error' }], + 'keyframe-block-no-duplicate-selectors': [true, { severity: 'error' }], + 'no-duplicate-at-import-rules': [true, { severity: 'error' }], + 'no-duplicate-selectors': [true, { severity: 'error' }], + + // == Empty + 'block-no-empty': [true, { severity: 'error' }], + 'comment-no-empty': [true, { severity: 'error' }], + 'no-empty-source': [true, { severity: 'error' }], + + // == Invalid + 'color-no-invalid-hex': [true, { severity: 'error' }], + 'function-calc-no-unspaced-operator': [true, { severity: 'error' }], + 'keyframe-declaration-no-important': [true, { severity: 'error' }], + 'media-query-no-invalid': [true, { severity: 'error' }], + 'named-grid-areas-no-invalid': [true, { severity: 'error' }], + 'no-invalid-double-slash-comments': [true, { severity: 'error' }], + 'no-invalid-position-at-import-rule': [true, { severity: 'error' }], + 'string-no-newline': [true, { severity: 'error' }], + + // == Irregular + 'no-irregular-whitespace': [true, { severity: 'error' }], + + // == Missing + 'custom-property-no-missing-var-function': [true, { severity: 'error' }], + 'font-family-no-missing-generic-family-keyword': [true, { severity: 'error' }], + + // == Non-standard + 'function-linear-gradient-no-nonstandard-direction': [true, { severity: 'error' }], + + // == Overrides + 'declaration-block-no-shorthand-property-overrides': [true, { severity: 'error' }], + + // == Unmatchable + 'selector-anb-no-unmatchable': [true, { severity: 'error' }], + + // == Unknown + 'annotation-no-unknown': [true, { severity: 'error' }], + 'at-rule-no-unknown': [true, { severity: 'error' }], + 'function-no-unknown': [true, { severity: 'error' }], + 'media-feature-name-no-unknown': [true, { severity: 'error' }], + 'property-no-unknown': [true, { severity: 'error' }], + 'selector-pseudo-class-no-unknown': [true, { severity: 'error' }], + 'selector-type-no-unknown': [true, { severity: 'error' }], + 'unit-no-unknown': [true, { severity: 'error' }], + + // == Maintainability Rules + + // Prevent overly specific selectors + // Example: Good: `.class1 .class2`, Bad: `#id.class1 .class2` + "selector-max-specificity": ["0,2,0", { severity: "warning" }], + // Enforces a maximum specificity of 2 classes, no IDs, and no inline styles. + // Encourages maintainable selectors. + + // Disallow the use of ID selectors + // Example: Good: `.button`, Bad: `#button` + "selector-max-id": [0, { severity: "warning" }], + // Prevents the use of IDs in selectors, as they are too specific and hard to override. + + // Limit the number of class selectors in a rule + // Example: Good: `.btn.primary`, Bad: `.btn.primary.large.rounded` + "selector-max-class": [3, { severity: "off" }], + // Can help avoid overly complex class chains, but may be unnecessary if specificity is already managed. + + // Limit the number of pseudo-classes in a selector + // Example: Good: `.list-item:hover`, Bad: `.list-item:nth-child(2):hover:active` + "selector-max-pseudo-class": [3, { severity: "warning" }], + // Allows up to 3 pseudo-classes in a single selector to balance flexibility and simplicity. + + // Restrict the number of type selectors (e.g., `div`, `span`) + // Example: Good: `.header`, Bad: `div.header` + "selector-max-type": [1, { severity: "warning" }], + // Promotes the use of semantic classes over type selectors for better reusability and maintainability. + + // Optional: Additional rules for project-specific preferences + // Uncomment the following if relevant to your project: + /* + // Example: Limit the depth of combinators + // Good: `.parent > .child`, Bad: `.parent > .child > .grandchild` + "selector-max-combinators": [2, { severity: "warning" }], + + // Example: Restrict the number of universal selectors in a rule + // Good: `* { margin: 0; }`, Bad: `.wrapper * .content { padding: 0; }` + "selector-max-universal": [1, { severity: "warning" }], + */ + + // = Enforce conventions - set as warnings + + // == Allowed, disallowed & required + 'at-rule-no-vendor-prefix': [true, { severity: 'warning' }], + 'length-zero-no-unit': [true, { severity: 'warning' }], + 'media-feature-name-no-vendor-prefix': [true, { severity: 'warning' }], + 'property-no-vendor-prefix': [true, { severity: 'warning' }], + 'value-no-vendor-prefix': [true, { severity: 'warning' }], + + // == Case + 'function-name-case': ['lower', { severity: 'warning' }], + 'selector-type-case': ['lower', { severity: 'warning' }], + 'value-keyword-case': ['lower', { severity: 'warning' }], + + // == Empty lines + 'at-rule-empty-line-before': ['always', { severity: 'warning' }], + 'comment-empty-line-before': ['always', { severity: 'warning' }], + 'custom-property-empty-line-before': ['always', { severity: 'warning' }], + 'declaration-empty-line-before': ['always', { severity: 'warning' }], + 'rule-empty-line-before': ['always', { severity: 'warning' }], + + // == Max & min + 'declaration-block-single-line-max-declarations': [1, { severity: 'warning' }], + 'number-max-precision': [4, { severity: 'warning' }], + + // == Notation + 'alpha-value-notation': ['percentage', { severity: 'warning' }], + 'color-function-notation': ['modern', { severity: 'warning' }], + 'color-hex-length': ['short', { severity: 'warning' }], + 'hue-degree-notation': ['angle', { severity: 'warning' }], + 'import-notation': ['string', { severity: 'warning' }], + 'keyframe-selector-notation': ['percentage', { severity: 'warning' }], + 'lightness-notation': ['percentage', { severity: 'warning' }], + 'media-feature-range-notation': ['context', { severity: 'warning' }], + 'selector-not-notation': ['complex', { severity: 'warning' }], + 'selector-pseudo-element-colon-notation': ['double', { severity: 'warning' }], + + // == Pattern + 'custom-media-pattern': ['^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', { severity: 'warning' }], + 'custom-property-pattern': ['^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', { severity: 'warning' }], + 'keyframes-name-pattern': ['^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', { severity: 'warning' }], + 'selector-class-pattern': ['^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', { severity: 'warning' }], + 'selector-id-pattern': ['^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', { severity: 'warning' }], + + // == Quotes + 'font-family-name-quotes': ['always-where-recommended', { severity: 'warning' }], + 'function-url-quotes': ['always', { severity: 'warning' }], + 'selector-attribute-quotes': ['always', { severity: 'warning' }], + + // == Redundant + 'declaration-block-no-redundant-longhand-properties': [true, { severity: 'warning' }], + 'shorthand-property-no-redundant-values': [true, { severity: 'warning' }], + + // == Whitespace inside + 'comment-whitespace-inside': ['always', { severity: 'warning' }], + }, +}; + +export default stylelintConfig; diff --git a/packages/plugin-stylelint/package.json b/packages/plugin-stylelint/package.json new file mode 100644 index 000000000..b7427195b --- /dev/null +++ b/packages/plugin-stylelint/package.json @@ -0,0 +1,63 @@ +{ + "name": "@code-pushup/stylelint-plugin", + "version": "0.57.0", + "description": "Code PushUp plugin for tracking code stylelint ☂", + "license": "MIT", + "homepage": "https://github.com/code-pushup/cli/tree/main/packages/plugin-stylelint#readme", + "bugs": { + "url": "https://github.com/code-pushup/cli/issues?q=is%3Aissue%20state%3Aopen%20type%3ABug%20label%3A\"🧩%20stylelint-plugin\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/code-pushup/cli.git", + "directory": "packages/plugin-stylelint" + }, + "keywords": [ + "CLI", + "Code PushUp", + "plugin", + "automation", + "developer tools", + "conformance", + "code stylelint", + "unit tests", + "testing", + "KPI tracking", + "automated feedback", + "regression guard", + "actionable feedback", + "audit", + "score monitoring" + ], + "publishConfig": { + "access": "public" + }, + "type": "module", + "dependencies": { + "@code-pushup/models": "0.57.0", + "@code-pushup/utils": "0.57.0", + "ansis": "^3.3.0", + "stylelint": "^16.12.0", + "parse-lcov": "^1.0.4", + "zod": "^3.22.4" + }, + "peerDependencies": { + "@nx/devkit": ">=17.0.0", + "@nx/jest": ">=17.0.0", + "@nx/vite": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@nx/devkit": { + "optional": true + }, + "@nx/jest": { + "optional": true + }, + "@nx/vite": { + "optional": true + } + }, + "scripts": { + "postinstall": "node ./src/scripts/postinstall/bin.js" + } +} diff --git a/packages/plugin-stylelint/project.json b/packages/plugin-stylelint/project.json new file mode 100644 index 000000000..ccaa945ea --- /dev/null +++ b/packages/plugin-stylelint/project.json @@ -0,0 +1,45 @@ +{ + "name": "plugin-stylelint", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/plugin-stylelint/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/plugin-stylelint", + "main": "packages/plugin-stylelint/src/index.ts", + "tsConfig": "packages/plugin-stylelint/tsconfig.lib.json", + "additionalEntryPoints": ["packages/plugin-stylelint/src/bin.ts"], + "assets": ["packages/plugin-stylelint/*.md"] + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": [ + "packages/plugin-stylelint/**/*.ts", + "packages/plugin-stylelint/package.json" + ] + } + }, + "unit-test": { + "executor": "@nx/vite:test", + "options": { + "configFile": "packages/plugin-stylelint/vite.config.unit.ts" + } + }, + "integration-test": { + "executor": "@nx/vite:test", + "options": { + "configFile": "packages/plugin-stylelint/vite.config.integration.ts" + } + }, + "postinstall": { + "command": "tsx --tsconfig=packages/plugin-stylelint/tsconfig.lib.json packages/plugin-stylelint/src/scripts/postinstall/bin.ts" + } + }, + "tags": ["scope:plugin", "type:feature", "publishable"] +} diff --git a/packages/plugin-stylelint/src/index.ts b/packages/plugin-stylelint/src/index.ts new file mode 100644 index 000000000..5a863a51c --- /dev/null +++ b/packages/plugin-stylelint/src/index.ts @@ -0,0 +1,6 @@ +import { stylelintPlugin } from './lib/stylelint-plugin.js'; + +export { getAudits, getCategoryRefs } from './lib/utils.js'; + +export default stylelintPlugin; +export type { StyleLintPluginConfig } from './lib/config.js'; diff --git a/packages/plugin-stylelint/src/lib/config.ts b/packages/plugin-stylelint/src/lib/config.ts new file mode 100644 index 000000000..4596170d8 --- /dev/null +++ b/packages/plugin-stylelint/src/lib/config.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { toArray } from '@code-pushup/utils'; + +const patternsSchema = z.union([z.string(), z.array(z.string()).min(1)], { + description: + 'Lint target files. May contain file paths, directory paths or glob patterns', +}); + +const stylelintrcSchema = z.string({ + description: 'Path to StyleLint config file', +}); + +const stylelintTargetObjectSchema = z.object({ + stylelintrc: stylelintrcSchema.optional(), + patterns: patternsSchema, +}); +type StyleLintTargetObject = z.infer<typeof stylelintTargetObjectSchema>; + +export const stylelintTargetSchema = z + .union([patternsSchema, stylelintTargetObjectSchema]) + .transform( + (target): StyleLintTargetObject => + typeof target === 'string' || Array.isArray(target) + ? { patterns: target } + : target, + ); +export type StyleLintTarget = z.infer<typeof stylelintTargetSchema>; + +export const stylelintPluginConfigSchema = z + .union([stylelintTargetSchema, z.array(stylelintTargetSchema).min(1)]) + .transform(toArray); +export type StyleLintPluginConfig = z.input<typeof stylelintPluginConfigSchema>; + +export type StyleLintPluginRunnerConfig = { + targets: StyleLintTarget[]; + slugs: string[]; +}; diff --git a/packages/plugin-stylelint/src/lib/config.unit.test.ts b/packages/plugin-stylelint/src/lib/config.unit.test.ts new file mode 100644 index 000000000..034197a93 --- /dev/null +++ b/packages/plugin-stylelint/src/lib/config.unit.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from 'vitest'; +import { + type CoverageType, + type StylelintPluginConfig, + stylelintPluginConfigSchema, +} from './config.js'; + +describe('stylelintPluginConfigSchema', () => { + it('accepts a code stylelint configuration with all entities', () => { + expect(() => + stylelintPluginConfigSchema.parse({ + stylelintTypes: ['branch', 'function'], + reports: [ + { + resultsPath: 'stylelint/cli/lcov.info', + pathToProject: 'packages/cli', + }, + ], + stylelintToolCommand: { + command: 'npx nx run-many', + args: ['-t', 'test', '--stylelint'], + }, + perfectScoreThreshold: 0.85, + } satisfies StylelintPluginConfig), + ).not.toThrow(); + }); + + it('accepts a minimal code stylelint configuration', () => { + expect(() => + stylelintPluginConfigSchema.parse({ + reports: ['stylelint/cli/lcov.info'], + } satisfies StylelintPluginConfig), + ).not.toThrow(); + }); + + it('replaces undefined stylelint with all available types', () => { + const config = { + reports: ['stylelint/cli/lcov.info'], + } satisfies StylelintPluginConfig; + expect(() => stylelintPluginConfigSchema.parse(config)).not.toThrow(); + + const { stylelintTypes } = stylelintPluginConfigSchema.parse(config); + expect(stylelintTypes).toEqual<CoverageType[]>([ + 'function', + 'branch', + 'line', + ]); + }); + + it('throws for empty stylelint type array', () => { + expect(() => + stylelintPluginConfigSchema.parse({ + stylelintTypes: [], + reports: ['stylelint/cli/lcov.info'], + } satisfies StylelintPluginConfig), + ).toThrow('too_small'); + }); + + it('throws for no report', () => { + expect(() => + stylelintPluginConfigSchema.parse({ + stylelintTypes: ['branch'], + reports: [], + } satisfies StylelintPluginConfig), + ).toThrow('too_small'); + }); + + it('throws for unsupported report format', () => { + expect(() => + stylelintPluginConfigSchema.parse({ + stylelintTypes: ['line'], + reports: ['stylelint/cli/stylelint-final.json'], + } satisfies StylelintPluginConfig), + ).toThrow(/Invalid input: must include.+lcov/); + }); + + it('throws for missing command', () => { + expect(() => + stylelintPluginConfigSchema.parse({ + stylelintTypes: ['line'], + reports: ['stylelint/cli/lcov.info'], + stylelintToolCommand: { + args: ['npx', 'nx', 'run-many', '-t', 'test', '--stylelint'], + }, + }), + ).toThrow('invalid_type'); + }); + + it('throws for invalid score threshold', () => { + expect(() => + stylelintPluginConfigSchema.parse({ + stylelintTypes: ['line'], + reports: ['stylelint/cli/lcov.info'], + perfectScoreThreshold: 1.1, + } satisfies StylelintPluginConfig), + ).toThrow('too_big'); + }); +}); diff --git a/packages/plugin-stylelint/src/lib/constants.ts b/packages/plugin-stylelint/src/lib/constants.ts new file mode 100644 index 000000000..f4f0464fa --- /dev/null +++ b/packages/plugin-stylelint/src/lib/constants.ts @@ -0,0 +1,156 @@ +import type { CategoryConfig, Group } from '@code-pushup/models'; +import type { GroupSlug } from './types.js'; + +export const STYLELINT_PLUGIN_SLUG = 'stylelint' as const; +export const DEFAULT_STYLELINTRC = '.stylelintrc.json' as const; + +const AUDIT_TO_GROUP_MAP = { + // Avoid errors + + descending: ['no-descending-specificity'], + + duplicate: [ + 'declaration-block-no-duplicate-custom-properties', + 'declaration-block-no-duplicate-properties', + 'font-family-no-duplicate-names', + 'keyframe-block-no-duplicate-selectors', + 'no-duplicate-at-import-rules', + 'no-duplicate-selectors', + ], + + empty: ['block-no-empty', 'comment-no-empty', 'no-empty-source'], + + invalid: [ + 'color-no-invalid-hex', + 'function-calc-no-unspaced-operator', + 'keyframe-declaration-no-important', + 'media-query-no-invalid', + 'named-grid-areas-no-invalid', + 'no-invalid-double-slash-comments', + 'no-invalid-position-at-import-rule', + 'string-no-newline', + ], + + irregular: ['no-irregular-whitespace'], + + missing: [ + 'custom-property-no-missing-var-function', + 'font-family-no-missing-generic-family-keyword', + ], + + 'non-standard': ['function-linear-gradient-no-nonstandard-direction'], + + overrides: ['declaration-block-no-shorthand-property-overrides'], + + unmatchable: ['selector-anb-no-unmatchable'], + + unknown: [ + 'annotation-no-unknown', + 'at-rule-no-unknown', + 'function-no-unknown', + 'media-feature-name-no-unknown', + 'property-no-unknown', + 'selector-pseudo-class-no-unknown', + 'selector-type-no-unknown', + 'unit-no-unknown', + ], + + // Enforce conventions + + 'allowed-disallowed-required': [ + 'at-rule-no-vendor-prefix', + 'length-zero-no-unit', + 'media-feature-name-no-vendor-prefix', + 'property-no-vendor-prefix', + 'value-no-vendor-prefix', + ], + + case: ['function-name-case', 'selector-type-case', 'value-keyword-case'], + + 'empty-lines': [ + 'at-rule-empty-line-before', + 'comment-empty-line-before', + 'custom-property-empty-line-before', + 'declaration-empty-line-before', + 'rule-empty-line-before', + ], + + 'max-min': [ + 'declaration-block-single-line-max-declarations', + 'number-max-precision', + ], + + notation: [ + 'alpha-value-notation', + 'color-function-notation', + 'color-hex-length', + 'hue-degree-notation', + 'import-notation', + 'keyframe-selector-notation', + 'lightness-notation', + 'media-feature-range-notation', + 'selector-not-notation', + 'selector-pseudo-element-colon-notation', + ], + + pattern: [ + 'custom-media-pattern', + 'custom-property-pattern', + 'keyframes-name-pattern', + 'selector-class-pattern', + 'selector-id-pattern', + ], + + quotes: [ + 'font-family-name-quotes', + 'function-url-quotes', + 'selector-attribute-quotes', + ], + + redundant: [ + 'declaration-block-no-redundant-longhand-properties', + 'shorthand-property-no-redundant-values', + ], + + 'whitespace-inside': ['comment-whitespace-inside'], +}; + +export const GROUPS = [ + { + slug: 'problems' as const, + title: 'Problems', + refs: [], + }, + { + slug: 'suggestions' as const, + title: 'Suggestions', + refs: [], + }, +] satisfies Group[]; + +export const CATEGORY_MAP: Record<string, CategoryConfig> = { + 'code-style': { + slug: 'code-style' as const, + title: 'Code Style', + refs: [ + { + slug: 'suggestions' as GroupSlug, + weight: 1, + type: 'group', + plugin: 'stylelint', + }, + ], + }, + 'bug-prevention': { + slug: 'bug-prevention' as const, + title: 'Bug Prevention', + refs: [ + { + slug: 'problems' as GroupSlug, + weight: 1, + type: 'group', + plugin: 'stylelint', + }, + ], + }, +}; diff --git a/packages/plugin-stylelint/src/lib/runner/__snapshots__/normalize-config.integration.test.ts.snap b/packages/plugin-stylelint/src/lib/runner/__snapshots__/normalize-config.integration.test.ts.snap new file mode 100644 index 000000000..f59f75cda --- /dev/null +++ b/packages/plugin-stylelint/src/lib/runner/__snapshots__/normalize-config.integration.test.ts.snap @@ -0,0 +1,907 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`getNormalizedConfig > should get config from specified JS file 1`] = ` +{ + "config": { + "rules": { + "alpha-value-notation": [ + "percentage", + { + "severity": "warning", + }, + ], + "annotation-no-unknown": [ + true, + { + "severity": "error", + }, + ], + "at-rule-empty-line-before": [ + "always", + { + "severity": "warning", + }, + ], + "at-rule-no-unknown": [ + true, + { + "severity": "error", + }, + ], + "at-rule-no-vendor-prefix": [ + true, + { + "severity": "warning", + }, + ], + "block-no-empty": [ + true, + { + "severity": "error", + }, + ], + "color-function-notation": [ + "modern", + { + "severity": "warning", + }, + ], + "color-hex-length": [ + "short", + { + "severity": "warning", + }, + ], + "color-no-invalid-hex": [ + true, + { + "severity": "error", + }, + ], + "comment-empty-line-before": [ + "always", + { + "severity": "warning", + }, + ], + "comment-no-empty": [ + true, + { + "severity": "error", + }, + ], + "comment-whitespace-inside": [ + "always", + { + "severity": "warning", + }, + ], + "custom-media-pattern": [ + "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$", + { + "severity": "warning", + }, + ], + "custom-property-empty-line-before": [ + "always", + { + "severity": "warning", + }, + ], + "custom-property-no-missing-var-function": [ + true, + { + "severity": "error", + }, + ], + "custom-property-pattern": [ + "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$", + { + "severity": "warning", + }, + ], + "declaration-block-no-duplicate-custom-properties": [ + true, + { + "severity": "error", + }, + ], + "declaration-block-no-duplicate-properties": [ + true, + { + "ignore": [ + "consecutive-duplicates-with-different-syntaxes", + ], + "severity": "error", + }, + ], + "declaration-block-no-redundant-longhand-properties": [ + true, + { + "severity": "warning", + }, + ], + "declaration-block-no-shorthand-property-overrides": [ + true, + { + "severity": "error", + }, + ], + "declaration-block-single-line-max-declarations": [ + 1, + { + "severity": "warning", + }, + ], + "declaration-empty-line-before": [ + "always", + { + "severity": "warning", + }, + ], + "font-family-name-quotes": [ + "always-where-recommended", + { + "severity": "warning", + }, + ], + "font-family-no-duplicate-names": [ + true, + { + "severity": "error", + }, + ], + "font-family-no-missing-generic-family-keyword": [ + true, + { + "severity": "error", + }, + ], + "function-calc-no-unspaced-operator": [ + true, + { + "severity": "error", + }, + ], + "function-linear-gradient-no-nonstandard-direction": [ + true, + { + "severity": "error", + }, + ], + "function-name-case": [ + "lower", + { + "severity": "warning", + }, + ], + "function-no-unknown": [ + true, + { + "severity": "error", + }, + ], + "function-url-quotes": [ + "always", + { + "severity": "warning", + }, + ], + "hue-degree-notation": [ + "angle", + { + "severity": "warning", + }, + ], + "import-notation": [ + "string", + { + "severity": "warning", + }, + ], + "keyframe-block-no-duplicate-selectors": [ + true, + { + "severity": "error", + }, + ], + "keyframe-declaration-no-important": [ + true, + { + "severity": "error", + }, + ], + "keyframe-selector-notation": [ + "percentage", + { + "severity": "warning", + }, + ], + "keyframes-name-pattern": [ + "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$", + { + "severity": "warning", + }, + ], + "length-zero-no-unit": [ + true, + { + "severity": "warning", + }, + ], + "lightness-notation": [ + "percentage", + { + "severity": "warning", + }, + ], + "media-feature-name-no-unknown": [ + true, + { + "severity": "error", + }, + ], + "media-feature-name-no-vendor-prefix": [ + true, + { + "severity": "warning", + }, + ], + "media-feature-range-notation": [ + "context", + { + "severity": "warning", + }, + ], + "media-query-no-invalid": [ + true, + { + "severity": "error", + }, + ], + "named-grid-areas-no-invalid": [ + true, + { + "severity": "error", + }, + ], + "no-descending-specificity": [ + true, + { + "severity": "error", + }, + ], + "no-duplicate-at-import-rules": [ + true, + { + "severity": "error", + }, + ], + "no-duplicate-selectors": [ + true, + { + "severity": "error", + }, + ], + "no-empty-source": [ + true, + { + "severity": "error", + }, + ], + "no-invalid-double-slash-comments": [ + true, + { + "severity": "error", + }, + ], + "no-invalid-position-at-import-rule": [ + true, + { + "severity": "error", + }, + ], + "no-irregular-whitespace": [ + true, + { + "severity": "error", + }, + ], + "number-max-precision": [ + 4, + { + "severity": "warning", + }, + ], + "property-no-unknown": [ + true, + { + "severity": "error", + }, + ], + "property-no-vendor-prefix": [ + true, + { + "severity": "warning", + }, + ], + "rule-empty-line-before": [ + "always", + { + "severity": "warning", + }, + ], + "selector-anb-no-unmatchable": [ + true, + { + "severity": "error", + }, + ], + "selector-attribute-quotes": [ + "always", + { + "severity": "warning", + }, + ], + "selector-class-pattern": [ + "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$", + { + "severity": "warning", + }, + ], + "selector-id-pattern": [ + "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$", + { + "severity": "warning", + }, + ], + "selector-max-class": [ + 3, + { + "severity": "off", + }, + ], + "selector-max-id": [ + 0, + { + "severity": "warning", + }, + ], + "selector-max-pseudo-class": [ + 3, + { + "severity": "warning", + }, + ], + "selector-max-specificity": [ + "0,2,0", + { + "severity": "warning", + }, + ], + "selector-max-type": [ + 1, + { + "severity": "warning", + }, + ], + "selector-no-vendor-prefix": [ + true, + ], + "selector-not-notation": [ + "complex", + { + "severity": "warning", + }, + ], + "selector-pseudo-class-no-unknown": [ + true, + { + "severity": "error", + }, + ], + "selector-pseudo-element-colon-notation": [ + "double", + { + "severity": "warning", + }, + ], + "selector-pseudo-element-no-unknown": [ + true, + ], + "selector-type-case": [ + "lower", + { + "severity": "warning", + }, + ], + "selector-type-no-unknown": [ + true, + { + "severity": "error", + }, + ], + "shorthand-property-no-redundant-values": [ + true, + { + "severity": "warning", + }, + ], + "string-no-newline": [ + true, + { + "severity": "error", + }, + ], + "unit-no-unknown": [ + true, + { + "severity": "error", + }, + ], + "value-keyword-case": [ + "lower", + { + "severity": "warning", + }, + ], + "value-no-vendor-prefix": [ + true, + { + "severity": "warning", + }, + ], + }, + }, + "filepath": "/Users/michael_hladky/WebstormProjects/quality-metrics-cli/packages/plugin-stylelint/mocks/fixtures/stylelint-config/index.js", +} +`; + +exports[`getNormalizedConfig > should get config from specified JSON file 1`] = ` +{ + "config": { + "rules": { + "alpha-value-notation": [ + "percentage", + { + "severity": "warning", + }, + ], + "annotation-no-unknown": [ + true, + { + "severity": "error", + }, + ], + "at-rule-empty-line-before": [ + "always", + { + "severity": "warning", + }, + ], + "at-rule-no-unknown": [ + true, + { + "severity": "error", + }, + ], + "at-rule-no-vendor-prefix": [ + true, + { + "severity": "warning", + }, + ], + "block-no-empty": null, + "color-function-notation": [ + "modern", + { + "severity": "warning", + }, + ], + "color-hex-length": [ + "short", + { + "severity": "warning", + }, + ], + "color-no-invalid-hex": [ + true, + ], + "comment-empty-line-before": [ + "always", + { + "severity": "warning", + }, + ], + "comment-no-empty": [ + true, + { + "severity": "error", + }, + ], + "comment-whitespace-inside": [ + "always", + { + "severity": "warning", + }, + ], + "custom-media-pattern": [ + "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$", + { + "severity": "warning", + }, + ], + "custom-property-empty-line-before": [ + "always", + { + "severity": "warning", + }, + ], + "custom-property-no-missing-var-function": [ + true, + { + "severity": "error", + }, + ], + "custom-property-pattern": [ + "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$", + { + "severity": "warning", + }, + ], + "declaration-block-no-duplicate-custom-properties": [ + true, + { + "severity": "error", + }, + ], + "declaration-block-no-duplicate-properties": [ + true, + { + "ignore": [ + "consecutive-duplicates-with-different-syntaxes", + ], + "severity": "error", + }, + ], + "declaration-block-no-redundant-longhand-properties": [ + true, + { + "severity": "warning", + }, + ], + "declaration-block-no-shorthand-property-overrides": [ + true, + { + "severity": "error", + }, + ], + "declaration-block-single-line-max-declarations": [ + 1, + { + "severity": "warning", + }, + ], + "declaration-empty-line-before": [ + "always", + { + "severity": "warning", + }, + ], + "font-family-name-quotes": [ + "always-where-recommended", + { + "severity": "warning", + }, + ], + "font-family-no-duplicate-names": [ + true, + { + "severity": "error", + }, + ], + "font-family-no-missing-generic-family-keyword": [ + true, + { + "severity": "error", + }, + ], + "function-calc-no-unspaced-operator": [ + true, + { + "severity": "error", + }, + ], + "function-linear-gradient-no-nonstandard-direction": [ + true, + { + "severity": "error", + }, + ], + "function-name-case": [ + "lower", + { + "severity": "warning", + }, + ], + "function-no-unknown": [ + true, + { + "severity": "error", + }, + ], + "function-url-quotes": [ + "always", + { + "severity": "warning", + }, + ], + "hue-degree-notation": [ + "angle", + { + "severity": "warning", + }, + ], + "import-notation": [ + "string", + { + "severity": "warning", + }, + ], + "keyframe-block-no-duplicate-selectors": [ + true, + { + "severity": "error", + }, + ], + "keyframe-declaration-no-important": [ + true, + { + "severity": "error", + }, + ], + "keyframe-selector-notation": [ + "percentage", + { + "severity": "warning", + }, + ], + "keyframes-name-pattern": [ + "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$", + { + "severity": "warning", + }, + ], + "length-zero-no-unit": [ + true, + { + "severity": "warning", + }, + ], + "lightness-notation": [ + "percentage", + { + "severity": "warning", + }, + ], + "media-feature-name-no-unknown": [ + true, + { + "severity": "error", + }, + ], + "media-feature-name-no-vendor-prefix": [ + true, + { + "severity": "warning", + }, + ], + "media-feature-range-notation": [ + "context", + { + "severity": "warning", + }, + ], + "media-query-no-invalid": [ + true, + { + "severity": "error", + }, + ], + "named-grid-areas-no-invalid": [ + true, + { + "severity": "error", + }, + ], + "no-descending-specificity": [ + true, + { + "severity": "error", + }, + ], + "no-duplicate-at-import-rules": [ + true, + { + "severity": "error", + }, + ], + "no-duplicate-selectors": [ + true, + { + "severity": "error", + }, + ], + "no-empty-source": [ + true, + { + "severity": "error", + }, + ], + "no-invalid-double-slash-comments": [ + true, + { + "severity": "error", + }, + ], + "no-invalid-position-at-import-rule": [ + true, + { + "severity": "error", + }, + ], + "no-irregular-whitespace": [ + true, + { + "severity": "error", + }, + ], + "number-max-precision": [ + 4, + { + "severity": "warning", + }, + ], + "property-no-unknown": [ + true, + { + "severity": "error", + }, + ], + "property-no-vendor-prefix": [ + true, + { + "severity": "warning", + }, + ], + "rule-empty-line-before": [ + "always", + { + "severity": "warning", + }, + ], + "selector-anb-no-unmatchable": [ + true, + { + "severity": "error", + }, + ], + "selector-attribute-quotes": [ + "always", + { + "severity": "warning", + }, + ], + "selector-class-pattern": [ + "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$", + { + "severity": "warning", + }, + ], + "selector-id-pattern": [ + "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$", + { + "severity": "warning", + }, + ], + "selector-max-class": [ + 3, + { + "severity": "off", + }, + ], + "selector-max-id": [ + 0, + { + "severity": "warning", + }, + ], + "selector-max-pseudo-class": [ + 3, + { + "severity": "warning", + }, + ], + "selector-max-specificity": [ + "0,2,0", + { + "severity": "warning", + }, + ], + "selector-max-type": [ + 1, + { + "severity": "warning", + }, + ], + "selector-no-vendor-prefix": [ + true, + ], + "selector-not-notation": [ + "complex", + { + "severity": "warning", + }, + ], + "selector-pseudo-class-no-unknown": [ + true, + { + "severity": "error", + }, + ], + "selector-pseudo-element-colon-notation": [ + "double", + { + "severity": "warning", + }, + ], + "selector-pseudo-element-no-unknown": [ + true, + ], + "selector-type-case": [ + "lower", + { + "severity": "warning", + }, + ], + "selector-type-no-unknown": [ + true, + { + "severity": "error", + }, + ], + "shorthand-property-no-redundant-values": [ + true, + { + "severity": "warning", + }, + ], + "string-no-newline": [ + true, + { + "severity": "error", + }, + ], + "unit-no-unknown": [ + true, + { + "severity": "error", + }, + ], + "value-keyword-case": [ + "lower", + { + "severity": "warning", + }, + ], + "value-no-vendor-prefix": [ + true, + { + "severity": "warning", + }, + ], + }, + }, + "filepath": "/Users/michael_hladky/WebstormProjects/quality-metrics-cli/packages/plugin-stylelint/mocks/fixtures/stylelint-config/.stylelintrc.json", +} +`; diff --git a/packages/plugin-stylelint/src/lib/runner/model.ts b/packages/plugin-stylelint/src/lib/runner/model.ts new file mode 100644 index 000000000..6aede63b7 --- /dev/null +++ b/packages/plugin-stylelint/src/lib/runner/model.ts @@ -0,0 +1,16 @@ +import type { ConfigRuleSettings, Primary, Severity } from 'stylelint'; + +// Typing resource https://stylelint.io/user-guide/configure/ +/** Config rule setting of Stylelint excluding null and undefined values */ +export type ActiveConfigRuleSetting = Exclude< + ConfigRuleSettings<Primary, Record<string, unknown>>, + null | undefined +>; + +/** Output of the `getNormalizedConfigForFile` function. Config file of Stylelint */ +export type NormalizedStyleLintConfig = { + config: { + rules: Record<string, ConfigRuleSettings<Primary, Record<string, any>>>; + defaultSeverity?: Severity; + }; +}; diff --git a/packages/plugin-stylelint/src/lib/runner/normalize-config.integration.test.ts b/packages/plugin-stylelint/src/lib/runner/normalize-config.integration.test.ts new file mode 100644 index 000000000..d0940eb49 --- /dev/null +++ b/packages/plugin-stylelint/src/lib/runner/normalize-config.integration.test.ts @@ -0,0 +1,52 @@ +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import type { NormalizedStyleLintConfig } from './model.js'; +import { getNormalizedConfig } from './normalize-config.js'; + +const configPath = path.join( + process.cwd(), + 'packages/plugin-stylelint/mocks/fixtures/stylelint-config/.stylelintrc.json', +); + +const baseConfigPath = path.join( + process.cwd(), + 'packages/plugin-stylelint/mocks/fixtures/stylelint-config/index.js', +); + +describe('getNormalizedConfig', () => { + let extendedConfig: NormalizedStyleLintConfig; + let baseConfig: NormalizedStyleLintConfig; + + beforeAll(async () => { + extendedConfig = await getNormalizedConfig({ stylelintrc: configPath }); + baseConfig = await getNormalizedConfig({ stylelintrc: baseConfigPath }); + }); + + it('should get config from specified JSON file', async () => { + expect(extendedConfig).toMatchSnapshot(); + }); + + it('should get config from specified JS file', async () => { + expect(baseConfig).toMatchSnapshot(); + }); + + it('should override values specified in config file from the extended base config', async () => { + expect(extendedConfig.config.rules['block-no-empty']).toBeNull(); + expect(baseConfig.config.rules['block-no-empty']).toStrictEqual([ + true, + { severity: 'error' }, + ]); + + expect(extendedConfig.config.rules['color-no-invalid-hex']).toBeTruthy(); + expect(baseConfig.config.rules['color-no-invalid-hex']).toStrictEqual([ + true, + { severity: 'error' }, + ]); + }); + + it('should have the same amount of rules as the base config', async () => { + expect(Object.keys(extendedConfig.config.rules)).toHaveLength( + Object.keys(baseConfig.config.rules).length, + ); + }); +}); diff --git a/packages/plugin-stylelint/src/lib/runner/normalize-config.ts b/packages/plugin-stylelint/src/lib/runner/normalize-config.ts new file mode 100644 index 000000000..106866d1d --- /dev/null +++ b/packages/plugin-stylelint/src/lib/runner/normalize-config.ts @@ -0,0 +1,33 @@ +import path from 'node:path'; +import * as process from 'node:process'; +// @ts-expect-error missing types for stylelint package after postinstall patch +import stylelint, { getConfigForFile } from 'stylelint'; +import type { StyleLintTarget } from '../config.js'; +import type { NormalizedStyleLintConfig } from './model.js'; + +const NORMALIZED_CONFIG_CACHE = new Map<string, NormalizedStyleLintConfig>(); +/** + * Function that consumes the StyleLint configuration processor and returns a normalized config + * @param stylelintrc - The path to the StyleLint configuration file + * @param cwd - The current working directory + * @returns A normalized StyleLint configuration + */ +export function getNormalizedConfig({ + stylelintrc, + cwd, +}: Required<Pick<StyleLintTarget, 'stylelintrc'>> & { + cwd?: string; +}): Promise<NormalizedStyleLintConfig> { + const parsedStylelintrc = + stylelintrc ?? path.join(cwd ?? process.cwd(), '.stylelintrc.json'); // @TODO use a const + if (NORMALIZED_CONFIG_CACHE.get(parsedStylelintrc) === undefined) { + const _linter = stylelint._createLinter({ configFile: stylelintrc }); + NORMALIZED_CONFIG_CACHE.set( + parsedStylelintrc, + getConfigForFile(_linter, parsedStylelintrc), + ); + } + return Promise.resolve( + NORMALIZED_CONFIG_CACHE.get(parsedStylelintrc) as NormalizedStyleLintConfig, + ); +} diff --git a/packages/plugin-stylelint/src/lib/runner/runner.integration.test.ts b/packages/plugin-stylelint/src/lib/runner/runner.integration.test.ts new file mode 100644 index 000000000..4d788af4a --- /dev/null +++ b/packages/plugin-stylelint/src/lib/runner/runner.integration.test.ts @@ -0,0 +1,13 @@ +import type { Audit, RunnerFunction } from '@code-pushup/models'; +import { type StyleLintOptions, lintStyles } from './stylelint-runner.js'; +import { mapStylelintResultsToAudits } from './utils.js'; + +export function createRunnerFunction( + opt: StyleLintOptions, + expectedAudits: Audit[], +): RunnerFunction { + return async () => { + const report = await lintStyles(opt); + return mapStylelintResultsToAudits(report, expectedAudits); + }; +} diff --git a/packages/plugin-stylelint/src/lib/runner/runner.ts b/packages/plugin-stylelint/src/lib/runner/runner.ts new file mode 100644 index 000000000..4d788af4a --- /dev/null +++ b/packages/plugin-stylelint/src/lib/runner/runner.ts @@ -0,0 +1,13 @@ +import type { Audit, RunnerFunction } from '@code-pushup/models'; +import { type StyleLintOptions, lintStyles } from './stylelint-runner.js'; +import { mapStylelintResultsToAudits } from './utils.js'; + +export function createRunnerFunction( + opt: StyleLintOptions, + expectedAudits: Audit[], +): RunnerFunction { + return async () => { + const report = await lintStyles(opt); + return mapStylelintResultsToAudits(report, expectedAudits); + }; +} diff --git a/packages/plugin-stylelint/src/lib/runner/stylelint-runner.integration.test.ts b/packages/plugin-stylelint/src/lib/runner/stylelint-runner.integration.test.ts new file mode 100644 index 000000000..1a0531004 --- /dev/null +++ b/packages/plugin-stylelint/src/lib/runner/stylelint-runner.integration.test.ts @@ -0,0 +1,140 @@ +import path from 'node:path'; +import type { LintResult } from 'stylelint'; +import stylelint from 'stylelint'; +import { type MockInstance, beforeEach, describe, expect } from 'vitest'; +import { lintStyles } from './stylelint-runner.js'; + +const fixturesDir = path.join( + 'packages', + 'plugin-stylelint', + 'mocks', + 'fixtures', +); +const colorNoInvalidHexSlug = 'color-no-invalid-hex'; +const colorNoInvalidHexWarning = { + column: 10, + endColumn: 13, + endLine: 3, + line: 3, + rule: colorNoInvalidHexSlug, + severity: 'error', + text: `Unexpected invalid hex color "#34" (${colorNoInvalidHexSlug})`, + url: undefined, +}; +const fixturesCssRoot = path.join(fixturesDir, 'css'); +let lintSpy: MockInstance< + [stylelint.LinterOptions], // Arguments of stylelint.lint + Promise<stylelint.LinterResult> // Return type of stylelint.lint +>; + +describe('lintStyles', () => { + beforeEach(() => { + lintSpy = vi.spyOn(stylelint, 'lint'); + }); + + it('should use stylelint.lint with format set to "json" statically to generate lint results', async () => { + const options = { + configFile: path.join( + fixturesCssRoot, + '.stylelintrc.color-no-invalid-hex.json', + ), + files: path.join(fixturesCssRoot, `${colorNoInvalidHexSlug}.css`), + }; + + await expect(lintStyles(options)).resolves.not.toThrow(); + + expect(lintSpy).toHaveBeenCalledTimes(1); + expect(lintSpy).toHaveBeenCalledWith({ + ...options, + formatter: 'json', // added inside lintStyles + }); + }); + + it('should return a LintResult object', async () => { + const options = { + configFile: path.join( + fixturesCssRoot, + '.stylelintrc.color-no-invalid-hex.json', + ), + files: path.join(fixturesCssRoot, `${colorNoInvalidHexSlug}.css`), + }; + + await expect(lintStyles(options)).resolves.toStrictEqual([ + { + errored: true, + ignored: undefined, + _postcssResult: expect.any(Object), + source: expect.pathToEndWith('css/color-no-invalid-hex.css'), + deprecations: [], + invalidOptionWarnings: [], + parseErrors: [], + warnings: [colorNoInvalidHexWarning], + }, + ]); + }); + + it('should throw an error if stylelint.lint fails', async () => { + await expect(lintStyles({})).rejects.toThrow( + 'Error while linting: Error: You must pass stylelint a `files` glob or a `code` string, though not both', + ); + }); +}); + +describe.each([['css'], ['scss'], ['less']])( + 'lintStyles configured for %s', + format => { + const formatRoot = path.join(fixturesDir, format); + beforeEach(() => { + lintSpy = vi.spyOn(stylelint, 'lint'); + }); + + it('should lint files correctly', async () => { + const lintResult = await lintStyles({ + configFile: path.join( + formatRoot, + `.stylelintrc.${colorNoInvalidHexSlug}.json`, + ), + files: path.join(formatRoot, `${colorNoInvalidHexSlug}.${format}`), + }); + + expect(lintResult).toHaveLength(1); + const { warnings, source } = lintResult.at(0) as LintResult; + expect(source).pathToEndWith(`${colorNoInvalidHexSlug}.${format}`); + expect(warnings).toStrictEqual([colorNoInvalidHexWarning]); + }); + + it('should lint files correctly with extended config', async () => { + const lintResult = await lintStyles({ + configFile: path.join(formatRoot, '.stylelintrc.extends.json'), + files: path.join(formatRoot, `${colorNoInvalidHexSlug}.${format}`), + }); + + expect(lintResult).toHaveLength(1); + const { warnings, source } = lintResult.at(0) as LintResult; + expect(source).pathToEndWith(`${colorNoInvalidHexSlug}.${format}`); + expect(warnings).toStrictEqual([colorNoInvalidHexWarning]); + }); + }, +); + +describe.each([['js'], ['cjs'], ['mjs'], ['yml'], ['json']])( + 'lintStyles configured with a configFile of format %s', + configFileFormat => { + const formatRoot = path.join(fixturesDir, 'config-format'); + beforeEach(() => { + lintSpy = vi.spyOn(stylelint, 'lint'); + }); + + it('should lint files correctly', async () => { + const lintResult = await lintStyles({ + configFile: path.join(formatRoot, `.stylelintrc.${configFileFormat}`), + files: `${formatRoot}/*.css`, + }); + + expect(lintResult).toHaveLength(1); + const { warnings, source } = lintResult.at(0) as LintResult; + expect(source).pathToEndWith(`${colorNoInvalidHexSlug}.css`); + expect(warnings).toStrictEqual([colorNoInvalidHexWarning]); + }); + }, +); diff --git a/packages/plugin-stylelint/src/lib/runner/stylelint-runner.ts b/packages/plugin-stylelint/src/lib/runner/stylelint-runner.ts new file mode 100644 index 000000000..78e86e47a --- /dev/null +++ b/packages/plugin-stylelint/src/lib/runner/stylelint-runner.ts @@ -0,0 +1,24 @@ +import stylelint, { type LinterOptions } from 'stylelint'; + +export type StyleLintOptions = Omit<LinterOptions, 'formatter'>; + +/** + * Function that runs Stylelint programmatically with a certain configuration to run it and get + * the results that Stylelint would get + * @param config Configuration to run Stylelint + * @param options Options + * @returns The StyleLint process result + */ +export async function lintStyles({ config, ...options }: StyleLintOptions) { + try { + // eslint-disable-next-line functional/immutable-data,@typescript-eslint/no-empty-function + globalThis.console.assert = globalThis.console.assert || (() => {}); + const { results } = await stylelint.lint({ + ...options, + formatter: 'json', + }); + return results; + } catch (error) { + throw new Error(`Error while linting: ${error}`); + } +} diff --git a/packages/plugin-stylelint/src/lib/runner/utils.ts b/packages/plugin-stylelint/src/lib/runner/utils.ts new file mode 100644 index 000000000..b1b965b9e --- /dev/null +++ b/packages/plugin-stylelint/src/lib/runner/utils.ts @@ -0,0 +1,270 @@ +import type { LintResult, Secondary, Severity, Warning } from 'stylelint'; +import type { + Audit, + AuditOutputs, + AuditReport, + Issue, +} from '@code-pushup/models'; +import type { ActiveConfigRuleSetting } from './model.js'; + +export function mapStylelintResultsToAudits( + results: LintResult[], + expectedAudits: Audit[], +): AuditOutputs { + // Create an immutable Map of audits from the expected audits + const initialAuditMap = expectedAudits.reduce((map, audit) => { + map.set(audit.slug, { + ...audit, + score: 1, // Default score + value: 0, // Default value + details: { issues: [] }, + }); + return map; + }, new Map<string, AuditReport>()); + + // Process results and produce a new immutable audit map + const finalAuditMap = results.reduce((map, result) => { + const { source, warnings } = result; + + if (!source) { + throw new Error('Stylelint source can`t be undefined'); + } + + return warnings.reduce((innerMap, warning) => { + const { rule, line, text } = warning; + + const existingAudit = innerMap.get(rule); + if (!existingAudit) return innerMap; + + // Create a new audit object with updated details + const updatedAudit: AuditReport = { + ...existingAudit, + score: 0, // Indicate at least one issue exists + value: existingAudit.value + 1, + details: { + issues: [ + ...(existingAudit?.details?.issues ?? []), + { + severity: getSeverityFromWarning(warning), + message: text, + source: { + file: source, + position: { startLine: line }, + }, + }, + ], + }, + }; + + // Return a new map with the updated audit + return new Map(innerMap).set(rule, updatedAudit); + }, map); + }, initialAuditMap); + + // Return the updated audits as an array + return Array.from(finalAuditMap.values()); +} + +/** + * Processes warnings and updates the audit map using reduce. + * + * @param auditMap - Current map of audits + * @param warnings - Array of Stylelint warnings + * @param source - The source file associated with the warnings + */ +function processWarnings( + auditMap: Map<string, AuditReport>, + warnings: LintResult['warnings'], + source: string, +): Map<string, AuditReport> { + return warnings.reduce((innerMap, warning) => { + const { rule, line, text } = warning; + + const existingAudit = innerMap.get(rule); + if (!existingAudit) { + return innerMap; + } + + const updatedAudit: AuditReport = { + ...existingAudit, + score: 0, // At least one issue exists + value: existingAudit.value + 1, + details: { + issues: [ + ...(existingAudit.details?.issues ?? []), + { + severity: getSeverityFromWarning(warning), + message: text, + source: { + file: source, + position: { startLine: line }, + }, + }, + ], + }, + }; + + return innerMap.set(rule, updatedAudit); + }, auditMap); +} + +export function getSeverityFromWarning(warning: Warning): 'error' | 'warning' { + const { severity } = warning; + + if (severity === 'error' || severity === 'warning') { + return severity; + } + throw new Error(`Unknown severity: ${severity}`); +} + +/** + * Function that returns the severity from a ruleConfig. + * If the ruleConfig is not an array, the default severity of the config file must be returned, since the custom severity can be only specified in an array. + * If the ruleConfig is an array, a custom severity might have been set, in that case, it must be returned + * @param ruleConfig - The Stylelint rule config value + * @param defaultSeverity - The default severity of the config file. By default, it's 'error' + * @returns The severity (EX: 'error' | 'warning') + */ +export function getSeverityFromRuleConfig( + ruleConfig: ActiveConfigRuleSetting, + defaultSeverity: Severity = 'error', +): Severity { + //If it's not an array, the default severity of the config file must be returned, since the custom severity can be only specified in an array. + if (!Array.isArray(ruleConfig)) { + return defaultSeverity; + } + + // If it's an array, a custom severity might have been set, in that case, it must be returned + + const secondary: Secondary = ruleConfig.at(1); + + if (secondary == null) { + return defaultSeverity; + } + + if (!secondary['severity']) { + return defaultSeverity; + } + + if (typeof secondary['severity'] === 'function') { + console.warn('Function severity is not supported'); + return defaultSeverity; + } + + return secondary['severity']; +} + +export function parseErrorsToIssues( + parseErrors: LintResult['parseErrors'], + filePath: string, +): Issue[] { + return parseErrors.map(error => ({ + severity: 'error', + message: error.text, + source: { + file: filePath, + position: { + startLine: error.line, + startColumn: error.column, + }, + }, + })); +} + +export function getLineForConfigIssue( + fileContent: string, + warningText: string, +): number | undefined { + // Extract rule name and invalid value from warning text + const ruleMatch = /rule "([^"]+)"/.exec(warningText); + const valueMatch = /value "(.*?)"/.exec(warningText); + + const ruleName = ruleMatch ? ruleMatch[1] : undefined; + const invalidValue = valueMatch ? valueMatch[1] : undefined; + + if (!ruleName || !invalidValue) { + return undefined; // If either is missing, return undefined + } + + // Create a regex to match the line in the configuration + const regex = new RegExp(`"${ruleName}"\\s*:\\s*.*?${invalidValue}`, 'g'); + const lines = fileContent.split('\n'); + + // Find the matching line + for (let i = 0; i < lines.length; i++) { + if (regex.test(lines[i])) { + return i + 1; // Line numbers are 1-based + } + } + return undefined; // Return undefined if no match is found +} + +export function invalidOptionWarningsToIssues( + invalidOptionWarnings: LintResult['invalidOptionWarnings'], + filePath: string, + fileContent: string, +): Issue[] { + return invalidOptionWarnings.map(warning => { + const line = getLineForConfigIssue(fileContent, warning.text); + + return { + severity: 'error', + message: warning.text, + source: { + file: filePath, + position: { + startLine: line ?? 1, // Use detected line or fallback to 1 + }, + }, + }; + }); +} + +function getLineForConfigIssueForDeprecations( + fileContent: string, + warningText: string, +): number | undefined { + // Extract the deprecated rule name from the warning text + const ruleMatch = /rule "([^"]+)"/.exec(warningText); + const ruleName = ruleMatch ? ruleMatch[1] : undefined; + + if (!ruleName) { + return undefined; // Return undefined if the rule name cannot be extracted + } + + // Create a regex to match the rule name in the configuration + const regex = new RegExp(`"${ruleName}"\\s*:`, 'g'); + const lines = fileContent.split('\n'); + + // Find the matching line + for (let i = 0; i < lines.length; i++) { + if (regex.test(lines[i])) { + return i + 1; // Line numbers are 1-based + } + } + return undefined; // Return undefined if no match is found +} + +export function deprecationsToIssues( + deprecations: LintResult['deprecations'], + filePath: string, + fileContent: string, +): Issue[] { + return deprecations.map(deprecation => { + const line = getLineForConfigIssueForDeprecations( + fileContent, + deprecation.text, + ); + + return { + severity: 'warning', // Deprecations are less critical but need attention + message: `${deprecation.text}${deprecation.reference ? ` (See: ${deprecation.reference})` : ''}`, + source: { + file: filePath, + position: { + startLine: line ?? 1, // Use detected line or fallback to 1 + }, + }, + }; + }); +} diff --git a/packages/plugin-stylelint/src/lib/runner/utils.unit.test.ts b/packages/plugin-stylelint/src/lib/runner/utils.unit.test.ts new file mode 100644 index 000000000..0e79f4626 --- /dev/null +++ b/packages/plugin-stylelint/src/lib/runner/utils.unit.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from 'vitest'; +import type { ActiveConfigRuleSetting } from './model.js'; +import { getSeverityFromRuleConfig } from './utils.js'; + +describe('getSeverityFromRuleConfig', () => { + it('should respect the default severity when from the default', () => { + expect(getSeverityFromRuleConfig([true])).toBe('error'); + }); + + it('should consider the default severity when its different from the default', () => { + expect(getSeverityFromRuleConfig([true], 'warning')).toBe('warning'); + }); + + it.each([true, 5, 'percentage', ['/\\[.+]/', 'percentage'], { a: 1 }])( + 'should return the default severity for a primary value %s', + ruleConfig => { + expect( + getSeverityFromRuleConfig(ruleConfig as ActiveConfigRuleSetting), + ).toBe('error'); + }, + ); + + it('should return the default severity when the rule config does not have a secondary item', () => { + expect(getSeverityFromRuleConfig([true])).toBe('error'); + }); + + it('should return the default severity when the secondary item is missing the `severity` property', () => { + expect(getSeverityFromRuleConfig([true, {}])).toBe('error'); + }); + + it('should return the default severity when `severity` property is of type function', () => { + expect(getSeverityFromRuleConfig([true, { severity: () => {} }])).toBe( + 'error', + ); + }); + + it.each([ + { ruleConfig: [true, { severity: 'warning' }], expected: 'warning' }, + { ruleConfig: [true, { severity: 'error' }], expected: 'error' }, + ])('should return the set severity `%s`', ({ ruleConfig, expected }) => { + expect(getSeverityFromRuleConfig(ruleConfig)).toBe(expected); + }); + + it.each([null, undefined])( + 'should return the default severity for disabled rules %s', + ruleConfig => { + expect( + getSeverityFromRuleConfig( + ruleConfig as unknown as ActiveConfigRuleSetting, + ), + ).toBe('error'); + }, + ); +}); diff --git a/packages/plugin-stylelint/src/lib/stylelint-plugin.ts b/packages/plugin-stylelint/src/lib/stylelint-plugin.ts new file mode 100644 index 000000000..6229aeeb3 --- /dev/null +++ b/packages/plugin-stylelint/src/lib/stylelint-plugin.ts @@ -0,0 +1,55 @@ +import { createRequire } from 'node:module'; +import type { PluginConfig } from '@code-pushup/models'; +import { + type StyleLintPluginConfig, + type StyleLintTarget, + stylelintPluginConfigSchema, +} from './config.js'; +import { createRunnerFunction } from './runner/runner.js'; +import { getAudits, getGroups } from './utils.js'; + +/** + * Instantiates Code PushUp code stylelint plugin for core config. + * + * @example + * import stylelintPlugin from '@code-pushup/stylelint-plugin' + * + * export default { + * // ... core config ... + * plugins: [ + * // ... other plugins ... + * await stylelintPlugin({ + * reports: [{ resultsPath: 'stylelint/cli/lcov.info', pathToProject: 'packages/cli' }] + * }) + * ] + * } + * + * @returns Plugin configuration. + */ +export async function stylelintPlugin( + options?: StyleLintPluginConfig, +): Promise<PluginConfig> { + const { stylelintrc: configFile = '.stylelintrc.json', patterns: files } = + stylelintPluginConfigSchema.parse(options ?? {}).at(0) as StyleLintTarget; + + const packageJson = createRequire(import.meta.url)( + '../../package.json', + ) as typeof import('../../package.json'); + + const audits = await getAudits({ + stylelintrc: configFile, + }); + + return { + slug: 'stylelint', + title: 'Stylelint', + icon: 'folder-css', + description: 'Official Code PushUp code stylelint plugin.', + docsUrl: 'https://www.npmjs.com/package/@code-pushup/stylelint-plugin/', + packageName: packageJson.name, + version: packageJson.version, + audits, + groups: await getGroups({ stylelintrc: configFile }), + runner: createRunnerFunction({ configFile, files }, audits), + }; +} diff --git a/packages/plugin-stylelint/src/lib/stylelint-plugin.unit.test.ts b/packages/plugin-stylelint/src/lib/stylelint-plugin.unit.test.ts new file mode 100644 index 000000000..e074f840d --- /dev/null +++ b/packages/plugin-stylelint/src/lib/stylelint-plugin.unit.test.ts @@ -0,0 +1,84 @@ +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import type { RunnerConfig } from '@code-pushup/models'; +import { stylelintPlugin } from './stylelint-plugin.js'; + +vi.mock('./runner/index.ts', () => ({ + createRunnerConfig: vi.fn().mockReturnValue({ + command: 'node', + outputFile: 'runner-output.json', + } satisfies RunnerConfig), +})); + +describe('stylelintPlugin', () => { + const LCOV_PATH = path.join( + 'packages', + 'plugin-stylelint', + 'mocks', + 'single-record-lcov.info', + ); + + it('should initialise a Code stylelint plugin', async () => { + await expect( + stylelintPlugin({ + stylelintTypes: ['function'], + reports: [LCOV_PATH], + }), + ).resolves.toStrictEqual( + expect.objectContaining({ + slug: 'stylelint', + title: 'Code stylelint', + audits: expect.any(Array), + groups: expect.any(Array), + runner: expect.any(Object), + }), + ); + }); + + it('should generate audits from stylelint types', async () => { + await expect( + stylelintPlugin({ + stylelintTypes: ['function', 'branch'], + reports: [LCOV_PATH], + }), + ).resolves.toStrictEqual( + expect.objectContaining({ + audits: [ + { + slug: 'function-stylelint', + title: 'Function stylelint', + description: expect.stringContaining( + 'how many functions were called', + ), + }, + expect.objectContaining({ slug: 'branch-stylelint' }), + ], + }), + ); + }); + + it('should provide a group from defined stylelint types', async () => { + await expect( + stylelintPlugin({ + stylelintTypes: ['branch', 'line'], + reports: [{ resultsPath: LCOV_PATH }], + }), + ).resolves.toStrictEqual( + expect.objectContaining({ + audits: [ + expect.objectContaining({ slug: 'branch-stylelint' }), + expect.objectContaining({ slug: 'line-stylelint' }), + ], + groups: [ + expect.objectContaining({ + slug: 'stylelint', + refs: [ + expect.objectContaining({ slug: 'branch-stylelint' }), + expect.objectContaining({ slug: 'line-stylelint' }), + ], + }), + ], + }), + ); + }); +}); diff --git a/packages/plugin-stylelint/src/lib/types.ts b/packages/plugin-stylelint/src/lib/types.ts new file mode 100644 index 000000000..55d832649 --- /dev/null +++ b/packages/plugin-stylelint/src/lib/types.ts @@ -0,0 +1,4 @@ +import { EMPTY_GROUPS } from './constants.js'; + +type EmptyGroups = typeof EMPTY_GROUPS; +export type GroupSlug = EmptyGroups[number]['slug']; diff --git a/packages/plugin-stylelint/src/lib/utils.ts b/packages/plugin-stylelint/src/lib/utils.ts new file mode 100644 index 000000000..ba4a884a4 --- /dev/null +++ b/packages/plugin-stylelint/src/lib/utils.ts @@ -0,0 +1,90 @@ +import type { ConfigRuleSettings } from 'stylelint'; +import type { Audit, CategoryRef } from '@code-pushup/models'; +import { + type StyleLintPluginConfig, + type StyleLintTarget, + stylelintPluginConfigSchema, +} from './config.js'; +import { + DEFAULT_STYLELINTRC, + GROUPS, + STYLELINT_PLUGIN_SLUG, +} from './constants.js'; +import type { ActiveConfigRuleSetting } from './runner/model.js'; +import { getNormalizedConfig } from './runner/normalize-config.js'; +import { getSeverityFromRuleConfig } from './runner/utils.js'; + +export function auditSlugToFullAudit(slug: string): Audit { + return { + slug, + title: slug, + docsUrl: `https://stylelint.io/user-guide/rules/${slug}`, + }; +} + +export async function getAudits( + options: Required<Pick<StyleLintTarget, 'stylelintrc'>>, +): Promise<Audit[]> { + const normalizedConfig = await getNormalizedConfig(options); + return Object.keys(normalizedConfig.config.rules).map(auditSlugToFullAudit); +} + +export async function getGroups( + options: Required<Pick<StyleLintTarget, 'stylelintrc'>>, +) { + const { config } = await getNormalizedConfig(options); + const { rules, defaultSeverity } = config; + return GROUPS.map(group => ({ + ...group, + refs: Object.entries(rules) + .filter(filterNonNull) // TODO Type Narrowing is not fully working for the nulls / undefineds + .filter(([_, ruleConfig]) => { + const severity = getSeverityFromRuleConfig( + ruleConfig as ActiveConfigRuleSetting, + defaultSeverity, + ); + return ( + (severity === 'error' && group.slug === 'problems') || + (severity === 'warning' && group.slug === 'suggestions') + ); + }) + .map(([rule]) => ({ slug: rule, weight: 1 })), + })).filter(group => group.refs.length > 0); +} + +export async function getCategoryRefsFromGroups( + opt?: StyleLintPluginConfig, +): Promise<CategoryRef[]> { + const { stylelintrc = DEFAULT_STYLELINTRC } = + stylelintPluginConfigSchema.parse(opt)[0] as StyleLintTarget; + const groups = await getGroups({ stylelintrc }); + return groups.map(({ slug }) => ({ + plugin: STYLELINT_PLUGIN_SLUG, + slug, + weight: 1, + type: 'group', + })); +} + +export async function getCategoryRefsFromAudits( + opt?: StyleLintPluginConfig, +): Promise<CategoryRef[]> { + const { stylelintrc = DEFAULT_STYLELINTRC } = + stylelintPluginConfigSchema.parse(opt)[0] as StyleLintTarget; + const audits = await getAudits({ stylelintrc }); + return audits.map(({ slug }) => ({ + plugin: STYLELINT_PLUGIN_SLUG, + slug, + weight: 1, + type: 'audit', + })); +} + +function filterNonNull<T, O extends object = object>( + settings: Array<ConfigRuleSettings<T, O>>, +): Exclude<ConfigRuleSettings<T, O>, null | undefined>[] { + return settings.filter( + (setting): setting is Exclude<ConfigRuleSettings<T, O>, null | undefined> => + setting != null, + ); +} diff --git a/packages/plugin-stylelint/src/lib/utils.unit.test.ts b/packages/plugin-stylelint/src/lib/utils.unit.test.ts new file mode 100644 index 000000000..0f0324143 --- /dev/null +++ b/packages/plugin-stylelint/src/lib/utils.unit.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; +import type { AuditOutput } from '@code-pushup/models'; +import { applyMaxScoreAboveThreshold } from './utils.js'; + +describe('applyMaxScoreAboveThreshold', () => { + it('should transform score above threshold to maximum', () => { + expect( + applyMaxScoreAboveThreshold( + [ + { + slug: 'branch-stylelint', + value: 75, + score: 0.75, + }, + ], + 0.7, + ), + ).toEqual<AuditOutput[]>([ + { + slug: 'branch-stylelint', + value: 75, + score: 1, + }, + ]); + }); + + it('should leave score below threshold untouched', () => { + expect( + applyMaxScoreAboveThreshold( + [ + { + slug: 'line-stylelint', + value: 60, + score: 0.6, + }, + ], + 0.7, + ), + ).toEqual<AuditOutput[]>([ + { + slug: 'line-stylelint', + value: 60, + score: 0.6, + }, + ]); + }); +}); diff --git a/packages/plugin-stylelint/src/scripts/postinstall/bin.ts b/packages/plugin-stylelint/src/scripts/postinstall/bin.ts new file mode 100644 index 000000000..e19d396bb --- /dev/null +++ b/packages/plugin-stylelint/src/scripts/postinstall/bin.ts @@ -0,0 +1,6 @@ +import { patchStylelint } from './index.js'; + +(async () => { + await patchStylelint(); + console.log('stylelint patched!'); +})(); diff --git a/packages/plugin-stylelint/src/scripts/postinstall/index.ts b/packages/plugin-stylelint/src/scripts/postinstall/index.ts new file mode 100644 index 000000000..a110a4fe4 --- /dev/null +++ b/packages/plugin-stylelint/src/scripts/postinstall/index.ts @@ -0,0 +1,28 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +const stylelintEntryFromPackageRoot = resolve( + '..', + '..', + 'stylelint/lib/index.mjs', +); + +export async function patchStylelint( + stylelintPath = stylelintEntryFromPackageRoot, +) { + try { + let content = await readFile(stylelintPath, 'utf-8'); + + if (!content.includes('default as getConfigForFile')) { + content += ` + export { default as getConfigForFile } from './getConfigForFile.mjs'; + `; + await writeFile(stylelintPath, content, 'utf-8'); + console.log('Patched Stylelint successfully.'); + } else { + console.log('Stylelint already patched.'); + } + } catch (error) { + console.error('Error patching Stylelint:', (error as Error).message); + } +} diff --git a/packages/plugin-stylelint/tsconfig.json b/packages/plugin-stylelint/tsconfig.json new file mode 100644 index 000000000..893f9a925 --- /dev/null +++ b/packages/plugin-stylelint/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "types": ["vitest"] + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.test.json" + } + ] +} diff --git a/packages/plugin-stylelint/tsconfig.lib.json b/packages/plugin-stylelint/tsconfig.lib.json new file mode 100644 index 000000000..ef2f7e2b3 --- /dev/null +++ b/packages/plugin-stylelint/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": [ + "vite.config.unit.ts", + "vite.config.integration.ts", + "src/**/*.test.ts", + "src/**/*.mock.ts", + "mocks/**/*.ts" + ] +} diff --git a/packages/plugin-stylelint/tsconfig.test.json b/packages/plugin-stylelint/tsconfig.test.json new file mode 100644 index 000000000..9f29d6bb0 --- /dev/null +++ b/packages/plugin-stylelint/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"] + }, + "include": [ + "vite.config.unit.ts", + "vite.config.integration.ts", + "mocks/**/*.ts", + "src/**/*.test.ts" + ] +} diff --git a/packages/plugin-stylelint/vite.config.integration.ts b/packages/plugin-stylelint/vite.config.integration.ts new file mode 100644 index 000000000..acd22f891 --- /dev/null +++ b/packages/plugin-stylelint/vite.config.integration.ts @@ -0,0 +1,30 @@ +/// <reference types="vitest" /> +import { defineConfig } from 'vite'; +import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js'; + +export default defineConfig({ + cacheDir: '../../node_modules/.vite/plugin-stylelint', + test: { + reporters: ['basic'], + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + alias: tsconfigPathAliases(), + pool: 'threads', + poolOptions: { threads: { singleThread: true } }, + coverage: { + reporter: ['text', 'lcov'], + reportsDirectory: '../../stylelint/plugin-stylelint/integration-tests', + exclude: ['mocks/**', '**/types.ts'], + }, + environment: 'node', + include: ['src/**/*.integration.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + globalSetup: ['../../global-setup.ts'], + setupFiles: [ + '../../testing/test-setup/src/lib/extend/path.matcher.ts', + '../../testing/test-setup/src/lib/console.mock.ts', + '../../testing/test-setup/src/lib/reset.mocks.ts', + ], + }, +}); diff --git a/packages/plugin-stylelint/vite.config.unit.ts b/packages/plugin-stylelint/vite.config.unit.ts new file mode 100644 index 000000000..8f938db60 --- /dev/null +++ b/packages/plugin-stylelint/vite.config.unit.ts @@ -0,0 +1,32 @@ +/// <reference types="vitest" /> +import { defineConfig } from 'vite'; +import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js'; + +export default defineConfig({ + cacheDir: '../../node_modules/.vite/plugin-stylelint', + test: { + reporters: ['basic'], + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + alias: tsconfigPathAliases(), + pool: 'threads', + poolOptions: { threads: { singleThread: true } }, + coverage: { + reporter: ['text', 'lcov'], + reportsDirectory: '../../stylelint/plugin-stylelint/unit-tests', + exclude: ['mocks/**', '**/types.ts'], + }, + environment: 'node', + include: ['src/**/*.unit.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + globalSetup: ['../../global-setup.ts'], + setupFiles: [ + '../../testing/test-setup/src/lib/extend/path.matcher.ts', + '../../testing/test-setup/src/lib/cliui.mock.ts', + '../../testing/test-setup/src/lib/fs.mock.ts', + '../../testing/test-setup/src/lib/console.mock.ts', + '../../testing/test-setup/src/lib/reset.mocks.ts', + ], + }, +}); diff --git a/packages/utils/tsconfig.lib.json b/packages/utils/tsconfig.lib.json index a51a95ed1..22d5ae755 100644 --- a/packages/utils/tsconfig.lib.json +++ b/packages/utils/tsconfig.lib.json @@ -9,6 +9,7 @@ "exclude": [ "vite.config.unit.ts", "vite.config.integration.ts", + "../../testing/test-setup/src/vitest.d.ts", "src/**/*.test.ts", "src/**/*.mock.ts", "mocks/**/*.ts", diff --git a/packages/utils/tsconfig.test.json b/packages/utils/tsconfig.test.json index bb1ab5e0c..012d59e30 100644 --- a/packages/utils/tsconfig.test.json +++ b/packages/utils/tsconfig.test.json @@ -7,6 +7,7 @@ "include": [ "vite.config.unit.ts", "vite.config.integration.ts", + "../../testing/test-setup/src/vitest.d.ts", "mocks/**/*.ts", "src/**/*.test.ts", "src/**/*.test.tsx", diff --git a/stylelint.config.mjs b/stylelint.config.mjs new file mode 100644 index 000000000..c61787014 --- /dev/null +++ b/stylelint.config.mjs @@ -0,0 +1,14 @@ +export default { + extends: ['stylelint-config-standard-scss'], // configures rules for CSS and SCSS + plugins: ['stylelint-scss'], // Enable SCSS-specific rules + overrides: [ + { + files: ['**/*.scss'], + customSyntax: 'postcss-scss', // Use SCSS parser + }, + ], + rules: { + 'color-no-invalid-hex': true, // Shared rule for CSS and SCSS + 'scss/at-rule-no-unknown': true, // SCSS-specific rule + }, +}; diff --git a/testing/test-setup/src/lib/extend/path.matcher.unit.test.ts b/testing/test-setup/src/lib/extend/path.matcher.unit.test.ts index 676b0065c..c0024e3d0 100644 --- a/testing/test-setup/src/lib/extend/path.matcher.unit.test.ts +++ b/testing/test-setup/src/lib/extend/path.matcher.unit.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import * as testUtils from '@code-pushup/test-utils'; +// vite.ts includes `setupFiles: ['../test-setup/src/lib/extend/path.matcher.ts']` describe('path-matcher', () => { const osAgnosticPathSpy = vi.spyOn(testUtils, 'osAgnosticPath');