@duro-app/eslint-config 1.1.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/LICENSE +21 -0
- package/README.md +68 -0
- package/index.d.ts +14 -0
- package/package.json +61 -0
- package/src/index.js +155 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Frederic Rousseau
|
|
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,68 @@
|
|
|
1
|
+
# @duro-app/eslint-config
|
|
2
|
+
|
|
3
|
+
Shareable ESLint flat config for the Duro stack — the policies CLAUDE.md and
|
|
4
|
+
agent memory can only suggest, wired into the linter so every consumer repo
|
|
5
|
+
gates them at commit time (via amont's built-in `pre-commit-lint-js`, which
|
|
6
|
+
activates on the presence of a root ESLint config) and in CI.
|
|
7
|
+
|
|
8
|
+
Requires ESLint 9 or 10 (flat config). All plugins arrive as dependencies of
|
|
9
|
+
this package — consumers install one package.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// eslint.config.js (repo root — amont only detects a ROOT config)
|
|
15
|
+
import {react, effect, tests} from '@duro-app/eslint-config'
|
|
16
|
+
|
|
17
|
+
export default [
|
|
18
|
+
{ignores: ['**/dist/**', '**/build/**']},
|
|
19
|
+
...react,
|
|
20
|
+
...tests,
|
|
21
|
+
// Server-only packages in a monorepo can scope the effect preset:
|
|
22
|
+
...effect.map((c) => ({...c, files: ['apps/api/**/*.ts']})),
|
|
23
|
+
]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Presets
|
|
27
|
+
|
|
28
|
+
| Preset | Composes | For |
|
|
29
|
+
| -------- | ------------------------------------------------------------------ | ------------------------- |
|
|
30
|
+
| `base` | `@eslint/js` + `typescript-eslint` recommended, prettier interop | every TypeScript package |
|
|
31
|
+
| `react` | base + react-hooks + `@duro-app/eslint-plugin` + UI-library policy | RSD / `@duro-app/ui` apps |
|
|
32
|
+
| `effect` | base + `@effect` plugin + server-stack policy | Effect services |
|
|
33
|
+
| `tests` | accessibility-first selectors (self-scoped to test/e2e files) | any repo with tests |
|
|
34
|
+
|
|
35
|
+
`react` and `effect` both include `base`; applying both just repeats
|
|
36
|
+
identical entries, which flat config merges harmlessly. `tests` carries its
|
|
37
|
+
own `files` scope, so it can be appended unconditionally.
|
|
38
|
+
|
|
39
|
+
## What the policies enforce
|
|
40
|
+
|
|
41
|
+
- **`react`** — UI primitives come from `@duro-app/ui` only (react-aria,
|
|
42
|
+
Spectrum, MUI, Radix, Chakra, headlessui, antd and `@frontjutsu/ui` are
|
|
43
|
+
banned with a message that says where to add missing components); rich
|
|
44
|
+
text goes through `@fredericrous/lexical-multi`, never raw `lexical`;
|
|
45
|
+
plus everything in `@duro-app/eslint-plugin` (html.\* elements, deep token
|
|
46
|
+
imports, token values, form kit, the RSD flexGrow trap).
|
|
47
|
+
- **`effect`** — `@effect/sql` not Kysely; `@effect/opentelemetry` only via
|
|
48
|
+
subpath (the barrel statically imports the web SDK and crashes Node at
|
|
49
|
+
module load); barrel-import hygiene via `@effect/eslint-plugin`.
|
|
50
|
+
- **`tests`** — no `getByTestId` family, no CSS class/id `locator()`
|
|
51
|
+
strings: select by role/label/text, gate readiness on `aria-busy`.
|
|
52
|
+
|
|
53
|
+
Severity is the one gating line: `error` gates at commit and CI, `warn`
|
|
54
|
+
informs everywhere. Don't add `--max-warnings=0` — promote a rule to
|
|
55
|
+
`error` here instead if it earns gating.
|
|
56
|
+
|
|
57
|
+
## Escape hatches
|
|
58
|
+
|
|
59
|
+
Flat config is last-wins — scope an override after the preset:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
...react,
|
|
63
|
+
{
|
|
64
|
+
// The one file allowed to import the raw engine reset:
|
|
65
|
+
files: ['packages/editor/src/usePageNode.ts'],
|
|
66
|
+
rules: {'no-restricted-imports': 'off'},
|
|
67
|
+
}
|
|
68
|
+
```
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type {Linter} from 'eslint'
|
|
2
|
+
|
|
3
|
+
export declare const base: Linter.Config[]
|
|
4
|
+
export declare const react: Linter.Config[]
|
|
5
|
+
export declare const effect: Linter.Config[]
|
|
6
|
+
export declare const tests: Linter.Config[]
|
|
7
|
+
|
|
8
|
+
declare const presets: {
|
|
9
|
+
base: Linter.Config[]
|
|
10
|
+
react: Linter.Config[]
|
|
11
|
+
effect: Linter.Config[]
|
|
12
|
+
tests: Linter.Config[]
|
|
13
|
+
}
|
|
14
|
+
export default presets
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@duro-app/eslint-config",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
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.",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"eslint",
|
|
9
|
+
"eslintconfig",
|
|
10
|
+
"eslint-config",
|
|
11
|
+
"react-strict-dom",
|
|
12
|
+
"design-system",
|
|
13
|
+
"effect"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/fredericrous/duro-design-system",
|
|
18
|
+
"directory": "packages/eslint-config"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/fredericrous/duro-design-system",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/fredericrous/duro-design-system/issues"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"src",
|
|
29
|
+
"index.d.ts"
|
|
30
|
+
],
|
|
31
|
+
"main": "src/index.js",
|
|
32
|
+
"types": "index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./index.d.ts",
|
|
36
|
+
"import": "./src/index.js",
|
|
37
|
+
"default": "./src/index.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"eslint": "^9.0.0 || ^10.0.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@effect/eslint-plugin": "^0.3.2",
|
|
48
|
+
"@eslint/js": "^9.20.0 || ^10.0.0",
|
|
49
|
+
"eslint-config-prettier": "^10.1.8",
|
|
50
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
51
|
+
"typescript-eslint": "^8.56.0",
|
|
52
|
+
"@duro-app/eslint-plugin": "^1.1.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"eslint": "^9.20.0",
|
|
56
|
+
"typescript": "^5.7.0"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"typecheck": "tsc --noEmit"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import tseslint from 'typescript-eslint'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import effectPlugin from '@effect/eslint-plugin'
|
|
5
|
+
import prettier from 'eslint-config-prettier'
|
|
6
|
+
import duro from '@duro-app/eslint-plugin'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shareable flat config for the Duro stack. Four presets, each a plain flat
|
|
10
|
+
* config array:
|
|
11
|
+
*
|
|
12
|
+
* - `base` — TS + prettier interop; every TypeScript package.
|
|
13
|
+
* - `react` — base + react-hooks + the duro plugin + UI-library policy;
|
|
14
|
+
* anything rendering with react-strict-dom / @duro-app/ui.
|
|
15
|
+
* - `effect` — base + the @effect plugin + server-stack policy; Effect
|
|
16
|
+
* services and API packages.
|
|
17
|
+
* - `tests` — accessibility-first selectors; applies itself only to
|
|
18
|
+
* test/e2e files, so it can sit in any config unconditionally.
|
|
19
|
+
*
|
|
20
|
+
* `react` and `effect` both include `base`, so either works alone; applying
|
|
21
|
+
* both in one repo just repeats identical entries, which flat config merges
|
|
22
|
+
* harmlessly. Policy bans carry their rationale in the message — the lint
|
|
23
|
+
* output is meant to teach the fix, not just refuse the code.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export const base = tseslint.config(
|
|
27
|
+
js.configs.recommended,
|
|
28
|
+
...tseslint.configs.recommended,
|
|
29
|
+
{
|
|
30
|
+
name: 'duro-config/base',
|
|
31
|
+
rules: {
|
|
32
|
+
'@typescript-eslint/no-unused-vars': [
|
|
33
|
+
'warn',
|
|
34
|
+
{
|
|
35
|
+
argsIgnorePattern: '^_',
|
|
36
|
+
destructuredArrayIgnorePattern: '^_',
|
|
37
|
+
varsIgnorePattern: '^_',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
prettier,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const FOREIGN_UI_MESSAGE =
|
|
47
|
+
'UI primitives come from @duro-app/ui only. If the component you need is missing, add it to duro-design-system instead of reaching for another library — `npx duro list` shows what exists.'
|
|
48
|
+
|
|
49
|
+
const LEXICAL_MESSAGE =
|
|
50
|
+
'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
|
+
export const react = tseslint.config(
|
|
53
|
+
...base,
|
|
54
|
+
reactHooks.configs.flat.recommended,
|
|
55
|
+
duro.configs.recommended,
|
|
56
|
+
{
|
|
57
|
+
name: 'duro-config/react-policy',
|
|
58
|
+
rules: {
|
|
59
|
+
'no-restricted-imports': [
|
|
60
|
+
'error',
|
|
61
|
+
{
|
|
62
|
+
patterns: [
|
|
63
|
+
{
|
|
64
|
+
group: [
|
|
65
|
+
'react-aria',
|
|
66
|
+
'react-aria-components',
|
|
67
|
+
'@react-aria/*',
|
|
68
|
+
'@adobe/react-spectrum',
|
|
69
|
+
'@react-spectrum/*',
|
|
70
|
+
'@mui/*',
|
|
71
|
+
'@material-ui/*',
|
|
72
|
+
'@radix-ui/*',
|
|
73
|
+
'@chakra-ui/*',
|
|
74
|
+
'@headlessui/*',
|
|
75
|
+
'antd',
|
|
76
|
+
'antd/*',
|
|
77
|
+
'@frontjutsu/ui',
|
|
78
|
+
'@frontjutsu/ui/*',
|
|
79
|
+
],
|
|
80
|
+
message: FOREIGN_UI_MESSAGE,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
group: ['lexical', '@lexical/*'],
|
|
84
|
+
message: LEXICAL_MESSAGE,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const KYSELY_MESSAGE =
|
|
94
|
+
'DB access in this stack is @effect/sql (SqlClient + Migrator + SqlSchema) — Kysely duplicates it outside the Effect runtime.'
|
|
95
|
+
|
|
96
|
+
const OTEL_BARREL_MESSAGE =
|
|
97
|
+
'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.'
|
|
98
|
+
|
|
99
|
+
export const effect = tseslint.config(...base, {
|
|
100
|
+
name: 'duro-config/effect-policy',
|
|
101
|
+
plugins: {'@effect': effectPlugin},
|
|
102
|
+
rules: {
|
|
103
|
+
'@effect/no-import-from-barrel-package': 'warn',
|
|
104
|
+
'no-restricted-imports': [
|
|
105
|
+
'error',
|
|
106
|
+
{
|
|
107
|
+
paths: [{name: '@effect/opentelemetry', message: OTEL_BARREL_MESSAGE}],
|
|
108
|
+
patterns: [
|
|
109
|
+
{
|
|
110
|
+
group: ['kysely', 'kysely-*', '@kysely/*'],
|
|
111
|
+
message: KYSELY_MESSAGE,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const TESTID_MESSAGE =
|
|
120
|
+
'Select by what users and assistive tech perceive — getByRole/getByLabel/getByText — not test ids. If nothing selectable exists, the control is not accessible; fix the component.'
|
|
121
|
+
|
|
122
|
+
const CSS_LOCATOR_MESSAGE =
|
|
123
|
+
'CSS class/id locators are implementation details that drift. Use getByRole/getByLabel/getByText; gate readiness on aria-busy over a named region, not a class hook.'
|
|
124
|
+
|
|
125
|
+
export const tests = [
|
|
126
|
+
{
|
|
127
|
+
name: 'duro-config/tests',
|
|
128
|
+
files: [
|
|
129
|
+
'**/*.test.{ts,tsx,js,jsx}',
|
|
130
|
+
'**/*.spec.{ts,tsx,js,jsx}',
|
|
131
|
+
'**/e2e/**/*.{ts,tsx,js,jsx}',
|
|
132
|
+
'**/__tests__/**/*.{ts,tsx,js,jsx}',
|
|
133
|
+
],
|
|
134
|
+
rules: {
|
|
135
|
+
'no-restricted-syntax': [
|
|
136
|
+
'error',
|
|
137
|
+
{
|
|
138
|
+
selector: 'CallExpression[callee.property.name=/^(get|query|find)(All)?ByTestId$/]',
|
|
139
|
+
message: TESTID_MESSAGE,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
selector: 'CallExpression[callee.name=/^(get|query|find)(All)?ByTestId$/]',
|
|
143
|
+
message: TESTID_MESSAGE,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
selector:
|
|
147
|
+
'CallExpression[callee.property.name="locator"] > Literal.arguments:first-child[value=/^[.#]/]',
|
|
148
|
+
message: CSS_LOCATOR_MESSAGE,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
export default {base, react, effect, tests}
|