@duro-app/eslint-config 5.32.0 → 5.33.0

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
@@ -94,3 +94,36 @@ Flat config is last-wins — scope an override after the preset:
94
94
  rules: {'no-restricted-imports': 'off'},
95
95
  }
96
96
  ```
97
+
98
+ ## adr-lint rules (opt-in)
99
+
100
+ `adrLint` and `adrLintTyped` add the decided code rules a linter matches
101
+ exactly (no `@ts-ignore`, no unexplained `eslint-disable`, no swallowed
102
+ `catch`; the typed variant also checks `switch` exhaustiveness and needs
103
+ `projectService`). They come from
104
+ [`@aval-adr/eslint-config`](https://www.npmjs.com/package/@aval-adr/eslint-config)
105
+ and are not part of `base`, `react`, `effect` or `tests`.
106
+
107
+ Put them last and name the presets you use in `stack`: flat config keeps the
108
+ last options of a rule, and adr-lint emits its own `no-restricted-imports`, so
109
+ `stack` re-states the `react` / `effect` import bans inside that one rule.
110
+ `restrictedImports` adds your own bans to it.
111
+
112
+ ```js
113
+ import {react, adrLint} from '@duro-app/eslint-config'
114
+
115
+ export default [
116
+ ...react,
117
+ ...adrLint({
118
+ stack: ['react'],
119
+ restrictedImports: [{patterns: [{group: ['**/infra/**']}]}],
120
+ }),
121
+ ]
122
+ ```
123
+
124
+ The stack's bans are also copied into every `files`-scoped entry, because in a
125
+ file two scopes cover only the later one's options apply.
126
+
127
+ `restrictedImports` without `stack` throws, since a react repository would
128
+ otherwise lose its UI bans in silence: pass `stack: []` when no preset applies.
129
+ An unknown `stack` name throws too.
package/index.d.ts CHANGED
@@ -5,10 +5,31 @@ export declare const react: Linter.Config[]
5
5
  export declare const effect: Linter.Config[]
6
6
  export declare const tests: Linter.Config[]
7
7
 
8
+ export type AdrLintStack = 'react' | 'effect'
9
+
10
+ export interface AdrLintRestrictedImport {
11
+ files?: string[]
12
+ paths?: (string | {name: string; message?: string; allowTypeImports?: boolean})[]
13
+ patterns?: (string | {group: string[]; message?: string; allowTypeImports?: boolean})[]
14
+ }
15
+
16
+ /**
17
+ * With `restrictedImports`, `stack` is required: it names the presets whose
18
+ * import bans the merged `no-restricted-imports` must keep (`[]` for none).
19
+ */
20
+ export type AdrLintOptions =
21
+ | {stack?: AdrLintStack[]; restrictedImports?: never}
22
+ | {stack: AdrLintStack[]; restrictedImports: AdrLintRestrictedImport[]}
23
+
24
+ export declare function adrLint(options?: AdrLintOptions): Linter.Config[]
25
+ export declare function adrLintTyped(options?: AdrLintOptions): Linter.Config[]
26
+
8
27
  declare const presets: {
9
28
  base: Linter.Config[]
10
29
  react: Linter.Config[]
11
30
  effect: Linter.Config[]
12
31
  tests: Linter.Config[]
32
+ adrLint: typeof adrLint
33
+ adrLintTyped: typeof adrLintTyped
13
34
  }
14
35
  export default presets
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duro-app/eslint-config",
3
- "version": "5.32.0",
3
+ "version": "5.33.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Shareable ESLint flat config for the Duro stack: base TS conventions, the design-system rules and UI-library policy for React apps, Effect server conventions, and accessibility-first test selectors.",
@@ -44,12 +44,13 @@
44
44
  "eslint": "^9.0.0 || ^10.0.0"
45
45
  },
46
46
  "dependencies": {
47
+ "@aval-adr/eslint-config": "^0.6.0",
47
48
  "@effect/eslint-plugin": "^0.3.2",
48
49
  "@eslint/js": "^9.20.0 || ^10.0.0",
49
50
  "eslint-config-prettier": "^10.1.8",
50
51
  "eslint-plugin-react-hooks": "^7.0.1",
51
52
  "typescript-eslint": "^8.56.0",
52
- "@duro-app/eslint-plugin": "^5.32.0"
53
+ "@duro-app/eslint-plugin": "^5.33.0"
53
54
  },
54
55
  "devDependencies": {
55
56
  "eslint": "^9.20.0",
package/src/index.js CHANGED
@@ -4,6 +4,7 @@ import reactHooks from 'eslint-plugin-react-hooks'
4
4
  import effectPlugin from '@effect/eslint-plugin'
5
5
  import prettier from 'eslint-config-prettier'
6
6
  import duro from '@duro-app/eslint-plugin'
7
+ import {adrLint as aval, adrLintTyped as avalTyped} from '@aval-adr/eslint-config'
7
8
 
8
9
  /**
9
10
  * Shareable flat config for the Duro stack. Four presets, each a plain flat
@@ -49,6 +50,33 @@ const FOREIGN_UI_MESSAGE =
49
50
  const LEXICAL_MESSAGE =
50
51
  'Rich text goes through @fredericrous/lexical-multi at the app layer, never raw Lexical — the wrapper carries the multi-editor state, native support, and edit-state accessibility.'
51
52
 
53
+ const FOREIGN_UI_PATTERN = {
54
+ allowTypeImports: true,
55
+ group: [
56
+ 'react-aria',
57
+ 'react-aria-components',
58
+ '@react-aria/*',
59
+ '@adobe/react-spectrum',
60
+ '@react-spectrum/*',
61
+ '@mui/*',
62
+ '@material-ui/*',
63
+ '@radix-ui/*',
64
+ '@chakra-ui/*',
65
+ '@headlessui/*',
66
+ 'antd',
67
+ 'antd/*',
68
+ '@frontjutsu/ui',
69
+ '@frontjutsu/ui/*',
70
+ ],
71
+ message: FOREIGN_UI_MESSAGE,
72
+ }
73
+
74
+ const LEXICAL_PATTERN = {
75
+ allowTypeImports: true,
76
+ group: ['lexical', '@lexical/*'],
77
+ message: LEXICAL_MESSAGE,
78
+ }
79
+
52
80
  export const react = tseslint.config(
53
81
  ...base,
54
82
  // Classic hooks correctness only. react-hooks v7's flat.recommended also
@@ -78,33 +106,7 @@ export const react = tseslint.config(
78
106
  '@typescript-eslint/no-restricted-imports': [
79
107
  'error',
80
108
  {
81
- patterns: [
82
- {
83
- allowTypeImports: true,
84
- group: [
85
- 'react-aria',
86
- 'react-aria-components',
87
- '@react-aria/*',
88
- '@adobe/react-spectrum',
89
- '@react-spectrum/*',
90
- '@mui/*',
91
- '@material-ui/*',
92
- '@radix-ui/*',
93
- '@chakra-ui/*',
94
- '@headlessui/*',
95
- 'antd',
96
- 'antd/*',
97
- '@frontjutsu/ui',
98
- '@frontjutsu/ui/*',
99
- ],
100
- message: FOREIGN_UI_MESSAGE,
101
- },
102
- {
103
- allowTypeImports: true,
104
- group: ['lexical', '@lexical/*'],
105
- message: LEXICAL_MESSAGE,
106
- },
107
- ],
109
+ patterns: [FOREIGN_UI_PATTERN, LEXICAL_PATTERN],
108
110
  },
109
111
  ],
110
112
  },
@@ -117,6 +119,18 @@ const KYSELY_MESSAGE =
117
119
  const OTEL_BARREL_MESSAGE =
118
120
  'Import the subpath you need (e.g. @effect/opentelemetry/NodeSdk) — the barrel re-exports WebSdk, which statically imports @opentelemetry/sdk-trace-web and crashes a Node service at module load.'
119
121
 
122
+ const OTEL_BARREL_PATH = {
123
+ allowTypeImports: true,
124
+ name: '@effect/opentelemetry',
125
+ message: OTEL_BARREL_MESSAGE,
126
+ }
127
+
128
+ const KYSELY_PATTERN = {
129
+ allowTypeImports: true,
130
+ group: ['kysely', 'kysely-*', '@kysely/*'],
131
+ message: KYSELY_MESSAGE,
132
+ }
133
+
120
134
  export const effect = tseslint.config(...base, {
121
135
  name: 'duro-config/effect-policy',
122
136
  plugins: {'@effect': effectPlugin},
@@ -129,20 +143,8 @@ export const effect = tseslint.config(...base, {
129
143
  '@typescript-eslint/no-restricted-imports': [
130
144
  'error',
131
145
  {
132
- paths: [
133
- {
134
- allowTypeImports: true,
135
- name: '@effect/opentelemetry',
136
- message: OTEL_BARREL_MESSAGE,
137
- },
138
- ],
139
- patterns: [
140
- {
141
- allowTypeImports: true,
142
- group: ['kysely', 'kysely-*', '@kysely/*'],
143
- message: KYSELY_MESSAGE,
144
- },
145
- ],
146
+ paths: [OTEL_BARREL_PATH],
147
+ patterns: [KYSELY_PATTERN],
146
148
  },
147
149
  ],
148
150
  },
@@ -184,4 +186,65 @@ export const tests = [
184
186
  },
185
187
  ]
186
188
 
187
- export default {base, react, effect, tests}
189
+ const STACK_IMPORT_BANS = {
190
+ react: {patterns: [FOREIGN_UI_PATTERN, LEXICAL_PATTERN]},
191
+ effect: {paths: [OTEL_BARREL_PATH], patterns: [KYSELY_PATTERN]},
192
+ }
193
+
194
+ // The preset emits one `no-restricted-imports` per `files` scope, and in a
195
+ // file several scopes cover flat config keeps only the last one's options. So
196
+ // the stack's bans go on every entry: unscoped, and inside each scoped entry
197
+ // too, or a layer contract over `src/**` would drop the UI bans there.
198
+ function adrLintOptions({stack, restrictedImports = []}) {
199
+ if (stack === undefined && restrictedImports.length > 0) {
200
+ throw new Error(
201
+ '@duro-app/eslint-config: adrLint({restrictedImports}) needs `stack` — the presets whose import bans it must keep (["react"], ["effect"], or [] for none); without it a react repo would lose its UI bans',
202
+ )
203
+ }
204
+ const stackBans = (stack ?? []).map((name) => {
205
+ if (!Object.hasOwn(STACK_IMPORT_BANS, name)) {
206
+ throw new Error(
207
+ `@duro-app/eslint-config: unknown stack "${name}" — expected "react" and/or "effect"`,
208
+ )
209
+ }
210
+ return STACK_IMPORT_BANS[name]
211
+ })
212
+ const stackPaths = stackBans.flatMap((bans) => bans.paths ?? [])
213
+ const stackPatterns = stackBans.flatMap((bans) => bans.patterns ?? [])
214
+ // Once per scope: the preset merges entries that share `files`, and a ban
215
+ // listed twice there reports one import twice.
216
+ const scopesGiven = new Set()
217
+ const scoped = restrictedImports.map((entry) => {
218
+ if (entry.files === undefined) return entry
219
+ const scope = JSON.stringify(entry.files)
220
+ if (scopesGiven.has(scope)) return entry
221
+ scopesGiven.add(scope)
222
+ return {
223
+ ...entry,
224
+ paths: [...stackPaths, ...(entry.paths ?? [])],
225
+ patterns: [...stackPatterns, ...(entry.patterns ?? [])],
226
+ }
227
+ })
228
+ return {restrictedImports: [...stackBans, ...scoped]}
229
+ }
230
+
231
+ /**
232
+ * Opt-in: the decided code rules a linter matches exactly, from
233
+ * @aval-adr/eslint-config. Flat config keeps the last options of a rule, so
234
+ * `stack` re-states the named presets' import bans inside the one merged
235
+ * `no-restricted-imports` that adr-lint emits.
236
+ *
237
+ * @param {import('../index.d.ts').AdrLintOptions} [options]
238
+ * @returns {import('eslint').Linter.Config[]}
239
+ */
240
+ export const adrLint = ({stack, restrictedImports} = {}) =>
241
+ aval(adrLintOptions({stack, restrictedImports}))
242
+
243
+ /**
244
+ * @param {import('../index.d.ts').AdrLintOptions} [options]
245
+ * @returns {import('eslint').Linter.Config[]}
246
+ */
247
+ export const adrLintTyped = ({stack, restrictedImports} = {}) =>
248
+ avalTyped(adrLintOptions({stack, restrictedImports}))
249
+
250
+ export default {base, react, effect, tests, adrLint, adrLintTyped}