-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy patheslint.config.mjs
More file actions
277 lines (267 loc) · 9.92 KB
/
Copy patheslint.config.mjs
File metadata and controls
277 lines (267 loc) · 9.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
import js from '@eslint/js';
import globals from 'globals';
import importPlugin from 'eslint-plugin-import';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import tseslint from 'typescript-eslint';
const tsFiles = ['backend/**/*.ts', 'shared/**/*.ts', 'frontend/src/**/*.{ts,tsx}'];
const typedConfigs = [...tseslint.configs.recommended].map((config) => ({
...config,
files: tsFiles,
}));
const typeAwareSourceFiles = [
'backend/src/**/*.ts',
'frontend/src/**/*.{ts,tsx}',
'shared/src/**/*.ts',
];
// Colocated backend test files (PR-B1+) live under backend/src/**/*.test.ts.
// Pre-PR-B1 backend tests lived in backend/test/ and were never subject to
// type-aware rules (the glob above only matched non-test src). Preserve
// that behaviour for the migrated colocated tests so the relocation isn't
// entangled with a global lint-rule expansion (one change, one purpose).
// Frontend tests stay type-aware as they always were under frontend/src/.
const backendColocatedTestGlobs = ['backend/src/**/*.test.ts', 'backend/src/**/fixtures/**'];
// Names of the module-isolation subdirs under views/modules/. Exported so
// the drift-detector test can read it and assert it ⊇ actual subdirs of
// `{backend,frontend}/src/views/modules/`. A new module added to the
// registry without being added here would silently lose cross-import
// protection — the drift-detector closes that window.
// Defined ABOVE `export default` so `moduleIsolationConfigs()` (called
// during default-export evaluation) doesn't TDZ-throw when it reads it.
export const MODULE_ISOLATION_NAMES = ['maintainer', 'health', 'activity', 'reef'];
export default tseslint.config(
{
ignores: [
'**/dist/**',
'**/dist-test/**',
'**/node_modules/**',
'**/coverage/**',
'**/.claude/**',
'**/.gc/**',
],
},
{
linterOptions: {
reportUnusedDisableDirectives: 'error',
reportUnusedInlineConfigs: 'error',
},
},
{
files: ['**/*.{js,mjs,cjs}'],
...js.configs.recommended,
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.browser,
...globals.node,
},
},
},
...typedConfigs,
{
files: tsFiles,
plugins: {
import: importPlugin,
},
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'import/first': 'error',
'no-console': 'error',
},
},
{
files: ['backend/src/logging.ts', 'frontend/src/test/**/*.ts', '**/*.test.{ts,tsx}'],
rules: {
'no-console': 'off',
},
},
{
files: typeAwareSourceFiles,
ignores: ['shared/src/**/*.test.ts', ...backendColocatedTestGlobs],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parserOptions: {
project: [
'./backend/tsconfig.test.json',
'./frontend/tsconfig.test.json',
'./shared/tsconfig.json',
],
tsconfigRootDir: import.meta.dirname,
},
globals: {
...globals.browser,
...globals.node,
},
},
rules: {
'@typescript-eslint/consistent-type-imports': [
'error',
{
fixStyle: 'inline-type-imports',
},
],
'@typescript-eslint/no-floating-promises': [
'error',
{
ignoreVoid: true,
},
],
'@typescript-eslint/no-misused-promises': [
'error',
{
checksVoidReturn: {
attributes: false,
},
},
],
'@typescript-eslint/switch-exhaustiveness-check': 'error',
'@typescript-eslint/restrict-template-expressions': [
'error',
{
allowBoolean: true,
allowNumber: true,
allowNullish: true,
},
],
},
},
{
files: ['frontend/src/**/*.{ts,tsx}'],
plugins: {
react,
'react-hooks': reactHooks,
},
settings: {
react: { version: 'detect' },
},
rules: {
'react/no-danger': 'error',
'react-hooks/exhaustive-deps': 'error',
'react-hooks/rules-of-hooks': 'error',
},
},
{
files: ['**/*.test.{ts,tsx}'],
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-empty-function': 'off',
},
},
// Modular-dashboard cross-module isolation (PR-C / bead 9yj.5 / PRD §1 /
// premortem #4). Each `views/modules/<X>/` directory is a self-contained
// module: it may import shared/, the host's lib/logging/config layer, and
// anything OUTSIDE views/modules/, but NEVER files belonging to a sibling
// module. Cross-module collaboration must happen through CityContext /
// ModuleResources, not direct imports — otherwise the "delete a module by
// removing it from ALL_MODULES / ALL_VIEWS" promise breaks.
//
// Implemented as per-side overrides because the file glob has to live in
// `files:` and the forbidden pattern depends on which side (backend vs
// frontend) the importer is on. Colocated `*.test.ts(x)` files inside a
// module ARE part of that module — they are exempted from the
// restriction because test files legitimately reach for sibling helpers
// and the rule's grouping cannot express "any module except mine" via a
// single pattern. The lint rule enforces production-code isolation; the
// tests verify the same boundary at runtime.
...moduleIsolationConfigs(),
);
/**
* Build per-module ESLint zones for the backend and frontend module trees
* (PR-C / bead 9yj.5). Each module subdirectory under
* `<side>/src/views/modules/<X>/` becomes its own zone:
*
* - Files inside `<X>/` may import their own siblings (no rule fires).
* - Files inside `<X>/` may import anything OUTSIDE `views/modules/`
* (shared/, lib/, logging, config — the common surface).
* - Files inside `<X>/` may NOT import from any other `views/modules/<Y>/`
* — the `from`/`except` zone says "anywhere in modules/ EXCEPT my own
* dir is off-limits."
*
* Implemented via `import/no-restricted-paths` (not plain
* `no-restricted-imports`) because the latter matches literal import
* strings, and a relative sibling import (`../<other>/foo.js`) bypasses
* a glob like `**\/views/modules/*\/**`. `no-restricted-paths` resolves
* the import to an absolute path before matching, so it catches every
* shape of cross-module reference.
*
* Module DIRECTORIES are the unit of isolation: a single-file module
* (e.g. `backend/src/views/modules/health.module.ts`) lives at the
* top of the tree, has no siblings of its own, and cannot import into
* a sibling subdirectory by accident — so it does not need a zone of
* its own. Adding a new module subdir means adding it to MODULE_DIRS
* below (the registry edit is the design-review checkpoint anyway,
* per PRD premortem #6).
*/
function moduleIsolationConfigs() {
const MESSAGE =
'cross-module import forbidden — modules talk via CityContext / ModuleResources, not direct imports. See specs/architecture/module-author-checklist.md.';
// PR-C Phase-4 HIGH fix: extend the `no-restricted-imports` patterns to
// cover ALL practical depths from a module subdir, closing the bypass
// the reviewer found where a 2-up reach (`maintainer/fixtures/x.ts`
// → `../../health.module.js`) slipped past the original patterns. We
// initially tried `import/no-restricted-paths` (depth-independent by
// design) but it requires the import to RESOLVE first, and the default
// node resolver doesn't swap TypeScript's ESM `.js` → `.ts` — so the
// rule silently skips on every TS file in this codebase. Installing
// `eslint-import-resolver-typescript` would fix that but adds a dep
// for a problem we can pin down with one more pattern depth.
//
// Pattern shapes (literal-string minimatch, applied per importer):
// `./X/**` — flat-file module reaches sibling SUBDIR module
// `./X.module*` — flat-file module reaches sibling FLAT module
// `../X/**` — 1-up: subdir module reaches sibling SUBDIR
// `../X.module*` — 1-up: subdir module reaches sibling FLAT
// `../../X/**` — 2-up: nested subdir reaches sibling SUBDIR (e.g. fixtures/)
// `../../X.module*` — 2-up: nested subdir reaches sibling FLAT
// `../../../X/**` — 3-up: deeply nested (services/foo/bar.ts)
// `../../../X.module*`
// `../../modules/X/**` + `../../modules/X.module*` — explicit modules/-segment forms
//
// The `../../X` form (no `modules/` segment) is what the reviewer's
// bypass exploited. The drift-detector test in `backend/test/views-registry
// .test.ts` asserts MODULE_ISOLATION_NAMES ⊇ actual subdirs/flat-files of
// views/modules/ so a new module added to the registry without being
// added here surfaces immediately.
const MODULE_NAMES = MODULE_ISOLATION_NAMES;
function patternsFor() {
const groups = [];
for (const name of MODULE_NAMES) {
groups.push(`./${name}/**`);
groups.push(`./${name}.module*`);
groups.push(`../${name}/**`);
groups.push(`../${name}.module*`);
groups.push(`../../${name}/**`);
groups.push(`../../${name}.module*`);
groups.push(`../../../${name}/**`);
groups.push(`../../../${name}.module*`);
groups.push(`../../modules/${name}/**`);
groups.push(`../../modules/${name}.module*`);
}
return [{ group: groups, message: MESSAGE }];
}
return [
{
files: ['backend/src/views/modules/**/*.ts'],
ignores: ['backend/src/views/modules/**/*.test.ts'],
rules: {
'no-restricted-imports': ['error', { patterns: patternsFor() }],
},
},
{
files: ['frontend/src/views/modules/**/*.{ts,tsx}'],
ignores: ['frontend/src/views/modules/**/*.test.{ts,tsx}'],
rules: {
'no-restricted-imports': ['error', { patterns: patternsFor() }],
},
},
];
}