diff --git a/eslint.config.mjs b/eslint.config.mjs index bff6a5c..0ff7c6c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,16 +1,11 @@ import { FlatCompat } from '@eslint/eslintrc' import js from '@eslint/js' import tsParser from '@typescript-eslint/parser' -import typescriptEslint from '@typescript-eslint/eslint-plugin' import react from 'eslint-plugin-react' import reactHooks from 'eslint-plugin-react-hooks' import jsxA11y from 'eslint-plugin-jsx-a11y' -import promise from 'eslint-plugin-promise' -import simpleImportSort from 'eslint-plugin-simple-import-sort' -import importPlugin from 'eslint-plugin-import' -import tailwindcss from 'eslint-plugin-tailwindcss' -import nextPlugin from '@next/eslint-plugin-next' -import { rules as customRulesMap } from './src/lint/index.js' +import globals from 'globals' +import { sharedConfig } from './src/lint/shared-config.mjs' import path from 'node:path' import { fileURLToPath } from 'node:url' @@ -26,27 +21,27 @@ export default [ ...compat.extends( 'plugin:react-hooks/recommended', 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'prettier', - 'plugin:tailwindcss/recommended' + 'plugin:@typescript-eslint/recommended' // 'plugin:react-server-components/recommended' // TODO: Re-enable when compatible with ESLint v9 ), + ...sharedConfig, { plugins: { - '@typescript-eslint': typescriptEslint, react: react, 'react-hooks': reactHooks, 'jsx-a11y': jsxA11y, - promise: promise, - 'simple-import-sort': simpleImportSort, - import: importPlugin, - tailwindcss: tailwindcss, - '@next/next': nextPlugin, - cleanlab: { rules: customRulesMap }, }, languageOptions: { parser: tsParser, + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.es2021, + ...globals.node, + React: 'readonly', + }, parserOptions: { project: './tsconfig.json', ecmaVersion: 'latest', @@ -74,64 +69,13 @@ export default [ }, rules: { - 'no-console': [ - 'error', - { - allow: ['warn', 'error', 'info'], - }, - ], + // Design-system specific rules '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/array-type': 'error', '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unused-vars': [ - 'error', - { - vars: 'all', - args: 'after-used', - ignoreRestSiblings: true, - varsIgnorePattern: '^_', - argsIgnorePattern: '^_', - destructuredArrayIgnorePattern: '^_', - }, - ], - 'dot-notation': 'error', - 'promise/catch-or-return': 'error', - 'promise/no-callback-in-promise': 'error', - 'promise/no-nesting': 'error', - 'promise/no-new-statics': 'error', - 'promise/no-promise-in-callback': 'error', - 'promise/no-return-in-finally': 'error', - 'promise/no-return-wrap': 'error', - 'promise/param-names': 'error', - 'promise/valid-params': 'error', - 'react/jsx-curly-brace-presence': [ - 'error', - { - props: 'never', - children: 'never', - }, - ], - 'react-hooks/exhaustive-deps': 'error', - 'simple-import-sort/exports': 'error', - 'simple-import-sort/imports': 'error', - 'no-plusplus': 'off', - 'no-restricted-syntax': [ - 'error', - { - selector: 'ForInStatement', - message: 'for..in loops are error prone.', - }, - { - selector: 'LabeledStatement', - message: 'Labels are a form of GOTO; dont use them.', - }, - { - selector: 'WithStatement', - message: '`with` is disallowed.', - }, - ], + + // Override no-restricted-imports to use design-system specific messages 'no-restricted-imports': [ 'error', { @@ -148,33 +92,6 @@ export default [ ], }, ], - 'no-useless-escape': 'error', - quotes: [ - 'error', - 'single', - { - avoidEscape: true, - }, - ], - '@typescript-eslint/consistent-type-exports': 'error', - '@typescript-eslint/consistent-type-imports': 'error', - '@typescript-eslint/prefer-optional-chain': 'warn', - '@typescript-eslint/prefer-readonly': 'warn', - 'no-useless-rename': 'error', - 'tailwindcss/no-custom-classname': 'off', - 'tailwindcss/classnames-order': 'off', - '@next/next/no-async-client-component': 'error', - 'import/no-useless-path-segments': [ - 'error', - { - noUselessIndex: true, - }, - ], - 'import/no-cycle': 'warn', - 'object-shorthand': ['error', 'properties'], - 'no-console': ['error', { allow: ['warn', 'error', 'info'] }], - 'react/jsx-curly-brace-presence': ['error', { props: 'never', children: 'never' }], - 'cleanlab/cn-no-trivial': 'error', }, }, { @@ -263,4 +180,21 @@ export default [ }, }, }, + { + files: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'], + languageOptions: { + globals: { + ...globals.node, + describe: 'readonly', + it: 'readonly', + expect: 'readonly', + beforeEach: 'readonly', + afterEach: 'readonly', + beforeAll: 'readonly', + afterAll: 'readonly', + vi: 'readonly', + test: 'readonly', + }, + }, + }, ] diff --git a/package.json b/package.json index 00b9caa..fa7982b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "./utils": "./dist/utils/index.js", "./chat": "./dist/chat/index.js", "./lint": "./dist/lint/index.js", + "./lint/shared-config": "./dist/lint/shared-config.mjs", "./logos/*": "./dist/logos/*" }, "scripts": { @@ -137,8 +138,10 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-storybook": "^0.11.6", "eslint-plugin-tailwindcss": "^3.18.2", + "eslint-plugin-unused-imports": "^4.3.0", "eslint-vitest-rule-tester": "^2.2.1", "fuse.js": "^7.1.0", + "globals": "^16.5.0", "happy-dom": "^14.12.3", "husky": "^8.0.3", "image-size": "^2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22487bb..a2bf10e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -267,12 +267,18 @@ importers: eslint-plugin-tailwindcss: specifier: ^3.18.2 version: 3.18.2(tailwindcss@3.4.17) + eslint-plugin-unused-imports: + specifier: ^4.3.0 + version: 4.3.0(@typescript-eslint/eslint-plugin@8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@1.21.6))(typescript@5.7.3))(eslint@9.32.0(jiti@1.21.6))(typescript@5.7.3))(eslint@9.32.0(jiti@1.21.6)) eslint-vitest-rule-tester: specifier: ^2.2.1 version: 2.2.1(eslint@9.32.0(jiti@1.21.6))(typescript@5.7.3)(vitest@1.6.1(@types/node@22.15.18)(happy-dom@14.12.3)(jsdom@16.7.0)(terser@5.36.0)) fuse.js: specifier: ^7.1.0 version: 7.1.0 + globals: + specifier: ^16.5.0 + version: 16.5.0 happy-dom: specifier: ^14.12.3 version: 14.12.3 @@ -3723,6 +3729,15 @@ packages: peerDependencies: tailwindcss: ^3.4.0 + eslint-plugin-unused-imports@4.3.0: + resolution: {integrity: sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0 + eslint: ^9.0.0 || ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -4090,6 +4105,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -11383,6 +11402,12 @@ snapshots: postcss: 8.5.1 tailwindcss: 3.4.17 + eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@1.21.6))(typescript@5.7.3))(eslint@9.32.0(jiti@1.21.6))(typescript@5.7.3))(eslint@9.32.0(jiti@1.21.6)): + dependencies: + eslint: 9.32.0(jiti@1.21.6) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.32.0(jiti@1.21.6))(typescript@5.7.3))(eslint@9.32.0(jiti@1.21.6))(typescript@5.7.3) + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -11833,6 +11858,8 @@ snapshots: globals@14.0.0: {} + globals@16.5.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 diff --git a/src/lint/shared-config.mjs b/src/lint/shared-config.mjs new file mode 100644 index 0000000..6925a87 --- /dev/null +++ b/src/lint/shared-config.mjs @@ -0,0 +1,184 @@ +/** + * Shared ESLint configuration between design-system and codex packages. + * This config contains the rules and settings that should be common across projects. + * + * Usage in other projects: + * ```js + * import { sharedConfig } from '@cleanlab/design-system/lint/shared-config' + * + * export default [ + * ...sharedConfig, + * // Your additional config here + * ] + * ``` + */ + +import { FlatCompat } from '@eslint/eslintrc' +import js from '@eslint/js' +import typescriptEslint from '@typescript-eslint/eslint-plugin' +import tailwindcss from 'eslint-plugin-tailwindcss' +import unusedImports from 'eslint-plugin-unused-imports' +import simpleImportSort from 'eslint-plugin-simple-import-sort' +import promise from 'eslint-plugin-promise' +import importPlugin from 'eslint-plugin-import' +import nextPlugin from '@next/eslint-plugin-next' +import { rules as customRules } from './index.js' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}) + +export const sharedConfig = [ + ...compat.extends('prettier', 'plugin:tailwindcss/recommended'), + { + plugins: { + '@typescript-eslint': typescriptEslint, + tailwindcss, + 'unused-imports': unusedImports, + 'simple-import-sort': simpleImportSort, + promise, + import: importPlugin, + '@next/next': nextPlugin, + cleanlab: { rules: customRules }, + }, + + settings: { + tailwindcss: { + callees: ['cn', 'cva'], + }, + }, + + rules: { + // Tailwind CSS + 'tailwindcss/no-custom-classname': 'off', + 'tailwindcss/classnames-order': 'off', + + // Import restrictions + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: 'tailwind-variants', + message: 'Import from design-system utils instead', + }, + { + name: 'tailwind-merge', + message: 'Import from design-system utils instead', + }, + ], + }, + ], + + // Syntax restrictions + 'no-restricted-syntax': [ + 'error', + { + selector: 'ForInStatement', + message: 'for..in loops are error prone.', + }, + { + selector: 'LabeledStatement', + message: 'Labels are a form of GOTO; dont use them.', + }, + { + selector: 'WithStatement', + message: '`with` is disallowed.', + }, + { + selector: 'MemberExpression[object.name="React"][property.name=/^use[A-Z]/]', + message: + 'Import hooks directly from React instead of using React namespace (e.g., use "import { useState } from \'react\'" instead of "React.useState")', + }, + ], + + // React hooks + 'react-hooks/exhaustive-deps': 'error', + + // Import sorting + 'simple-import-sort/exports': 'error', + 'simple-import-sort/imports': 'error', + + // Import rules + 'import/no-useless-path-segments': [ + 'error', + { + noUselessIndex: true, + }, + ], + 'import/no-cycle': 'warn', + + // Unused imports/vars (from codex) + 'no-unused-vars': 'off', + 'no-undef': 'error', + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': [ + 'warn', + { + vars: 'all', + varsIgnorePattern: '^_', + args: 'after-used', + argsIgnorePattern: '^_', + }, + ], + + // TypeScript + '@typescript-eslint/array-type': 'error', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'all', + argsIgnorePattern: '^_', + caughtErrors: 'all', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_', + ignoreRestSiblings: true, + }, + ], + '@typescript-eslint/consistent-type-exports': [ + 'error', + { fixMixedExportsWithInlineTypeSpecifier: true }, + ], + '@typescript-eslint/consistent-type-imports': ['error', { fixStyle: 'inline-type-imports' }], + '@typescript-eslint/prefer-optional-chain': 'warn', + '@typescript-eslint/prefer-readonly': 'warn', + + // Promise rules + 'promise/catch-or-return': 'error', + 'promise/no-callback-in-promise': 'error', + 'promise/no-nesting': 'error', + 'promise/no-new-statics': 'error', + 'promise/no-promise-in-callback': 'error', + 'promise/no-return-in-finally': 'error', + 'promise/no-return-wrap': 'error', + 'promise/param-names': 'error', + 'promise/valid-params': 'error', + + // Code quality + 'dot-notation': 'error', + 'no-plusplus': 'off', + 'no-useless-escape': 'error', + 'quotes': ['error', 'single', { avoidEscape: true }], + 'object-shorthand': ['error', 'properties'], + 'no-useless-rename': 'error', + 'no-console': ['error', { allow: ['warn', 'error', 'info'] }], + + // React + 'react/jsx-curly-brace-presence': ['error', { props: 'never', children: 'never' }], + + // Next.js + '@next/next/no-async-client-component': 'error', + + // Custom rules + 'cleanlab/cn-no-trivial': 'error', + }, + }, +]