Skip to content

Commit

Permalink
Add Stylelint to lint CSS (#916)
Browse files Browse the repository at this point in the history
  • Loading branch information
connor-baer authored Dec 1, 2023
1 parent ebbb437 commit b3b5cfe
Show file tree
Hide file tree
Showing 12 changed files with 1,394 additions and 48 deletions.
5 changes: 5 additions & 0 deletions .changeset/dull-elephants-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sumup/foundry': minor
---

Added support for linting CSS files with [Stylelint](https://stylelint.io/).
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Check code for syntax errors and format it automatically. The preset adds the fo
The preset includes the following tools:

- **[ESLint](https://www.npmjs.com/package/eslint)** identifies and fixes problematic patterns in your code so you can spot mistakes early.
- **[Stylelint](https://www.npmjs.com/package/stylelint)** identifies and fixes problematic patterns in your styles so you can spot mistakes early.
- **[Prettier](https://prettier.io)** is our code formatter of choice. It makes all our code look the same after every save.
- **[lint-staged](https://www.npmjs.com/package/lint-staged)** is a tool for running linters on files staged for your next commit in git. Together with Husky (see below) it prevents problematic code from being committed.
- **[Husky](https://github.com/typicode/husky)** makes setting up git hooks very easy. Whenever someone installs your project, Husky will automatically set up git hooks as part of its `postinstall` script.
Expand Down
1,170 changes: 1,124 additions & 46 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"./eslint": "./dist/eslint.js",
"./husky": "./dist/husky.js",
"./lint-staged": "./dist/lint-staged.js",
"./prettier": "./dist/prettier.js"
"./prettier": "./dist/prettier.js",
"./stylelint": "./dist/stylelint.js"
},
"scripts": {
"build": "npm run cleanup && tsc && chmod +x dist/cli/index.js",
Expand Down Expand Up @@ -76,6 +77,10 @@
"lodash": "^4.17.21",
"prettier": "^2.8.7",
"read-pkg-up": "^7.0.1",
"stylelint": "^15.11.0",
"stylelint-config-recess-order": "^4.4.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-no-unsupported-browser-features": "^7.0.0",
"yargs": "^17.7.2"
},
"devDependencies": {
Expand Down
9 changes: 9 additions & 0 deletions src/configs/lint-staged/__snapshots__/config.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ exports[`lint-staged should override the default config 1`] = `
"*.(js|jsx|json)": [
"next lint",
],
"*.css": [
"foundry run stylelint --fix",
],
"*.jsx?": [
"custom command",
],
Expand All @@ -16,6 +19,9 @@ exports[`lint-staged with options should return a config for { language: 'JavaSc
"*.(js|jsx|json)": [
"foundry run eslint --fix",
],
"*.css": [
"foundry run stylelint --fix",
],
}
`;

Expand All @@ -25,5 +31,8 @@ exports[`lint-staged with options should return a config for { language: 'TypeSc
"foundry run eslint --fix",
],
"*.(ts|tsx)": [Function],
"*.css": [
"foundry run stylelint --fix",
],
}
`;
2 changes: 2 additions & 0 deletions src/configs/lint-staged/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ interface LintStagedConfig {

export const javascript: LintStagedConfig = {
'*.(js|jsx|json)': ['foundry run eslint --fix'],
'*.css': ['foundry run stylelint --fix'],
};

export const typescript: LintStagedConfig = {
'*.(js|jsx|json|ts|tsx)': ['foundry run eslint --fix'],
'*.(ts|tsx)': () => 'tsc -p tsconfig.json --noEmit',
'*.css': ['foundry run stylelint --fix'],
};

const LANGUAGES = {
Expand Down
107 changes: 107 additions & 0 deletions src/configs/stylelint/config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* Copyright 2023, SumUp Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { customizeConfig, createConfig } from './config';

describe('stylelint', () => {
describe('customizeConfig', () => {
it('should merge the presets of the base config with the custom config', () => {
const baseConfig = {
extends: ['some-preset'],
rules: {
'some-custom-rule': false,
},
};
const newConfig = {
extends: ['other-preset'],
};
const actual = customizeConfig(baseConfig, newConfig);
expect(actual).toEqual(
expect.objectContaining({
extends: ['some-preset', 'other-preset'],
rules: {
'some-custom-rule': false,
},
}),
);
});

it('should merge the extends of the base config with the custom config', () => {
const base = {
extends: ['airbnb-base', 'plugin:prettier/recommended'],
};
const custom = {
extends: ['plugin:react/recommended'],
};
const expected = {
extends: [
'airbnb-base',
'plugin:prettier/recommended',
'plugin:react/recommended',
],
};
const actual = customizeConfig(base, custom);
expect(actual).toEqual(expected);
});

it('should merge the rules of the base config with the custom config', () => {
const base = {
rules: {
'no-use-before-define': ['error', { functions: false }],
'curly': ['error', 'all'],
'no-underscore-dangle': [
'error',
{ allow: ['__DEV__', '__PRODUCTION__'] },
],
},
};
const custom = {
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'no-underscore-dangle': ['warning', { allow: ['__resourcePath'] }],
},
};
const expected = {
rules: {
'no-use-before-define': ['error', { functions: false }],
'curly': ['error', 'all'],
'no-underscore-dangle': ['warning', { allow: ['__resourcePath'] }],
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
};
const actual = customizeConfig(base, custom);
expect(actual).toEqual(expected);
});
});

describe('with overrides', () => {
it('should merge with the default config', () => {
const overrides = {
extends: ['stylelint-config-styled-components'],
};
const actual = createConfig(overrides);
expect(actual).toEqual(
expect.objectContaining({
extends: expect.arrayContaining([
'stylelint-config-standard',
'stylelint-config-styled-components',
]),
}),
);
});
});
});
65 changes: 65 additions & 0 deletions src/configs/stylelint/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Copyright 2023, SumUp Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Config as StylelintConfig } from 'stylelint';
import { mergeWith, isArray, isObject, uniq } from 'lodash/fp';

export const customizeConfig = mergeWith(customizer);

function isArrayTypeGuard(array: unknown): array is unknown[] {
return isArray(array);
}

function customizer(
objValue: unknown,
srcValue: unknown,
key: string,
): unknown {
if (isArrayTypeGuard(objValue) && isArrayTypeGuard(srcValue)) {
return uniq([...objValue, ...srcValue]);
}
if (isObject(objValue) && isObject(srcValue)) {
return key === 'rules' ? { ...objValue, ...srcValue } : undefined;
}
return undefined;
}

const base: StylelintConfig = {
extends: ['stylelint-config-standard', 'stylelint-config-recess-order'],
plugins: ['stylelint-no-unsupported-browser-features'],
rules: {
'declaration-block-no-redundant-longhand-properties': null,
'media-feature-range-notation': ['prefix'],
'no-descending-specificity': null,
'selector-class-pattern': null,
'selector-not-notation': ['simple'],
'selector-pseudo-class-no-unknown': [
true,
{ ignorePseudoClasses: ['global'] },
],
'value-keyword-case': ['lower', { camelCaseSvgKeywords: true }],
'plugin/no-unsupported-browser-features': [
true,
{ severity: 'warning', ignorePartialSupport: true },
],
},
reportDescriptionlessDisables: true,
reportInvalidScopeDisables: true,
reportNeedlessDisables: true,
};

export function createConfig(overrides: StylelintConfig = {}): StylelintConfig {
return customizeConfig(base, overrides);
}
49 changes: 49 additions & 0 deletions src/configs/stylelint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright 2023, SumUp Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import dedent from 'dedent';

import { Script, File } from '../../types/shared';

export const files = (): File[] => [
{
name: '.stylelintrc.js',
content: `
module.exports = require('@sumup/foundry/stylelint')()`,
},
{
name: '.stylelintignore',
content: `${dedent`
node_modules/
build/
dist/
.next/
.out/
static/
public/
coverage/
__coverage__/
__reports__/
`}\n`,
},
];

export const scripts = (): Script[] => [
{
name: 'lint:css',
command: "foundry run stylelint '**/*.css'",
description: 'check files for problematic patterns and report them',
},
];
8 changes: 7 additions & 1 deletion src/presets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ const lint: PresetConfig = {
),
value: Preset.LINT,
short: 'Lint',
tools: [Tool.ESLINT, Tool.PRETTIER, Tool.HUSKY, Tool.LINT_STAGED],
tools: [
Tool.ESLINT,
Tool.STYLELINT,
Tool.PRETTIER,
Tool.HUSKY,
Tool.LINT_STAGED,
],
prompts: [Prompt.OPEN_SOURCE],
};

Expand Down
18 changes: 18 additions & 0 deletions src/stylelint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Copyright 2023, SumUp Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { createConfig } from './configs/stylelint/config';

export = createConfig;
1 change: 1 addition & 0 deletions src/types/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export enum Preset {

export enum Tool {
ESLINT = 'eslint',
STYLELINT = 'stylelint',
PRETTIER = 'prettier',
HUSKY = 'husky',
LINT_STAGED = 'lint-staged',
Expand Down

0 comments on commit b3b5cfe

Please sign in to comment.