Skip to content

Commit

Permalink
Add support for Circuit UI's Stylelint plugin (#956)
Browse files Browse the repository at this point in the history
  • Loading branch information
connor-baer authored Mar 14, 2024
1 parent 08122eb commit 1855159
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 60 deletions.
5 changes: 5 additions & 0 deletions .changeset/tough-geckos-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sumup/foundry': minor
---

Added support for [Circuit UI's Stylelint plugin](https://circuit.sumup.com/?path=/docs/packages-stylelint-plugin-circuit-ui--docs).
2 changes: 1 addition & 1 deletion src/configs/eslint/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* limitations under the License.
*/

import { describe, expect, it, vi, Mock } from 'vitest';
import { describe, expect, it, vi, type Mock } from 'vitest';

import { Language, Environment, Framework, Plugin } from '../../types/shared';
import { getAllChoiceCombinations } from '../../lib/choices';
Expand Down
54 changes: 54 additions & 0 deletions src/configs/stylelint/__snapshots__/config.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`stylelint > with options > should return a config for { plugins: ['Circuit UI'] } 1`] = `
{
"extends": [
"stylelint-config-standard",
"stylelint-config-recess-order",
],
"plugins": [
"stylelint-no-unsupported-browser-features",
"@sumup/circuit-ui",
],
"reportDescriptionlessDisables": true,
"reportInvalidScopeDisables": true,
"reportNeedlessDisables": true,
"rules": {
"@sumup/circuit-ui/component-lifecycle-imports": "error",
"@sumup/circuit-ui/no-deprecated-components": "warn",
"@sumup/circuit-ui/no-deprecated-props": "warn",
"@sumup/circuit-ui/no-invalid-custom-properties": "error",
"@sumup/circuit-ui/no-renamed-props": "error",
"declaration-block-no-redundant-longhand-properties": null,
"media-feature-range-notation": [
"prefix",
],
"no-descending-specificity": null,
"plugin/no-unsupported-browser-features": [
true,
{
"ignorePartialSupport": true,
"severity": "warning",
},
],
"selector-class-pattern": null,
"selector-not-notation": [
"simple",
],
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": [
"global",
],
},
],
"value-keyword-case": [
"lower",
{
"camelCaseSvgKeywords": true,
},
],
},
}
`;
19 changes: 18 additions & 1 deletion src/configs/stylelint/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,19 @@
* limitations under the License.
*/

import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi, type Mock } from 'vitest';

import { Plugin } from '../../types/shared';
import { getOptions as getOptionsMock } from '../../lib/options';

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

vi.mock('../../lib/options', () => ({
getOptions: vi.fn(() => ({})),
}));

const getOptions = getOptionsMock as Mock;

