@smartive/eslint-config 7.1.0-next.2 → 8.0.0-next.1

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/README.md CHANGED
@@ -8,6 +8,8 @@ This package provides smartive's default [eslint](https://eslint.org/) configura
8
8
  $ npm install eslint @smartive/eslint-config -D
9
9
  ```
10
10
 
11
+ Requires **ESLint 10.0.0 or newer** — see [ESLint 10 is required](#eslint-10-is-required).
12
+
11
13
  ## Usage
12
14
 
13
15
  This package offers three different rule sets, one for plain TypeScript applications, a separate one for React applications and one that works well with Next.js applications (minimum supported version is Next.js v16).
@@ -39,3 +41,79 @@ To use eslint add the following to your package.json:
39
41
  "lint:fix": "eslint {your source directory} --fix"
40
42
  }
41
43
  ```
44
+
45
+ ## Included plugins
46
+
47
+ - [`typescript-eslint`](https://typescript-eslint.io/)
48
+ - [`eslint-plugin-import-x`](https://github.com/un-ts/eslint-plugin-import-x) (`typescript` and `react` rule sets)
49
+ - [`@eslint-react/eslint-plugin`](https://eslint-react.xyz/) (`react` and `nextjs` rule sets)
50
+ - [`eslint-plugin-prettier`](https://github.com/prettier/eslint-plugin-prettier)
51
+
52
+ `eslint-plugin-react`, `eslint-plugin-react-hooks` and `eslint-plugin-import` are no longer used.
53
+
54
+ ### ESLint 10 is required
55
+
56
+ `@eslint-react/eslint-plugin` dropped ESLint 9 in its v3.0.0, which declares a `^10.0.0` peer range. Note
57
+ that its `peerDependencies` say `eslint: "*"`, so npm will not warn you about an older ESLint — it will
58
+ simply misbehave at some point.
59
+
60
+ ### `eslint-config-next` and ESLint 10
61
+
62
+ `eslint-config-next` bundles `eslint-plugin-react`, which supports ESLint 9 at most — its peer range ends
63
+ at `^9.7`, and on ESLint 10 every one of its rules throws while loading, because React version detection
64
+ uses the `context.getFilename()` API that ESLint 10 removed. The same config parses plain JavaScript with
65
+ the Babel parser Next.js bundles, and that parser calls `scopeManager.addGlobals()`, also removed in
66
+ ESLint 10, so linting any `.js` file throws as well
67
+ ([vercel/next.js#89764](https://github.com/vercel/next.js/issues/89764), still open — there is no fix in
68
+ `eslint-config-next@canary` either).
69
+
70
+ The `nextjs` rule set therefore:
71
+
72
+ - turns off every rule `eslint-config-next` enables that comes from a plugin replaced here —
73
+ `eslint-plugin-react` (38 rules), `eslint-plugin-react-hooks` (16) and `eslint-plugin-import` (1). The
74
+ list is derived from the loaded config rather than hard-coded, so it tracks upstream changes.
75
+ - restores the one import rule from `eslint-plugin-import-x`, so no coverage is lost.
76
+ - pins `settings.react.version`, so re-enabling any of those rules in a consuming project degrades to a
77
+ normal lint result instead of a crash.
78
+ - parses `.js`, `.jsx` and `.mjs` with the typescript-eslint parser instead of the Babel one.
79
+
80
+ `jsx-a11y` (6 rules) and `@next/next` (22 rules) are left untouched — nothing here replaces them. Both
81
+ declare peer ranges ending at ESLint 9, but both work on ESLint 10.
82
+
83
+ ESLint React covers most of what is turned off. What is genuinely lost:
84
+
85
+ | Rule | Replacement |
86
+ | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
87
+ | `import/no-anonymous-default-export` | `import-x/no-anonymous-default-export`, enabled in its place |
88
+ | `react/jsx-no-duplicate-props` | TypeScript reports this as `TS17001` |
89
+ | `react/jsx-uses-react`, `react/jsx-uses-vars`, `react/jsx-no-undef` | redundant — ESLint 10 tracks JSX references natively |
90
+ | `react/no-is-mounted`, `react/require-render-return` | class-component patterns, not relevant to modern React |
91
+ | `react-hooks/config`, `react-hooks/gating`, `react-hooks/incompatible-library`, `react-hooks/preserve-manual-memoization` | none — ESLint React does not implement these React Compiler rules |
92
+ | `react/no-unescaped-entities` | none |
93
+
94
+ ## Custom rules
95
+
96
+ ### `smartive/forbid-component-props`
97
+
98
+ Forbids the given props on components (`<Foo />`, `<Foo.Bar />`) while leaving intrinsic elements
99
+ (`<div />`) alone. It replaces `react/forbid-component-props`, which has no equivalent in ESLint React.
100
+ Enabled in the `react` and `nextjs` rule sets as:
101
+
102
+ ```javascript
103
+ 'smartive/forbid-component-props': ['warn', { forbid: ['style', 'className'] }]
104
+ ```
105
+
106
+ ## Development
107
+
108
+ ```sh
109
+ $ npm run check-types # type-check the config sources
110
+ $ npm run prettier # formatting
111
+ $ npm test # build, type-check the tests, then run them
112
+ ```
113
+
114
+ The tests in `test/` lint the fixtures in `test/fixtures/` with each of the three rule sets and assert that
115
+ a given fixture line is flagged. They deliberately never assert on rule ids, so that swapping out a plugin
116
+ for an equivalent one does not require rewriting them.
117
+
118
+ > `npm test` runs the TypeScript test files directly through Node's type stripping, which needs Node
119
+ > 22.18 or newer.
package/dist/configs.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export declare const flatConfigTypescript: (rulesOnly?: boolean) => import("eslint/config").Config[];
2
- export declare const flatConfigReact: () => import("eslint/config").Config[];
3
- export declare const flatConfigNext: () => import("eslint/config").Config[];
1
+ export declare const flatConfigTypescript: (rulesOnly?: boolean) => import("eslint/config").ConfigObject[];
2
+ export declare const flatConfigReact: () => import("eslint/config").ConfigObject[];
3
+ export declare const flatConfigNext: () => import("eslint/config").ConfigObject[];
package/dist/configs.js CHANGED
@@ -1,13 +1,13 @@
1
- import { fixupConfigRules, fixupPluginRules } from '@eslint/compat';
1
+ import eslintReact from '@eslint-react/eslint-plugin';
2
2
  import js from '@eslint/js';
3
- import { flatConfigs as eslintPluginImportConfigs } from 'eslint-plugin-import';
3
+ import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript';
4
+ import { createNodeResolver, importX } from 'eslint-plugin-import-x';
4
5
  import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
5
- import reactPlugin from 'eslint-plugin-react';
6
- import reactHooks from 'eslint-plugin-react-hooks';
7
6
  import { defineConfig } from 'eslint/config';
8
7
  import globals from 'globals';
9
8
  import { createRequire } from 'node:module';
10
9
  import tsEslint from 'typescript-eslint';
10
+ import { smartivePlugin } from './plugin/index.js';
11
11
  import { defaultRules, prettierRules, reactRules, typescriptRules } from './rules.js';
12
12
  const tsEslintConfigs = [...tsEslint.configs.recommendedTypeChecked, ...tsEslint.configs.stylisticTypeChecked];
13
13
  const baseConfig = {
@@ -30,57 +30,128 @@ const baseConfig = {
30
30
  },
31
31
  },
32
32
  };
33
- const reactConfig = { name: '@smartive/eslint-config/react', rules: reactRules };
33
+ /**
34
+ * Type-aware rules cannot run on plain JavaScript.
35
+ *
36
+ * This has to come *after* every block that switches such a rule on — `typescriptRules` in
37
+ * `baseConfig` and `reactRules` both do — because in flat config the later block wins. Applied too
38
+ * early, ESLint fails with "You have used a rule which requires type information" on `.js` files.
39
+ */
40
+ const jsDisableTypeChecked = {
41
+ name: '@smartive/eslint-config/js-disable-type-checked',
42
+ files: ['**/*.js', '**/*.mjs'],
43
+ rules: tsEslint.configs.disableTypeChecked.rules,
44
+ };
45
+ /** The same, for the type-aware rules ESLint React contributes. Applied last, for the same reason. */
46
+ const jsDisableTypeCheckedReact = {
47
+ name: '@smartive/eslint-config/js-disable-type-checked-react',
48
+ files: ['**/*.js', '**/*.mjs'],
49
+ rules: eslintReact.configs['disable-type-checked'].rules,
50
+ };
51
+ const reactConfig = {
52
+ name: '@smartive/eslint-config/react',
53
+ plugins: { smartive: smartivePlugin },
54
+ rules: reactRules,
55
+ };
34
56
  export const flatConfigTypescript = (rulesOnly = false) => defineConfig([
35
57
  js.configs.recommended,
36
58
  eslintPluginPrettierRecommended,
37
59
  ...(rulesOnly
38
60
  ? [
39
61
  {
62
+ // `eslint-config-next` registers this plugin, but only for `**/*.ts` and `**/*.tsx`. Without
63
+ // registering it here, every `@typescript-eslint/*` rule reference — these, plus the ones in
64
+ // `typescriptRules` and `reactRules` — makes ESLint fail with `Could not find plugin` the
65
+ // moment a `.js` file is linted. Registering it twice is harmless: it is the same instance,
66
+ // since npm dedupes `typescript-eslint` between this package and `eslint-config-next`.
67
+ plugins: { '@typescript-eslint': tsEslint.plugin },
40
68
  rules: tsEslintConfigs.reduce((combinedRules, { rules }) => ({ ...combinedRules, ...(rules ? rules : {}) }), {}),
41
69
  },
42
70
  ]
43
71
  : [
44
- fixupConfigRules(eslintPluginImportConfigs.errors),
45
- fixupConfigRules(eslintPluginImportConfigs.warnings),
46
- fixupConfigRules(eslintPluginImportConfigs.typescript),
72
+ importX.flatConfigs.errors,
73
+ importX.flatConfigs.warnings,
74
+ importX.flatConfigs.typescript,
47
75
  {
48
76
  settings: {
49
- 'import/resolver': {
50
- node: true,
51
- typescript: {
52
- alwaysTryTypes: true,
53
- },
54
- },
77
+ 'import-x/resolver-next': [createTypeScriptImportResolver({ alwaysTryTypes: true }), createNodeResolver()],
55
78
  },
56
79
  },
57
80
  ...tsEslint.configs.recommendedTypeChecked,
58
81
  ...tsEslint.configs.stylisticTypeChecked,
59
82
  ]),
60
- {
61
- files: ['**/*.js', '**/*.mjs'],
62
- extends: [tsEslint.configs.disableTypeChecked],
63
- },
64
83
  baseConfig,
84
+ jsDisableTypeChecked,
85
+ ]);
86
+ export const flatConfigReact = () => defineConfig([
87
+ ...flatConfigTypescript(),
88
+ eslintReact.configs['recommended-type-checked'],
89
+ reactConfig,
90
+ jsDisableTypeChecked,
91
+ jsDisableTypeCheckedReact,
65
92
  ]);
66
- export const flatConfigReact = () => {
67
- const reactPluginFixed = fixupPluginRules(reactPlugin);
93
+ /**
94
+ * Prefixes of every plugin `eslint-config-next` bundles that this config replaces.
95
+ *
96
+ * `jsx-a11y` and `@next/next` are deliberately absent — nothing here replaces those, so their rules stay
97
+ * on. Both declare a peer range ending at ESLint 9 but work on ESLint 10.
98
+ */
99
+ const replacedNextRulePrefixes = ['react/', 'react-hooks/', 'import/'];
100
+ /**
101
+ * Every rule `eslint-config-next` switches on that comes from a plugin this config replaces, turned off.
102
+ *
103
+ * `eslint-config-next` bundles `eslint-plugin-react`, `eslint-plugin-react-hooks` and
104
+ * `eslint-plugin-import`; ESLint React and `eslint-plugin-import-x` take their place here. Leaving the
105
+ * bundled ones on means duplicate reports, and for `eslint-plugin-react` a hard failure: its peer range
106
+ * ends at `^9.7`, and on ESLint 10 every one of its rules throws while loading, because React version
107
+ * detection calls the `context.getFilename()` API that ESLint 10 removed.
108
+ *
109
+ * Derived from the loaded config rather than hard-coded, so it keeps up with `eslint-config-next`.
110
+ *
111
+ * @see https://github.com/vercel/next.js/issues/89764
112
+ */
113
+ const disableReplacedNextRules = (configs) => Object.fromEntries(configs
114
+ .flatMap((nextConfig) => Object.keys(nextConfig.rules ?? {}))
115
+ .filter((rule) => replacedNextRulePrefixes.some((prefix) => rule.startsWith(prefix)))
116
+ .map((rule) => [rule, 'off']));
117
+ export const flatConfigNext = () => {
118
+ const nextConfigs = createRequire(import.meta.url)('eslint-config-next/core-web-vitals');
68
119
  return defineConfig([
69
- ...flatConfigTypescript(),
120
+ ...nextConfigs,
121
+ ...flatConfigTypescript(true),
122
+ eslintReact.configs['recommended-type-checked'],
70
123
  {
71
- ...reactPlugin.configs.flat.recommended,
72
- plugins: { react: reactPluginFixed },
124
+ name: '@smartive/eslint-config/next-compat',
125
+ rules: disableReplacedNextRules(nextConfigs),
126
+ // Belt and braces: `eslint-config-next` sets `react.version: 'detect'`, and the bundled
127
+ // `eslint-plugin-react` implements detection via `context.getFilename()`, which ESLint 10
128
+ // removed. Pinning the version skips that path, so re-enabling one of the rules above in a
129
+ // consuming project degrades to a normal lint result instead of a crash.
130
+ settings: { react: { version: '19.0' } },
73
131
  },
74
132
  {
75
- ...reactPlugin.configs.flat['jsx-runtime'],
76
- plugins: { react: reactPluginFixed },
133
+ name: '@smartive/eslint-config/next-import-x',
134
+ // `import/no-anonymous-default-export` is the only import rule `eslint-config-next` switches on,
135
+ // and it was just turned off with the rest of `eslint-plugin-import`. Restore it from
136
+ // `eslint-plugin-import-x` so the Next.js rule set keeps the coverage.
137
+ plugins: importX.flatConfigs.errors.plugins,
138
+ rules: { 'import-x/no-anonymous-default-export': 'warn' },
139
+ },
140
+ {
141
+ name: '@smartive/eslint-config/next-js-parser',
142
+ files: ['**/*.js', '**/*.jsx', '**/*.mjs'],
143
+ // `eslint-config-next` parses plain JavaScript with the Babel parser Next.js bundles, and that
144
+ // parser calls `scopeManager.addGlobals()`, which ESLint 10 removed — so linting any `.js` file
145
+ // throws. The typescript-eslint parser handles plain JavaScript including JSX, and is ESLint 10
146
+ // compatible, so it takes over for those files.
147
+ // https://github.com/vercel/next.js/issues/89764
148
+ languageOptions: {
149
+ parser: tsEslint.parser,
150
+ parserOptions: { ecmaFeatures: { jsx: true } },
151
+ },
77
152
  },
78
- reactHooks.configs.flat.recommended,
79
153
  reactConfig,
154
+ jsDisableTypeChecked,
155
+ jsDisableTypeCheckedReact,
80
156
  ]);
81
157
  };
82
- export const flatConfigNext = () => defineConfig([
83
- ...createRequire(import.meta.url)('eslint-config-next/core-web-vitals'),
84
- ...flatConfigTypescript(true),
85
- reactConfig,
86
- ]);
@@ -0,0 +1,6 @@
1
+ import type { Rule } from 'eslint';
2
+ /**
3
+ * Replacement for `react/forbid-component-props`, which has no equivalent in
4
+ * `@eslint-react/eslint-plugin` (https://eslint-react.xyz/docs/migrating-from-eslint-plugin-react).
5
+ */
6
+ export declare const forbidComponentProps: Rule.RuleModule;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Distinguishes components (`<Foo />`, `<Foo.Bar />`) from intrinsic elements (`<div />`, `<svg:rect />`).
3
+ */
4
+ const isComponent = (name) => {
5
+ switch (name.type) {
6
+ case 'JSXIdentifier':
7
+ return /^[A-Z]/.test(name.name);
8
+ case 'JSXMemberExpression':
9
+ return true;
10
+ default:
11
+ return false;
12
+ }
13
+ };
14
+ /**
15
+ * Replacement for `react/forbid-component-props`, which has no equivalent in
16
+ * `@eslint-react/eslint-plugin` (https://eslint-react.xyz/docs/migrating-from-eslint-plugin-react).
17
+ */
18
+ export const forbidComponentProps = {
19
+ meta: {
20
+ type: 'suggestion',
21
+ docs: {
22
+ description: 'Forbid certain props on components.',
23
+ url: 'https://github.com/smartive/eslint-config#smartiveforbid-component-props',
24
+ },
25
+ schema: [
26
+ {
27
+ type: 'object',
28
+ properties: {
29
+ forbid: {
30
+ type: 'array',
31
+ items: { type: 'string' },
32
+ uniqueItems: true,
33
+ },
34
+ },
35
+ additionalProperties: false,
36
+ },
37
+ ],
38
+ messages: {
39
+ forbiddenProp: 'Prop "{{prop}}" is forbidden on components.',
40
+ },
41
+ },
42
+ create(context) {
43
+ const { forbid = [] } = (context.options[0] ?? {});
44
+ const forbidden = new Set(forbid);
45
+ if (forbidden.size === 0) {
46
+ return {};
47
+ }
48
+ return {
49
+ JSXOpeningElement(node) {
50
+ const element = node;
51
+ if (!isComponent(element.name)) {
52
+ return;
53
+ }
54
+ for (const attribute of element.attributes) {
55
+ if (attribute.type !== 'JSXAttribute' || attribute.name.type !== 'JSXIdentifier') {
56
+ continue;
57
+ }
58
+ if (forbidden.has(attribute.name.name)) {
59
+ context.report({
60
+ node: attribute,
61
+ messageId: 'forbiddenProp',
62
+ data: { prop: attribute.name.name },
63
+ });
64
+ }
65
+ }
66
+ },
67
+ };
68
+ },
69
+ };
@@ -0,0 +1,2 @@
1
+ import type { ESLint } from 'eslint';
2
+ export declare const smartivePlugin: ESLint.Plugin;
@@ -0,0 +1,7 @@
1
+ import { forbidComponentProps } from './forbid-component-props.js';
2
+ export const smartivePlugin = {
3
+ meta: { name: '@smartive/eslint-config' },
4
+ rules: {
5
+ 'forbid-component-props': forbidComponentProps,
6
+ },
7
+ };
package/dist/rules.js CHANGED
@@ -24,9 +24,7 @@ export const typescriptRules = {
24
24
  '@typescript-eslint/no-explicit-any': ['error', { fixToUnknown: true }],
25
25
  };
26
26
  export const reactRules = {
27
- 'react/prop-types': 'off',
28
- 'react/display-name': 'off',
29
- 'react/forbid-component-props': ['warn', { forbid: ['style', 'className'] }],
27
+ 'smartive/forbid-component-props': ['warn', { forbid: ['style', 'className'] }],
30
28
  '@typescript-eslint/no-misused-promises': [
31
29
  'error',
32
30
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartive/eslint-config",
3
- "version": "7.1.0-next.2",
3
+ "version": "8.0.0-next.1",
4
4
  "description": "ESLint configuration by smartive",
5
5
  "files": [
6
6
  "README.md",
@@ -27,19 +27,18 @@
27
27
  },
28
28
  "homepage": "https://github.com/smartive/eslint-config#readme",
29
29
  "dependencies": {
30
+ "@eslint-react/eslint-plugin": "^5.18.6",
30
31
  "@typescript-eslint/eslint-plugin": "^8.15.0",
31
32
  "@typescript-eslint/parser": "^8.15.0",
32
33
  "eslint-config-prettier": "^10.1.8",
33
34
  "eslint-import-resolver-typescript": "^4.4.4",
34
- "eslint-plugin-import": "^2.31.0",
35
+ "eslint-plugin-import-x": "^4.17.1",
35
36
  "eslint-plugin-prettier": "^5.5.4",
36
- "eslint-plugin-react": "^7.37.5",
37
- "eslint-plugin-react-hooks": "^7.0.1",
38
37
  "globals": "^17.0.0",
39
38
  "typescript-eslint": "^8.15.0"
40
39
  },
41
40
  "peerDependencies": {
42
- "eslint": "^9.22.0 || ^10.0.0",
41
+ "eslint": "^10.0.0",
43
42
  "eslint-config-next": "^16.0.0",
44
43
  "next": "^16.0.0"
45
44
  },
@@ -52,35 +51,41 @@
52
51
  }
53
52
  },
54
53
  "devDependencies": {
55
- "@commitlint/cli": "21.0.0",
56
- "@commitlint/config-conventional": "21.0.0",
57
- "@eslint/compat": "2.1.0",
58
- "@eslint/js": "10.0.1",
54
+ "@commitlint/cli": "21.2.2",
55
+ "@commitlint/config-conventional": "21.2.2",
56
+ "@eslint/js": "9.39.5",
59
57
  "@smartive/prettier-config": "3.1.2",
60
- "@types/node": "24.12.3",
58
+ "@types/node": "25.9.6",
59
+ "@types/react": "19.3.0",
60
+ "@types/react-dom": "19.3.0",
61
61
  "cz-conventional-changelog": "3.3.0",
62
- "eslint": "10.3.0",
62
+ "eslint": "10.9.1",
63
+ "eslint-config-next": "16.3.5",
63
64
  "husky": "9.1.7",
64
- "prettier": "3.8.3",
65
- "typescript": "5.9.3"
66
- },
67
- "overrides": {
68
- "eslint": "$eslint"
65
+ "next": "16.3.5",
66
+ "prettier": "3.9.6",
67
+ "react": "19.3.0",
68
+ "react-dom": "19.3.0",
69
+ "typescript": "6.0.3"
69
70
  },
70
71
  "engines": {
71
- "node": "^20.19.0 || >=22.12"
72
+ "node": "^20.19.0 || ^22.13.0 || >=24"
72
73
  },
73
74
  "scripts": {
74
75
  "tsc": "tsc",
75
- "prettier": "prettier --check .",
76
+ "prettier": "prettier --check . --ignore-path .prettierignore-cli",
76
77
  "check-types": "tsc --noEmit",
77
78
  "build": "rm -rf dist && tsc",
78
79
  "commitlint": "commitlint --edit",
79
- "prepare": "[ ! -f node_modules/.bin/husky ] || husky"
80
+ "prepare": "[ ! -f node_modules/.bin/husky ] || husky",
81
+ "test": "npm run build && tsc --noEmit -p tsconfig.test.json && node --test \"test/**/*.test.ts\""
80
82
  },
81
83
  "config": {
82
84
  "commitizen": {
83
85
  "path": "./node_modules/cz-conventional-changelog"
84
86
  }
87
+ },
88
+ "allowScripts": {
89
+ "unrs-resolver@1.12.2": true
85
90
  }
86
91
  }