@hayuno/eslint-config 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Hayuno AG
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @hayuno-ag/eslint-config
2
+
3
+ This packages offers configurations for ESLint and Prettier which are used for projects at Hayuno AG.
4
+
5
+ ## Prerequisites
6
+
7
+ Install `eslint`, `typescript`, `@typescript-eslint/parser`, `eslint-config-prettier`, `prettier` and `@hayuno-ag/eslint-config` as dev dependencies:
8
+
9
+ ```sh
10
+ npm install --save-dev @hayuno-ag/eslint-config eslint typescript @typescript-eslint/parser eslint-config-prettier prettier
11
+ ```
12
+
13
+ Note: If you're using Next.js, `next` is already expected to be installed in your project. The plugin is included as a dependency of this package: `@next/eslint-plugin-next`.
14
+
15
+ ## Integration
16
+
17
+ By setting-up your `eslint.config.mjs` (example):
18
+
19
+ ```js
20
+ import { defineConfig } from 'eslint/config';
21
+ import eslintConfig from '@hayuno-ag/eslint-config';
22
+ // additional imports
23
+
24
+ export default defineConfig([
25
+ globalIgnores(['**/node_modules', '**/dist', '**/build', 'bin/**', 'package-lock.json']),
26
+ ...eslintConfig,
27
+ {
28
+ languageOptions: {
29
+ /* your language options */
30
+ },
31
+ rules: {
32
+ /* your override rules */
33
+ },
34
+ },
35
+ // Test-specific rules
36
+ {
37
+ files: ['tests/**/*.test.tsx', 'tests/**/*.test.ts'],
38
+ rules: {
39
+ '@hayuno-ag/no-speculative-it': 'warn',
40
+ /* your additional rule overrides */
41
+ },
42
+ },
43
+ // Ignore patterns
44
+ {
45
+ ignores: [
46
+ /* your ignore file patterns */
47
+ ],
48
+ },
49
+ ]);
50
+ ```
51
+
52
+ ## Custom Rules Plugin
53
+
54
+ This package also offers a custom rules plugin, which can be used to add custom rules to your project.
55
+
56
+ Currently, there's only one rule: `@hayuno-ag/no-speculative-it`, defined at `rules/no-speculative-it.ts`.
57
+
58
+ ### Defining additional rules
59
+
60
+ Taking `no-speculative-it.js` as template, you can easily define additional rules by just adding another
61
+ js-file implementing the rule and storing it at `rules/custom`. `rules/custom/index.js` will then automatically
62
+ load all rules from this directory and `index.mjs` will export them as long as `jest` is installed as
63
+ dependency in the consuming project.
64
+
65
+ By default, all newly-created rules are set to `error` severity. If you want to change this, you can
66
+ simply set the `meta.severity` property in the rule definition as shown in `no-speculative-it.js`.
67
+
68
+ As shown in [Integration](#integration) above, you can change a rule's severity level by adding it to the `rules`
69
+ property of your `eslint.config.mjs`. If you don't want to override a rule's severity level, you don't have
70
+ to add it to the according `rules` property. It then is executed with its default severity level.
71
+
72
+ The names of the rules defined in this project are prefixed with `@hayuno-ag/`. Therefore, `no-speculative-it`
73
+ is referenced by `@hayuno-ag/no-speculative-it`.
74
+
75
+ ### `@hayuno-ag/no-speculative-it`
76
+
77
+ This rule is used to prevent speculative `it` statements and enforces the usage of imperative statements instead.
78
+
79
+ Example: Instead of `it('should restart the universe from the beginning')` the imperative statement
80
+ `it('restarts the universe from the beginning')` should be used.
81
+
82
+ Default severity: `error`.
83
+
84
+ ## Additional information
85
+
86
+ This package requires the following peer dependencies to be installed in your project:
87
+
88
+ - `eslint` (>= 9.33.0)
89
+ - `typescript` (>= 4.0.0)
90
+ - `@typescript-eslint/parser` (^8.37.0)
91
+ - `eslint-config-prettier` (^10.1.8)
92
+ - `prettier` (^3.6.2)
93
+ - `jest` (^30.2.0) - only required if you want to use the Jest-related rules
94
+
95
+ All ESLint plugins (such as `eslint-plugin-react`, `eslint-plugin-react-hooks`, `eslint-plugin-jsx-a11y`,
96
+ etc.) are included as regular dependencies in this package, so you don't need to install them separately.
package/index.mjs ADDED
@@ -0,0 +1,72 @@
1
+ import { createRequire } from 'node:module';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { FlatCompat } from '@eslint/eslintrc';
5
+ import js from '@eslint/js';
6
+ import typescriptEslint from '@typescript-eslint/eslint-plugin';
7
+ import importPlugin from 'eslint-plugin-import';
8
+ import jestPlugin from 'eslint-plugin-jest';
9
+ import reactPlugin from 'eslint-plugin-react';
10
+ import reactHooksPlugin from 'eslint-plugin-react-hooks';
11
+ import testingLibraryPlugin from 'eslint-plugin-testing-library';
12
+ import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
13
+ import nextPlugin from '@next/eslint-plugin-next';
14
+ import prettierConfig from 'eslint-config-prettier';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = path.dirname(__filename);
19
+
20
+ const moduleExists = (module) => {
21
+ try {
22
+ require.resolve(module, {
23
+ paths: [process.cwd(), path.join(process.cwd(), 'node_modules')],
24
+ });
25
+ return true;
26
+ } catch (error) {
27
+ if (error && error.code === 'MODULE_NOT_FOUND') return false;
28
+ throw error;
29
+ }
30
+ };
31
+
32
+ const compat = new FlatCompat({
33
+ baseDirectory: __dirname,
34
+ recommendedConfig: js.configs.recommended,
35
+ allConfig: js.configs.all,
36
+ });
37
+
38
+ const legacyRuleConfigs = ['./rules/base'];
39
+ if (moduleExists('next')) legacyRuleConfigs.push('./rules/next');
40
+ if (moduleExists('react')) legacyRuleConfigs.push('./rules/react');
41
+ if (moduleExists('jest')) legacyRuleConfigs.push('./rules/jest');
42
+ if (moduleExists('typescript')) legacyRuleConfigs.push('./rules/typescript');
43
+ legacyRuleConfigs.push('./rules/prettier');
44
+
45
+ const baseConfigs = compat.extends(...legacyRuleConfigs.map(require.resolve));
46
+
47
+ const globalPlugins = [
48
+ {
49
+ plugins: {
50
+ '@typescript-eslint': typescriptEslint,
51
+ import: importPlugin,
52
+ jest: jestPlugin,
53
+ react: reactPlugin,
54
+ 'react-hooks': reactHooksPlugin,
55
+ 'jsx-a11y': jsxA11yPlugin,
56
+ '@next/next': nextPlugin,
57
+ 'testing-library': testingLibraryPlugin,
58
+ },
59
+ },
60
+ ];
61
+
62
+ const customPlugin = require('./rules/custom');
63
+ const customRules = {
64
+ plugins: {
65
+ '@hayuno-ag': customPlugin,
66
+ },
67
+ rules: Object.fromEntries(
68
+ Object.entries(customPlugin.rules).map(([name, rule]) => [`@hayuno-ag/${name}`, rule.meta?.severity || 'error']),
69
+ ),
70
+ };
71
+
72
+ export default [...globalPlugins, ...baseConfigs, customRules, prettierConfig];
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@hayuno/eslint-config",
3
+ "version": "1.0.5",
4
+ "description": "ESLint configuration used for projects at Hayuno AG",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "keywords": [
9
+ "eslint",
10
+ "eslintconfig",
11
+ "typescript",
12
+ "prettier"
13
+ ],
14
+ "homepage": "https://gitlab.com/hayuno/eslint-config#hayuno-ageslint-config",
15
+ "bugs": {
16
+ "url": "https://gitlab.com/hayuno/eslint-config/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+ssh://git@gitlab.com/hayuno/eslint-config.git"
21
+ },
22
+ "license": "MIT",
23
+ "author": "Hayuno AG",
24
+ "type": "commonjs",
25
+ "main": "index.mjs",
26
+ "files": [
27
+ "*.js",
28
+ "*.mjs",
29
+ "rules"
30
+ ],
31
+ "scripts": {
32
+ "format": "prettier --check .",
33
+ "format:fix": "prettier --write .",
34
+ "prepare": "husky"
35
+ },
36
+ "dependencies": {
37
+ "@next/eslint-plugin-next": ">=15.0.0",
38
+ "@typescript-eslint/eslint-plugin": "^8.37.0",
39
+ "eslint-plugin-import": "^2.32.0",
40
+ "eslint-plugin-jest": "^29.0.1",
41
+ "eslint-plugin-jest-dom": "^5.5.0",
42
+ "eslint-plugin-jsx-a11y": "^6.10.2",
43
+ "eslint-plugin-prettier": "^5.5.4",
44
+ "eslint-plugin-react": "^7.37.5",
45
+ "eslint-plugin-react-hooks": "^5.2.0",
46
+ "eslint-plugin-testing-library": "^7.15.1"
47
+ },
48
+ "devDependencies": {
49
+ "husky": "^9.1.7",
50
+ "prettier": "^3.6.2"
51
+ },
52
+ "peerDependencies": {
53
+ "@typescript-eslint/parser": "^8.37.0",
54
+ "eslint": ">=9.33.0",
55
+ "eslint-config-prettier": "^10.1.8",
56
+ "jest": "^30.2.0",
57
+ "prettier": "^3.6.2",
58
+ "typescript": ">=4.0.0"
59
+ },
60
+ "engines": {
61
+ "node": ">=22.14.0"
62
+ }
63
+ }
package/rules/base.js ADDED
@@ -0,0 +1,41 @@
1
+ module.exports = {
2
+ ignorePatterns: ['!.*'],
3
+ extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
4
+ plugins: ['@typescript-eslint'],
5
+ env: {
6
+ browser: true,
7
+ node: true,
8
+ es2022: true,
9
+ },
10
+ parser: '@typescript-eslint/parser',
11
+ parserOptions: {
12
+ ecmaVersion: 2022,
13
+ sourceType: 'module',
14
+ },
15
+ reportUnusedDisableDirectives: true,
16
+ rules: {
17
+ 'import/extensions': ['error', 'ignorePackages', { js: 'never', '': 'never' }],
18
+ 'import/no-cycle': 'off',
19
+ 'import/no-extraneous-dependencies': [
20
+ 'error',
21
+ {
22
+ devDependencies: ['**/*.test.ts'],
23
+ },
24
+ ],
25
+ 'import/no-useless-path-segments': ['error', { noUselessIndex: true }],
26
+ 'import/order': ['error', { 'newlines-between': 'never' }],
27
+ 'import/prefer-default-export': 'off',
28
+ 'arrow-body-style': 'error',
29
+ 'max-classes-per-file': 'off',
30
+ 'max-len': ['error', { code: 120 }],
31
+ 'no-console': 'error',
32
+ 'no-empty-function': ['error', { allow: ['arrowFunctions'] }],
33
+ 'no-use-before-define': ['error', { functions: false }],
34
+ 'prefer-arrow-callback': 'error',
35
+ },
36
+ overrides: [
37
+ {
38
+ files: ['scripts/**', 'bin/**'],
39
+ },
40
+ ],
41
+ };
@@ -0,0 +1,18 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const customDir = __dirname;
5
+ const files = fs.readdirSync(customDir);
6
+
7
+ const ruleFiles = files.filter((file) => file.endsWith('.js') && file !== 'index.js');
8
+
9
+ const rules = {};
10
+ ruleFiles.forEach((file) => {
11
+ const filePath = path.join(customDir, file);
12
+ const ruleModule = require(filePath);
13
+ Object.assign(rules, ruleModule);
14
+ });
15
+
16
+ module.exports = {
17
+ rules,
18
+ };
@@ -0,0 +1,57 @@
1
+ module.exports = {
2
+ 'no-speculative-it': {
3
+ meta: {
4
+ type: 'suggestion',
5
+ docs: {
6
+ description: 'Enforce imperative mood in it-descriptions instead of "should"',
7
+ category: 'Best Practices',
8
+ recommended: false,
9
+ },
10
+ fixable: null,
11
+ schema: [],
12
+ messages: {
13
+ noShould: 'Prefer using imperative mood (e.g. "{{suggestion}}") instead of "should {{action}}"',
14
+ },
15
+ severity: 'error',
16
+ },
17
+ create(context) {
18
+ const shouldPattern = /^should\s+(.+)/i;
19
+
20
+ return {
21
+ CallExpression(node) {
22
+ if (node.callee.type === 'Identifier' && node.callee.name === 'it' && node.arguments.length > 0) {
23
+ const firstArg = node.arguments[0];
24
+
25
+ if (firstArg.type === 'Literal' && typeof firstArg.value === 'string') {
26
+ const match = firstArg.value.match(shouldPattern);
27
+
28
+ if (match) {
29
+ const action = match[1];
30
+ const suggestion = action.replace(/^(\w+)/, (word) => {
31
+ switch (word) {
32
+ case 'not':
33
+ return 'does not';
34
+ case 'be':
35
+ return 'is/has';
36
+ }
37
+ if (word.endsWith('y')) return word.slice(0, -1) + 'ies';
38
+ if (word.endsWith('ch')) return word + 'es';
39
+ return word + 's';
40
+ });
41
+
42
+ context.report({
43
+ node: firstArg,
44
+ messageId: 'noShould',
45
+ data: {
46
+ action,
47
+ suggestion,
48
+ },
49
+ });
50
+ }
51
+ }
52
+ }
53
+ },
54
+ };
55
+ },
56
+ },
57
+ };
package/rules/jest.js ADDED
@@ -0,0 +1,13 @@
1
+ module.exports = {
2
+ overrides: [
3
+ {
4
+ files: ['*.test.*'],
5
+ extends: ['plugin:jest/recommended', 'plugin:jest-dom/recommended'],
6
+ env: { 'jest/globals': true },
7
+ rules: {
8
+ 'require-await': 'error',
9
+ 'jest/consistent-test-it': ['error', { fn: 'it', withinDescribe: 'it' }],
10
+ },
11
+ },
12
+ ],
13
+ };
package/rules/next.js ADDED
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ extends: ['plugin:@next/next/recommended', 'plugin:@next/next/core-web-vitals'],
3
+ parser: '@typescript-eslint/parser',
4
+ };
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ extends: ['plugin:prettier/recommended'],
3
+ plugins: ['prettier'],
4
+ rules: {
5
+ 'prettier/prettier': 'error',
6
+ },
7
+ };
package/rules/react.js ADDED
@@ -0,0 +1,58 @@
1
+ module.exports = {
2
+ extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended', 'plugin:jsx-a11y/recommended'],
3
+ rules: {
4
+ 'react/default-props-match-prop-types': 'off',
5
+ 'react/destructuring-assignment': ['error', 'always', { ignoreClassFields: true }],
6
+ 'react/function-component-definition': [
7
+ 'error',
8
+ {
9
+ unnamedComponents: 'arrow-function',
10
+ namedComponents: ['arrow-function', 'function-declaration'],
11
+ },
12
+ ],
13
+ 'react/jsx-filename-extension': ['error', { extensions: ['.js', '.jsx'] }],
14
+ 'react/jsx-props-no-spreading': 'off',
15
+ 'react/no-unused-prop-types': 'off',
16
+ 'react/prop-types': 'off',
17
+ 'react/react-in-jsx-scope': 'off',
18
+ 'react/require-default-props': 'off',
19
+ 'react/state-in-constructor': 'off',
20
+ 'import/order': [
21
+ 'error',
22
+ {
23
+ pathGroups: [{ pattern: 'react', group: 'external', position: 'before' }],
24
+ pathGroupsExcludedImportTypes: ['react'],
25
+ 'newlines-between': 'never',
26
+ },
27
+ ],
28
+ 'jsx-a11y/anchor-is-valid': [
29
+ 'error',
30
+ {
31
+ specialLink: ['hrefLeft', 'hrefRight', 'to'],
32
+ aspects: ['noHref', 'invalidHref', 'preferButton'],
33
+ },
34
+ ],
35
+ 'no-restricted-globals': ['error', { name: 'React', message: 'Prefer importing from "react".' }],
36
+ 'no-restricted-imports': [
37
+ 'error',
38
+ {
39
+ name: 'react',
40
+ importNames: ['default'],
41
+ message: 'Prefer using named exports.',
42
+ },
43
+ {
44
+ name: 'react',
45
+ importNames: ['FC'],
46
+ message: 'Prefer type inference for component return types.',
47
+ },
48
+ ],
49
+ },
50
+ overrides: [
51
+ {
52
+ files: ['*.ts', '*.tsx'],
53
+ rules: {
54
+ 'react/jsx-filename-extension': ['error', { extensions: ['.tsx'] }],
55
+ },
56
+ },
57
+ ],
58
+ };
@@ -0,0 +1,80 @@
1
+ module.exports = {
2
+ settings: {
3
+ react: {
4
+ version: 'detect',
5
+ },
6
+ 'import/extensions': ['.ts', '.tsx'],
7
+ 'import/parsers': {
8
+ '@typescript-eslint/parser': ['.ts', '.tsx'],
9
+ },
10
+ 'import/resolver': {
11
+ node: {
12
+ extensions: ['.ts', '.tsx'],
13
+ moduleDirectory: ['node_modules', 'src/'],
14
+ },
15
+ },
16
+ },
17
+ rules: {
18
+ 'import/extensions': [
19
+ 'error',
20
+ 'ignorePackages',
21
+ {
22
+ js: 'never',
23
+ ts: 'never',
24
+ tsx: 'never',
25
+ '': 'never',
26
+ },
27
+ ],
28
+ },
29
+ overrides: [
30
+ {
31
+ files: ['*.ts', '*.tsx'],
32
+ parser: '@typescript-eslint/parser',
33
+ plugins: ['@typescript-eslint'],
34
+ extends: [
35
+ 'plugin:@typescript-eslint/recommended',
36
+ 'plugin:@typescript-eslint/recommended-requiring-type-checking',
37
+ ],
38
+ parserOptions: {
39
+ project: ['./tsconfig.json', './**/tsconfig.json'],
40
+ createDefaultProgram: true,
41
+ ecmaVersion: 2022,
42
+ sourceType: 'module',
43
+ },
44
+ rules: {
45
+ '@typescript-eslint/consistent-type-imports': 'error',
46
+ '@typescript-eslint/default-param-last': 'error',
47
+ '@typescript-eslint/explicit-function-return-type': 'off',
48
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
49
+ '@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }],
50
+ '@typescript-eslint/no-explicit-any': 'error',
51
+ '@typescript-eslint/no-unused-vars': [
52
+ 'error',
53
+ {
54
+ args: 'all',
55
+ argsIgnorePattern: '^_',
56
+ caughtErrors: 'all',
57
+ caughtErrorsIgnorePattern: '^_',
58
+ destructuredArrayIgnorePattern: '^_',
59
+ varsIgnorePattern: '^_',
60
+ ignoreRestSiblings: true,
61
+ },
62
+ ],
63
+ '@typescript-eslint/no-use-before-define': ['error', { functions: false }],
64
+ '@typescript-eslint/no-useless-constructor': 'error',
65
+ '@typescript-eslint/switch-exhaustiveness-check': 'error',
66
+ 'import/no-unresolved': 'off',
67
+ 'consistent-return': 'off',
68
+ 'default-case': 'off',
69
+ 'default-param-last': 'off',
70
+ 'no-empty-function': 'off',
71
+ 'no-restricted-syntax': [
72
+ 'error',
73
+ { selector: 'TSEnumDeclaration', message: 'Prefer types over enums because enums are not type-safe.' },
74
+ ],
75
+ 'no-use-before-define': 'off',
76
+ 'no-useless-constructor': 'off',
77
+ },
78
+ },
79
+ ],
80
+ };