@rockpack/codestyle
Version:
Opinionated ESLint, Prettier, and Stylelint configuration for React and TypeScript projects
354 lines (322 loc) • 9.29 kB
JavaScript
import reactPlugin from '@eslint-react/eslint-plugin';
import js from '@eslint/js';
import json from '@eslint/json';
import { getMode } from '@rockpack/utils';
import tseslintPlugin from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import checkFile from 'eslint-plugin-check-file';
import importLite from 'eslint-plugin-import-lite';
import noOnlyTests from 'eslint-plugin-no-only-tests';
import packageJsonConfig from 'eslint-plugin-package-json';
import perfectionist from 'eslint-plugin-perfectionist';
import prettierRecommended from 'eslint-plugin-prettier/recommended';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import regexpPlugin from 'eslint-plugin-regexp';
import sonar from 'eslint-plugin-sonarjs';
import unicorn from 'eslint-plugin-unicorn';
import { globalIgnores } from 'eslint/config';
import globals from 'globals';
import { existsSync, readFileSync } from 'node:fs';
import path from 'node:path';
import tseslint from 'typescript-eslint';
const ignores = [
'.idea',
'.last-run.json',
'**/*.d.ts',
'*.d.ts',
'node_modules',
'logs',
'*.log',
'lib-cov/',
'coverage/',
'coverage-e2e/',
'NO_COMMIT/',
'test-reports/**',
'docs/*',
'build/*',
'lib/*',
'dist/*',
'example/*',
'*.css',
'*.scss',
'*.less',
'*.ico',
'*.jpg',
'*.jpeg',
'*.png',
'*.svg',
'*.bmp',
'*.gif',
'*.webp',
'*.woff',
'*.woff2',
'*.txt',
'*.mdx',
'*.md',
'*.ejs',
'*.hbs',
'*.jade',
'*.html',
'docs/',
'public/',
'seo_report',
'**/.last-run.json',
];
const jsFiles = ['**/*.{js,jsx,mjs,cjs}'];
const tsFiles = ['**/*.{ts,tsx}'];
const sourceFiles = ['**/*.{js,jsx,mjs,cjs,ts,tsx}'];
export const isString = (value) => typeof value === 'string';
export const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
export const makeConfig = () => {
const mode = getMode(['development', 'production'], 'production');
const root = process.cwd();
const packageJsonPath = path.resolve(root, 'package.json');
const packageJson = existsSync(packageJsonPath) ? JSON.parse(readFileSync(packageJsonPath, 'utf8')) : {};
const { hasReact } = packageJson && isObject(packageJson.dependencies) && isString(packageJson.dependencies.react);
let tsConfig = false;
if (existsSync(path.resolve(root, './tsconfig.js'))) {
tsConfig = path.resolve(root, './tsconfig.js');
}
if (existsSync(path.resolve(root, './tsconfig.json'))) {
tsConfig = path.resolve(root, './tsconfig.json');
}
if (existsSync(path.resolve(root, './tsconfig.development.js')) && mode === 'development') {
tsConfig = path.resolve(root, './tsconfig.development.js');
}
if (existsSync(path.resolve(root, './tsconfig.production.js')) && mode === 'production') {
tsConfig = path.resolve(root, './tsconfig.production.js');
}
const languageOptions = {
ecmaVersion: 2024,
globals: {
...globals.browser,
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
sourceType: 'module',
};
const customTypescriptConfig = {
files: tsFiles,
languageOptions: {
...languageOptions,
parser: tsParser,
parserOptions: {
project: tsConfig || './tsconfig.json',
},
},
plugins: {
'@check-file': checkFile,
'@import-lite': importLite,
'@no-only-tests': noOnlyTests,
'@sonar': sonar,
'@typescript-eslint': tseslintPlugin,
'@unicorn': unicorn,
'import/parsers': tsParser,
},
rules: {
'@check-file/filename-naming-convention': [
'error',
{
'src/**/*.{ts,tsx}': 'KEBAB_CASE',
},
{
ignoreMiddleExtensions: true,
},
],
'@check-file/folder-naming-convention': [
'error',
{
'src/**/': 'KEBAB_CASE',
},
],
'@import-lite/no-default-export': 'error',
'@no-only-tests/no-only-tests': 'error',
'@sonar/cognitive-complexity': ['error', 20],
'@sonar/no-collapsible-if': 'error',
'@sonar/no-identical-expressions': 'error',
'@sonar/no-identical-functions': 'error',
'@sonar/no-inverted-boolean-check': 'error',
'@sonar/no-redundant-boolean': 'error',
'@sonar/no-small-switch': 'error',
'@sonar/no-unused-collection': 'error',
'@sonar/prefer-immediate-return': 'error',
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/explicit-function-return-type': 'warn',
'@typescript-eslint/naming-convention': [
'error',
{
format: ['UPPER_CASE', 'StrictPascalCase'],
selector: 'interface',
},
{
format: ['PascalCase'],
selector: 'typeLike',
},
{
format: ['UPPER_CASE', 'StrictPascalCase'],
selector: 'class',
},
],
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true,
},
],
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'after-used',
ignoreRestSiblings: false,
vars: 'all',
},
],
'@typescript-eslint/return-await': 'off',
'@unicorn/no-useless-undefined': ['error', { checkArguments: false, checkArrowFunctionBody: false }],
'@unicorn/prefer-array-flat': 'error',
'@unicorn/prefer-modern-dom-apis': 'error',
'@unicorn/prefer-node-protocol': 'error',
'@unicorn/prefer-string-starts-ends-with': 'error',
'@unicorn/throw-new-error': 'error',
'array-callback-return': [
'error',
{
allowImplicit: true,
},
],
camelcase: ['error', { properties: 'always' }],
'class-methods-use-this': 'off',
'getter-return': [
'error',
{
allowImplicit: true,
},
],
'newline-before-return': 'error',
'no-alert': 'error',
'no-await-in-loop': 'off',
'no-console': 'error',
'no-debugger': 'error',
'no-param-reassign': 'off',
'no-plusplus': 'off',
'no-return-await': 'off',
'no-underscore-dangle': 'off',
'no-unused-vars': 'off',
'no-warning-comments': 'warn',
},
};
if (hasReact) {
Object.assign(customTypescriptConfig.rules, {
'react/function-component-definition': [
2,
{
namedComponents: 'arrow-function',
unnamedComponents: 'arrow-function',
},
],
'react/jsx-uses-react': 'off',
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
});
}
const recommendedTypeScriptConfigs = [
...tseslint.configs.recommended.map((config) => ({
...config,
files: tsFiles,
})),
...tseslint.configs.stylistic.map((config) => ({
...config,
files: tsFiles,
})),
...tseslint.configs.recommendedTypeChecked.map((config) => ({
...config,
files: tsFiles,
})),
];
if (hasReact) {
customTypescriptConfig.rules['react/jsx-uses-react'] = 'off';
customTypescriptConfig.rules['react/react-in-jsx-scope'] = 'off';
}
const jsonCustomConfig = {
files: ['**/*.json'],
ignores: ['**/*-lock.json', 'package.json'],
language: 'json/json',
plugins: {
json,
},
...json.configs.recommended,
};
const customPackageJsonConfig = {
files: ['package.json'],
ignores: ['**/*-lock.json'],
rules: {
'package-json/require-exports': 'off',
'package-json/require-files': 'off',
'package-json/require-repository': 'off',
'package-json/require-sideEffects': 'off',
'package-json/require-type': 'off',
},
};
const customJsConfig = {
files: jsFiles,
languageOptions: {
globals: {
...globals.node,
...globals.jest,
...globals.browser,
},
},
...js.configs.recommended,
};
const perfectionistConfig = {
files: sourceFiles,
...perfectionist.configs['recommended-natural'],
};
const regexpConfig = {
files: sourceFiles,
...regexpPlugin.configs['flat/recommended'],
};
const disableDefaultExportBlockingForStorybook = {
files: [
'**/*.stories.@(js|jsx|ts|tsx|mdx)',
'**/playwright*.config.ts',
'**/.storybook/**',
'**/vite.config.ts',
'**/vitest.config.ts',
],
rules: {
'@import-lite/no-default-export': 'off',
},
};
return [
globalIgnores(ignores),
...recommendedTypeScriptConfigs,
prettierRecommended,
perfectionistConfig,
regexpConfig,
customTypescriptConfig,
jsonCustomConfig,
packageJsonConfig.configs.recommended,
customPackageJsonConfig,
packageJsonConfig.configs.stylistic,
customJsConfig,
hasReact
? {
settings: {
react: {
version: 'detect',
},
},
...reactHooksPlugin.configs.flat.recommended,
...reactPlugin.configs['recommended-type-checked'],
files: sourceFiles,
}
: {},
disableDefaultExportBlockingForStorybook,
];
};