eslint-config-agent 3.0.3 → 3.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/package.json CHANGED
@@ -1,15 +1,40 @@
1
1
  {
2
2
  "name": "eslint-config-agent",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
4
4
  "description": "ESLint configuration package with TypeScript support",
5
5
  "main": "index.js",
6
+ "types": "./index.d.ts",
6
7
  "type": "module",
7
8
  "exports": {
8
- ".": "./index.js",
9
- "./ddd": "./exports/ddd.js"
9
+ ".": {
10
+ "types": "./index.d.ts",
11
+ "default": "./index.js"
12
+ },
13
+ "./ddd": {
14
+ "types": "./exports/ddd.d.ts",
15
+ "default": "./exports/ddd.js"
16
+ },
17
+ "./recommended": {
18
+ "types": "./exports/recommended.d.ts",
19
+ "default": "./exports/recommended.js"
20
+ },
21
+ "./recommended-incremental": {
22
+ "types": "./exports/recommended-incremental.d.ts",
23
+ "default": "./exports/recommended-incremental.js"
24
+ },
25
+ "./incremental": {
26
+ "types": "./exports/incremental.d.ts",
27
+ "default": "./exports/incremental.js"
28
+ },
29
+ "./to-warnings": {
30
+ "types": "./exports/to-warnings.d.ts",
31
+ "default": "./exports/to-warnings.js"
32
+ },
33
+ "./package.json": "./package.json"
10
34
  },
11
35
  "files": [
12
36
  "index.js",
37
+ "index.d.ts",
13
38
  "README.md",
14
39
  "CHANGELOG.md",
15
40
  "configs/",
@@ -23,6 +48,14 @@
23
48
  "engines": {
24
49
  "node": ">=20.0.0"
25
50
  },
51
+ "peerDependencies": {
52
+ "typescript": ">=4.8.4"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "typescript": {
56
+ "optional": true
57
+ }
58
+ },
26
59
  "scripts": {
27
60
  "lint": "eslint .",
28
61
  "lint:test": "eslint test/",
@@ -32,6 +65,11 @@
32
65
  "test:invalid": "eslint test/invalid.tsx test/long-function.tsx test/jsx-extension-test.js",
33
66
  "test:hooks": "eslint test/react-hooks-rules.tsx",
34
67
  "test:imports": "eslint test/import-export-rules.ts",
68
+ "test:recommended": "node scripts/test-recommended.js",
69
+ "test:recommended-incremental": "node scripts/test-recommended-incremental.js",
70
+ "test:exports": "node scripts/test-exports.js",
71
+ "test:incremental": "node scripts/test-incremental.js",
72
+ "test:types": "tsc -p test/types/tsconfig.json",
35
73
  "test:edge": "eslint test/edge-cases.tsx",
36
74
  "test:performance": "eslint test/performance-test.tsx",
37
75
  "test:comprehensive": "node scripts/test-runner.js",
@@ -72,39 +110,32 @@
72
110
  "access": "public"
73
111
  },
74
112
  "packageManager": "pnpm@10.18.2",
113
+ "pnpm": {
114
+ "overrides": {
115
+ "semver": ">=7.5.2"
116
+ }
117
+ },
75
118
  "devDependencies": {
76
- "@commitlint/cli": "^20.5.2",
77
- "@commitlint/config-conventional": "^20.5.0",
78
- "@eslint/eslintrc": "^3.3.1",
79
- "@eslint/js": "^9.37.0",
80
- "@release-it/conventional-changelog": "^11.0.0",
81
- "@typescript-eslint/eslint-plugin": "^8.46.1",
82
- "@typescript-eslint/parser": "^8.46.1",
83
- "@typescript-eslint/rule-tester": "^8.59.0",
119
+ "@commitlint/cli": "^20.5.3",
120
+ "@commitlint/config-conventional": "^20.5.3",
121
+ "@release-it/conventional-changelog": "^11.0.1",
122
+ "@typescript-eslint/rule-tester": "^8.60.1",
84
123
  "cspell": "^9.8.0",
85
- "eslint": "^9.37.0",
86
- "eslint-import-resolver-typescript": "^4.4.4",
87
- "eslint-plugin-class-export": "^1.0.1",
88
- "eslint-plugin-import": "^2.32.0",
89
- "eslint-plugin-n": "^17.23.1",
90
- "eslint-plugin-preact": "^0.1.0",
91
- "eslint-plugin-react": "^7.37.5",
92
- "eslint-plugin-react-hooks": "^5.2.0",
93
- "eslint-plugin-security": "^3.0.1",
94
- "eslint-plugin-storybook": "^9.1.10",
95
124
  "husky": "^9.1.7",
96
- "knip": "^6.7.0",
125
+ "knip": "^6.15.0",
97
126
  "lint-staged": "^16.4.0",
98
127
  "prettier": "^3.8.3",
99
- "release-it": "^20.0.1",
100
- "typescript-eslint": "^8.46.1"
128
+ "release-it": "^20.2.0",
129
+ "storybook": "^10.4.2",
130
+ "typescript": "^6.0.3"
101
131
  },
102
132
  "dependencies": {
103
133
  "@eslint/eslintrc": "^3.3.5",
104
134
  "@eslint/js": "^9.39.4",
105
- "@typescript-eslint/eslint-plugin": "^8.59.0",
106
- "@typescript-eslint/parser": "^8.59.0",
135
+ "@typescript-eslint/eslint-plugin": "^8.60.1",
136
+ "@typescript-eslint/parser": "^8.60.1",
107
137
  "eslint": "^9.39.4",
138
+ "eslint-import-resolver-typescript": "^4.4.5",
108
139
  "eslint-plugin-class-export": "^1.0.1",
109
140
  "eslint-plugin-ddd": "^0.5.2",
110
141
  "eslint-plugin-default": "^1.1.0",
@@ -120,9 +151,11 @@
120
151
  "eslint-plugin-required-exports": "^0.2.0",
121
152
  "eslint-plugin-security": "^4.0.0",
122
153
  "eslint-plugin-single-export": "^1.1.2",
123
- "eslint-plugin-storybook": "^10.3.5",
154
+ "eslint-plugin-storybook": "^10.4.2",
124
155
  "eslint-plugin-switch-case": "^4.0.0",
125
- "globals": "^17.5.0",
126
- "typescript-eslint": "^8.59.0"
156
+ "eslint-plugin-unicorn": "^65.0.1",
157
+ "eslint-plugin-unused-imports": "^4.4.1",
158
+ "globals": "^17.6.0",
159
+ "typescript-eslint": "^8.60.1"
127
160
  }
128
161
  }
package/plugins/index.js CHANGED
@@ -18,7 +18,10 @@ import errorPlugin from 'eslint-plugin-error'
18
18
  import defaultPlugin from 'eslint-plugin-default'
19
19
  import dddPlugin from 'eslint-plugin-ddd'
20
20
  import preactPlugin from 'eslint-plugin-preact'
21
+ import unusedImportsPlugin from 'eslint-plugin-unused-imports'
22
+ import unicornPlugin from 'eslint-plugin-unicorn'
21
23
  import { noDefaultClassExportRule } from '../rules/no-default-class-export/index.js'
24
+ import { requireSpecFileTsxRule } from '../rules/require-spec-file-tsx/index.js'
22
25
 
23
26
  // Centralized plugin configuration
24
27
  export const plugins = {
@@ -35,9 +38,12 @@ export const plugins = {
35
38
  default: defaultPlugin,
36
39
  ddd: dddPlugin,
37
40
  preact: preactPlugin,
41
+ 'unused-imports': unusedImportsPlugin,
42
+ unicorn: unicornPlugin,
38
43
  custom: {
39
44
  rules: {
40
45
  'no-default-class-export': noDefaultClassExportRule,
46
+ 'require-spec-file-tsx': requireSpecFileTsxRule,
41
47
  },
42
48
  },
43
49
  }
package/rules/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  // Core rule configurations
9
- import noTrailingSpacesConfig from './no-trailing-spaces/index.js'
9
+ import { noTrailingSpacesConfig } from './no-trailing-spaces/index.js'
10
10
  import {
11
11
  maxFunctionLinesWarning,
12
12
  maxFunctionLinesError,
@@ -17,7 +17,7 @@ import {
17
17
  } from './max-file-lines/index.js'
18
18
 
19
19
  // Custom restricted syntax rules
20
- import { noProcessEnvPropertiesConfig } from './no-process-env-properties/index.js'
20
+ import { noProcessEnvironmentPropertiesConfig } from './no-process-env-properties/index.js'
21
21
  import { noTypeAssertionsConfig } from './no-type-assertions/index.js'
22
22
  import { noExportSpecifiersConfig } from './no-empty-exports/index.js'
23
23
  import { noDefaultClassExportRules } from './no-default-class-export/index.js'
@@ -42,7 +42,7 @@ const allRules = {
42
42
  maxFileLinesError,
43
43
 
44
44
  // Custom restricted syntax rules
45
- noProcessEnvPropertiesConfig,
45
+ noProcessEnvironmentPropertiesConfig,
46
46
  noTypeAssertionsConfig,
47
47
  noExportSpecifiersConfig,
48
48
  noDefaultClassExportRules,
@@ -61,27 +61,25 @@ const allRules = {
61
61
  export default allRules
62
62
 
63
63
  // Named exports for backward compatibility
64
+
65
+ export { noTrailingSpacesConfig } from './no-trailing-spaces/index.js'
64
66
  export {
65
- // Core rule configurations
66
- noTrailingSpacesConfig,
67
67
  maxFunctionLinesWarning,
68
68
  maxFunctionLinesError,
69
+ } from './max-function-lines/index.js'
70
+ export {
69
71
  maxFileLinesWarning,
70
72
  maxFileLinesError,
71
-
72
- // Custom restricted syntax rules
73
- noProcessEnvPropertiesConfig,
74
- noTypeAssertionsConfig,
75
- noExportSpecifiersConfig,
76
- noDefaultClassExportRules,
77
- noNullishCoalescingConfig,
78
- switchStatementsReturnTypeConfigs,
79
- switchCaseFunctionsReturnTypeConfigs,
80
- switchCaseExplicitReturnConfigs,
81
- noTrivialTypeAliasesConfigs,
82
- noInlineUnionTypesConfigs,
83
-
84
- // Plugin rule configurations
85
- pluginRules,
86
- typescriptEslintRules,
87
- }
73
+ } from './max-file-lines/index.js'
74
+ export { noProcessEnvironmentPropertiesConfig } from './no-process-env-properties/index.js'
75
+ export { noTypeAssertionsConfig } from './no-type-assertions/index.js'
76
+ export { noExportSpecifiersConfig } from './no-empty-exports/index.js'
77
+ export { noDefaultClassExportRules } from './no-default-class-export/index.js'
78
+ export { noNullishCoalescingConfig } from './nullish-coalescing/index.js'
79
+ export { switchStatementsReturnTypeConfigs } from './switch-statements-return-type/index.js'
80
+ export { switchCaseFunctionsReturnTypeConfigs } from './switch-case-functions-return-type/index.js'
81
+ export { switchCaseExplicitReturnConfigs } from './switch-case-explicit-return/index.js'
82
+ export { noTrivialTypeAliasesConfigs } from './no-trivial-type-aliases/index.js'
83
+ export { noInlineUnionTypesConfigs } from './no-inline-union-types/index.js'
84
+ export { pluginRules } from './plugin/index.js'
85
+ export { typescriptEslintRules } from './plugin/typescript-eslint/index.js'
@@ -7,7 +7,9 @@
7
7
  * Examples:
8
8
  * - ❌ function foo(status: 'active' | 'inactive') {}
9
9
  * - ❌ interface User { role: 'admin' | 'user'; }
10
+ * - ❌ interface User { id: string | number; }
10
11
  * - ❌ class Config { mode: 'dev' | 'prod'; }
12
+ * - ❌ class Config { value: string | number; }
11
13
  * - ✅ type Status = 'active' | 'inactive'; function foo(status: Status) {}
12
14
  * - ✅ type Role = 'admin' | 'user'; interface User { role: Role; }
13
15
  * - ✅ type Mode = 'dev' | 'prod'; class Config { mode: Mode; }
@@ -20,20 +22,20 @@ const rule = 'error'
20
22
  const selectorGeneral =
21
23
  'TSTypeAnnotation > TSUnionType:not(PropertyDefinition > .typeAnnotation > .typeAnnotation):not(TSPropertySignature > .typeAnnotation > .typeAnnotation)'
22
24
 
23
- // Interface properties with literal unions
25
+ // Interface properties with inline unions (literal or otherwise)
24
26
  const selectorInterfaceProperty =
25
- 'TSPropertySignature > TSTypeAnnotation > TSUnionType:has(TSLiteralType)'
27
+ 'TSPropertySignature > TSTypeAnnotation > TSUnionType'
26
28
 
27
- // Class properties with literal unions
29
+ // Class properties with inline unions (literal or otherwise)
28
30
  const selectorClassProperty =
29
- 'PropertyDefinition > TSTypeAnnotation > TSUnionType:has(TSLiteralType)'
31
+ 'PropertyDefinition > TSTypeAnnotation > TSUnionType'
30
32
 
31
33
  const messageGeneral =
32
34
  'Use a named type declaration instead of inline union types.'
33
35
  const messageProperty =
34
- 'Interface properties with literal unions should use a named type declaration.'
36
+ 'Interface properties with inline unions should use a named type declaration.'
35
37
  const messageClassProperty =
36
- 'Class properties with literal unions should use a named type declaration.'
38
+ 'Class properties with inline unions should use a named type declaration.'
37
39
 
38
40
  /**
39
41
  * Export the complete rule configurations for no-restricted-syntax
@@ -23,14 +23,14 @@ const message =
23
23
  /**
24
24
  * Export the complete rule configuration for no-restricted-syntax
25
25
  * Can be used in ESLint config as part of no-restricted-syntax rules:
26
- * "no-restricted-syntax": ["error", ...otherRules, noProcessEnvPropertiesConfig]
26
+ * "no-restricted-syntax": ["error", ...otherRules, noProcessEnvironmentPropertiesConfig]
27
27
  */
28
- const noProcessEnvPropertiesConfig = {
28
+ const noProcessEnvironmentPropertiesConfig = {
29
29
  selector,
30
30
  message,
31
31
  }
32
32
 
33
33
  // Consolidated exports
34
- export { rule, selector, message, noProcessEnvPropertiesConfig }
34
+ export { rule, selector, message, noProcessEnvironmentPropertiesConfig }
35
35
 
36
- export default noProcessEnvPropertiesConfig
36
+ export default noProcessEnvironmentPropertiesConfig
@@ -23,7 +23,7 @@
23
23
  const rule = 'error'
24
24
 
25
25
  // First selector: matches TSLiteralType descendants of first param (catches literals inside unions)
26
- const selectorNestedParams =
26
+ const selectorNestedParameters =
27
27
  'TSTypeReference[typeName.name="Record"] > TSTypeParameterInstantiation > .params:first-child TSLiteralType'
28
28
 
29
29
  // Second selector: matches direct TSLiteralType as first param
@@ -38,7 +38,7 @@ const message =
38
38
  * Can be used in ESLint config as part of no-restricted-syntax rules
39
39
  */
40
40
  const noRecordLiteralTypesNestedConfig = {
41
- selector: selectorNestedParams,
41
+ selector: selectorNestedParameters,
42
42
  message,
43
43
  }
44
44
 
@@ -58,7 +58,7 @@ const noRecordLiteralTypesConfigs = [
58
58
  // Consolidated exports
59
59
  export {
60
60
  rule,
61
- selectorNestedParams,
61
+ selectorNestedParameters,
62
62
  selectorFirstChild,
63
63
  message,
64
64
  noRecordLiteralTypesNestedConfig,
@@ -21,4 +21,45 @@ export const importRules = {
21
21
  'import/newline-after-import': 'off',
22
22
  'import/first': 'error',
23
23
  'import/prefer-default-export': 'off',
24
+ // Import hygiene: collapse duplicate import statements from the same module
25
+ // and forbid exporting mutable bindings (export let/var), which create
26
+ // shared mutable state across modules and are a common AI-generated footgun.
27
+ 'import/no-duplicates': 'error',
28
+ 'import/no-mutable-exports': 'error',
29
+ // Forbid circular import dependencies (A imports B imports A). Cycles cause
30
+ // order-dependent runtime bugs where a module reads a not-yet-initialized
31
+ // binding from its partner and silently sees `undefined`, defeat tree
32
+ // shaking, and are a reliable signal of tangled, hard-to-follow module
33
+ // boundaries. They are also a frequent AI-generated footgun, since an
34
+ // assistant editing one file cannot see the import graph it closes. Detect
35
+ // them statically. `maxDepth: Infinity` follows the full import graph so deep
36
+ // cycles are caught too; `ignoreExternal` skips traversal into node_modules
37
+ // for performance, since cycles inside dependencies are not the consumer's
38
+ // to fix.
39
+ 'import/no-cycle': ['error', { maxDepth: Infinity, ignoreExternal: true }],
40
+ // A module importing itself is always a mistake (usually a copy-paste or a
41
+ // bad auto-import) and produces a degenerate cycle; flag it explicitly.
42
+ 'import/no-self-import': 'error',
43
+ // Forbid empty named import blocks (`import {} from 'mod'`). An empty block
44
+ // is the residue of deleting the last named binding from an import: the
45
+ // statement now pulls in nothing yet still reads as if it imports names,
46
+ // which misleads readers and quietly keeps a dead dependency edge alive. A
47
+ // bare side-effect import (`import 'mod'`) — or removing the line — states
48
+ // the real intent. This is a frequent leftover from AI-assisted edits that
49
+ // rewrite an import list one binding at a time, putting it squarely in scope
50
+ // for this config's explicit-over-clever stance. The rule is auto-fixable.
51
+ 'import/no-empty-named-blocks': 'error',
52
+ // Forbid redundant segments in relative import paths, e.g. `./foo/../bar`
53
+ // (really `./bar`), `./../sibling` (really `../sibling`) or `./././mod`
54
+ // (really `./mod`). These resolve to the same module but read as if they
55
+ // point somewhere else, forcing a reviewer to mentally collapse the path to
56
+ // know what is actually imported, and they hide accidental directory
57
+ // traversals introduced by a bad auto-import or a copy-paste from another
58
+ // folder — a frequent AI-assisted-edit footgun and squarely the
59
+ // explicit-over-clever ground this config covers. The rule is auto-fixable,
60
+ // so adopting projects get a one-shot `--fix`. `commonjs: true` extends the
61
+ // check to `require()` calls; `noUselessIndex` is left at its default (off)
62
+ // so explicit `/index` references stay allowed and only genuinely redundant
63
+ // `../`/`./` segments are flagged.
64
+ 'import/no-useless-path-segments': ['error', { commonjs: true }],
24
65
  }
@@ -1,6 +1,5 @@
1
1
  import { securityRules } from './security/index.js'
2
2
  import { importRules } from './import/index.js'
3
- import { jsxA11yRules } from './jsx-a11y/index.js'
4
3
  import { reactRules } from './react/index.js'
5
4
  import { nRules } from './n/index.js'
6
5
  import { classExportRules } from './class-export/index.js'
@@ -8,7 +7,6 @@ import { classExportRules } from './class-export/index.js'
8
7
  export const pluginRules = {
9
8
  ...securityRules,
10
9
  ...importRules,
11
- ...jsxA11yRules,
12
10
  ...reactRules,
13
11
  ...nRules,
14
12
  ...classExportRules,
@@ -1,5 +1,5 @@
1
- import { noProcessEnvConfig } from './no-process-env/index.js'
1
+ import { noProcessEnvironmentConfig } from './no-process-env/index.js'
2
2
 
3
3
  export const nRules = {
4
- 'n/no-process-env': noProcessEnvConfig,
4
+ 'n/no-process-env': noProcessEnvironmentConfig,
5
5
  }
@@ -13,11 +13,11 @@ const options = {}
13
13
  /**
14
14
  * Export the complete rule configuration
15
15
  * Can be used in ESLint config as:
16
- * "n/no-process-env": noProcessEnvConfig
16
+ * "n/no-process-env": noProcessEnvironmentConfig
17
17
  */
18
- const noProcessEnvConfig = [rule, options]
18
+ const noProcessEnvironmentConfig = [rule, options]
19
19
 
20
20
  // Consolidated exports
21
- export { rule, options, noProcessEnvConfig }
21
+ export { rule, options, noProcessEnvironmentConfig }
22
22
 
23
- export default noProcessEnvConfig
23
+ export default noProcessEnvironmentConfig
@@ -13,4 +13,73 @@ export const reactRules = {
13
13
  'react/jsx-wrap-multilines': 'off',
14
14
  'react/jsx-closing-bracket-location': 'off',
15
15
  'react/jsx-no-useless-fragment': 'off',
16
+ // Guard against React's "leaked render" bug: a short-circuit like
17
+ // `{count && <List />}` renders the literal `0` (or `NaN`) when the left
18
+ // operand is a falsy *non-boolean*, and `{name && <h1>{name}</h1>}` renders
19
+ // an empty string the same way. React only skips rendering for `false`,
20
+ // `null` and `undefined`, so any other falsy value leaks straight into the
21
+ // DOM as visible text. That is a real rendering bug the type checker cannot
22
+ // catch (the JSX is perfectly typed) and exactly the shortcut AI assistants
23
+ // emit when conditionally rendering a list or label. Unlike the stylistic
24
+ // react rules turned off above, this is a correctness check, so it is turned
25
+ // on. `validStrategies: ['ternary', 'coerce']` forces the unambiguous form —
26
+ // an explicit `cond ? <X /> : null` or a boolean coercion `!!cond && <X />`
27
+ // — instead of the bare `&&` that leaks. The rule is auto-fixable, so
28
+ // consumers can adopt it with `eslint --fix`.
29
+ 'react/jsx-no-leaked-render': [
30
+ 'error',
31
+ { validStrategies: ['ternary', 'coerce'] },
32
+ ],
33
+ // Forbid using an array index as a React `key` (`items.map((item, i) => <Row
34
+ // key={i} />)`). A key is React's identity for a list child across renders;
35
+ // when it is the position rather than something stable about the item, React
36
+ // maps the wrong previous element onto the wrong new one as soon as the list
37
+ // is reordered, filtered, or has an item inserted/removed at the front or
38
+ // middle. The visible result is a class of silent UI bugs that no type check
39
+ // can catch: text typed into one input jumps to another row, the wrong item
40
+ // animates, a checkbox's checked state sticks to the wrong record, and
41
+ // uncontrolled component state generally "smears" across siblings. The fix is
42
+ // to key by a stable, item-specific value (an id, a slug, a content hash),
43
+ // which states the intended identity explicitly. `key={index}` is exactly the
44
+ // shortcut an AI assistant reaches for when the item has no obvious id, so it
45
+ // is squarely in this config's bug-prevention, explicit-over-clever stance —
46
+ // the same correctness-not-style bar that turns on `jsx-no-leaked-render`
47
+ // above while leaving the cosmetic react rules off. Like the other react
48
+ // rules it only applies to `.jsx`/`.tsx` files, so non-React TypeScript
49
+ // packages are unaffected. This is why a downstream repo (`oss-il`) already
50
+ // re-adds it by hand on top of the base config.
51
+ 'react/no-array-index-key': 'error',
52
+ // Forbid the `dangerouslySetInnerHTML` prop. Setting inner HTML directly from
53
+ // a string bypasses React's XSS defenses: any server-sourced or user-supplied
54
+ // content rendered this way can execute arbitrary scripts in the browser.
55
+ // `dangerouslySetInnerHTML` is intentionally verbose — its name is a warning —
56
+ // but AI assistants still reach for it when they need to inject a server HTML
57
+ // fragment or render a rich-text field, without thinking through whether the
58
+ // content is safe. The only legitimate uses (known-safe static markup, a
59
+ // sanitized string from a DOMPurify-style pass) should be reviewed explicitly;
60
+ // the rule forces that review by making every usage a lint error, which the
61
+ // engineer must suppress with an `eslint-disable` comment that makes the
62
+ // intentional decision visible in code review. The rule is not auto-fixable,
63
+ // so each suppression is a deliberate act.
64
+ 'react/no-danger': 'error',
65
+ // Forbid a freshly-constructed value as the `value` of a Context Provider:
66
+ // `<Ctx.Provider value={{ user, setUser }}>` or `value={[state, dispatch]}`
67
+ // or `value={() => ...}`. The object/array/function literal is rebuilt on
68
+ // every render of the providing component, so its identity changes every time
69
+ // even when the data inside is unchanged. React compares context values by
70
+ // reference, so a new reference forces *every* consumer of that context to
71
+ // re-render — including ones that `React.memo`/`useMemo` were added precisely
72
+ // to protect. The result is a silent, whole-subtree performance bug: the app
73
+ // is correct but does far more work than it should, and the cause (an inline
74
+ // literal three components up) is invisible at the consumer. The fix states
75
+ // the intent explicitly — hoist the value into a `useMemo`/`useCallback` (or a
76
+ // stable ref) so the reference only changes when the data does. An inline
77
+ // `value={{ ... }}` is exactly the shortcut an AI assistant emits when wiring
78
+ // up a provider, so it sits on the same correctness/perf-not-style bar that
79
+ // turns on `jsx-no-leaked-render` and `no-array-index-key` above while leaving
80
+ // the cosmetic react rules off. Like the other react rules it only applies to
81
+ // `.jsx`/`.tsx` files, so non-React TypeScript packages are unaffected. This
82
+ // is why a downstream repo (`oss-il`) already re-adds it by hand on top of the
83
+ // base config.
84
+ 'react/jsx-no-constructed-context-values': 'error',
16
85
  }