describe('stylelint', () => {
describe('customizeConfig', () => {
it('should merge the presets of the base config with the custom config', () => {
Expand Down Expand Up @@ -106,4 +115,12 @@ describe('stylelint', () => {
);
});
});

describe('with options', () => {
it("should return a config for { plugins: ['Circuit UI'] }", () => {
getOptions.mockReturnValue({ plugins: [Plugin.CIRCUIT_UI] });
const actual = createConfig();
expect(actual).toMatchSnapshot();
});
});
});
43 changes: 41 additions & 2 deletions src/configs/stylelint/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
*/

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

import { getOptions } from '../../lib/options';
import { Plugin } from '../../types/shared';

export const customizeConfig = mergeWith(customizer);

Expand Down Expand Up @@ -60,6 +63,42 @@ const base: StylelintConfig = {
reportNeedlessDisables: true,
};

function customizePlugin(plugins: Plugin[]) {
const pluginMap: { [key in Plugin]?: StylelintConfig } = {
[Plugin.CIRCUIT_UI]: {
plugins: ['@sumup/circuit-ui'],
rules: {
'@sumup/circuit-ui/component-lifecycle-imports': 'error',
'@sumup/circuit-ui/no-invalid-custom-properties': 'error',
'@sumup/circuit-ui/no-renamed-props': 'error',
'@sumup/circuit-ui/no-deprecated-props': 'warn',
'@sumup/circuit-ui/no-deprecated-components': 'warn',
},
},
};

return (config: StylelintConfig): StylelintConfig => {
if (!plugins || isEmpty(plugins)) {
return config;
}

return plugins.reduce((acc, plugin: Plugin) => {
const overrides = pluginMap[plugin];
return customizeConfig(acc, overrides);
}, config);
};
}

function applyOverrides(overrides: StylelintConfig) {
return (config: StylelintConfig): StylelintConfig =>
customizeConfig(config, overrides);
}

export function createConfig(overrides: StylelintConfig = {}): StylelintConfig {
return customizeConfig(base, overrides);
const options = getOptions();

return flow(
customizePlugin(options.plugins),
applyOverrides(overrides),
)(base);
}
142 changes: 86 additions & 56 deletions src/lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,26 +41,33 @@ const PLUGINS = [
{
name: Plugin.CIRCUIT_UI,
frameworkPackages: ['@sumup/circuit-ui', '@sumup/design-tokens'],
pluginPackage: '@sumup/eslint-plugin-circuit-ui',
supportedRange: '>=3.0.0 <5.0.0',
eslintPlugins: {
'@sumup/eslint-plugin-circuit-ui': '>=3.0.0 <5.0.0',
},
stylelintPlugins: {
'@sumup/stylelint-plugin-circuit-ui': '>=1.0.0 <3.0.0',
},
},
{
name: Plugin.NEXT_JS,
frameworkPackages: ['next'],
pluginPackage: 'eslint-config-next',
supportedRange: '>=10.0.0',
eslintPlugins: {
'eslint-config-next': '>=10.0.0',
},
},
{
name: Plugin.EMOTION,
frameworkPackages: ['@emotion/react', '@emotion/styled'],
pluginPackage: '@emotion/eslint-plugin',
supportedRange: '^11.0.0',
eslintPlugins: {
'@emotion/eslint-plugin': '^11.0.0',
},
},
{
name: Plugin.JEST,
frameworkPackages: ['jest'],
pluginPackage: 'eslint-plugin-jest',
supportedRange: '^27.0.0',
eslintPlugins: {
'eslint-plugin-jest': '^27.0.0',
},
},
{
name: Plugin.TESTING_LIBRARY,
Expand All @@ -69,26 +76,30 @@ const PLUGINS = [
'@testing-library/jest-dom',
'@testing-library/react',
],
pluginPackage: 'eslint-plugin-testing-library',
supportedRange: '^6.0.0',
eslintPlugins: {
'eslint-plugin-testing-library': '^6.0.0',
},
},
{
name: Plugin.CYPRESS,
frameworkPackages: ['cypress'],
pluginPackage: 'eslint-plugin-cypress',
supportedRange: '^2.0.0',
eslintPlugins: {
'eslint-plugin-cypress': '^2.0.0',
},
},
{
name: Plugin.PLAYWRIGHT,
frameworkPackages: ['@playwright/test'],
pluginPackage: 'eslint-plugin-playwright',
supportedRange: '>=0.17.0 <2.0.0',
eslintPlugins: {
'eslint-plugin-playwright': '>=0.17.0 <2.0.0',
},
},
{
name: Plugin.STORYBOOK,
frameworkPackages: ['storybook', '@storybook/react'],
pluginPackage: 'eslint-plugin-storybook',
supportedRange: '>=0.6.0 <1.0.0',
eslintPlugins: {
'eslint-plugin-storybook': '>=0.6.0 <1.0.0',
},
},
];

Expand All @@ -105,6 +116,7 @@ export function getOptions(): Required<Options> {
language: pick(config.language, detectLanguage),
environments: pick(config.environments, detectEnvironments),
frameworks: pick(config.frameworks, detectFrameworks),
// TODO: Differentiate between ESLint and Stylelint plugins
plugins: pick(config.plugins, detectPlugins),
openSource: pick(config.openSource, detectOpenSource),
workspaces: packageJson.workspaces || null,
Expand Down Expand Up @@ -180,61 +192,79 @@ export function detectFrameworks(packageJson: PackageJson): Framework[] {
}

export function warnAboutUnsupportedPlugins(packageJson: PackageJson): void {
PLUGINS.forEach(({ pluginPackage, supportedRange }) => {
let version = getDependencyVersion(packageJson, pluginPackage);
PLUGINS.forEach(({ eslintPlugins, stylelintPlugins = {} }) => {
Object.entries({ ...eslintPlugins, ...stylelintPlugins }).forEach(
([pluginPackage, supportedRange]: [string, string]) => {
let version = getDependencyVersion(packageJson, pluginPackage);

if (!version) {
return;
}

try {
// Extract the version from tarball URLs
if (version.startsWith('https://')) {
const matches = version.match(/(\d\.\d\.\d.*)\.tgz/);
if (!version) {
return;
}

if (matches) {
// eslint-disable-next-line prefer-destructuring
version = matches[1];
try {
// Extract the version from tarball URLs
if (version.startsWith('https://')) {
const matches = version.match(/(\d\.\d\.\d.*)\.tgz/);

if (matches) {
// eslint-disable-next-line prefer-destructuring
version = matches[1];
}
}

const isSupported = intersects(version, supportedRange);

if (!isSupported) {
logger.warn(
`"${pluginPackage}" is installed at version "${version}". Foundry has only been tested with versions "${supportedRange}". You may find that it works just fine, or you may not. Pull requests welcome!`,
);
}
} catch (error) {
logger.warn(
`Failed to verify whether "${pluginPackage}" installed at version "${version}" is supported. You may find that it works just fine, or you may not.`,
);
logger.debug((error as Error).message);
}
}

const isSupported = intersects(version, supportedRange);

if (!isSupported) {
logger.warn(
`"${pluginPackage}" is installed at version "${version}". Foundry has only been tested with versions "${supportedRange}". You may find that it works just fine, or you may not. Pull requests welcome!`,
);
}
} catch (error) {
logger.warn(
`Failed to verify whether "${pluginPackage}" installed at version "${version}" is supported. You may find that it works just fine, or you may not.`,
);
logger.debug((error as Error).message);
}
},
);
});
}

export function warnAboutMissingPlugins(packageJson: PackageJson): void {
PLUGINS.forEach(({ frameworkPackages, pluginPackage }) => {
PLUGINS.forEach(({ frameworkPackages, eslintPlugins, stylelintPlugins }) => {
const installedPackage = frameworkPackages.find((pkg) =>
hasDependency(packageJson, pkg),
);

if (installedPackage && !hasDependency(packageJson, pluginPackage)) {
logger.warn(
`"${installedPackage}" is installed but not the corresponding ESLint plugin. Please install "${pluginPackage}".`,
);
}
Object.keys({ ...eslintPlugins, ...stylelintPlugins }).forEach(
(pluginPackage) => {
if (installedPackage && !hasDependency(packageJson, pluginPackage)) {
logger.warn(
`"${installedPackage}" is installed but not the corresponding ESLint plugin. Please install "${pluginPackage}".`,
);
}
},
);
});
}

export function detectPlugins(packageJson: PackageJson): Plugin[] {
return PLUGINS.reduce((allPlugins, { pluginPackage, name }) => {
if (hasDependency(packageJson, pluginPackage)) {
allPlugins.push(name);
}
return allPlugins;
}, [] as Plugin[]);
const pluginSet = PLUGINS.reduce(
(allPlugins, { name, eslintPlugins, stylelintPlugins }) => {
const plugins = Object.keys({ ...eslintPlugins, ...stylelintPlugins });

plugins.forEach((pluginPackage) => {
if (hasDependency(packageJson, pluginPackage)) {
allPlugins.add(name);
}
});

return allPlugins;
},
new Set<Plugin>(),
);

return Array.from(pluginSet);
}

export function detectOpenSource(packageJson: PackageJson) {
Expand Down

0 comments on commit 1855159

Please sign in to comment.