@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 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}