@syntopica/eslint-config 0.8.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/PUBLIC_API.md +47 -0
- package/README.md +106 -0
- package/eslint.config.ts +7 -0
- package/package.json +186 -0
- package/src/accessibility.ts +26 -0
- package/src/astro.ts +62 -0
- package/src/base.ts +252 -0
- package/src/code-quality-sonar.ts +27 -0
- package/src/code-quality.ts +119 -0
- package/src/data-files.ts +71 -0
- package/src/eslint-config-next.d.ts +2 -0
- package/src/frontend-boundaries.ts +92 -0
- package/src/nestjs.ts +25 -0
- package/src/nextjs.ts +85 -0
- package/src/node.ts +28 -0
- package/src/react-version.ts +37 -0
- package/src/tailwind.ts +64 -0
- package/src/testing.ts +35 -0
- package/src/vite-react.ts +64 -0
- package/src/vite-vue.ts +88 -0
- package/tsconfig.json +17 -0
package/src/base.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import prettier from 'eslint-config-prettier'
|
|
3
|
+
import importPlugin from 'eslint-plugin-import'
|
|
4
|
+
import unusedImports from 'eslint-plugin-unused-imports'
|
|
5
|
+
import { globalIgnores } from 'eslint/config'
|
|
6
|
+
import tseslint from 'typescript-eslint'
|
|
7
|
+
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
9
|
+
const promisePlugin = require('eslint-plugin-promise') as Record<
|
|
10
|
+
string,
|
|
11
|
+
unknown
|
|
12
|
+
>
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
14
|
+
const regexp = require('eslint-plugin-regexp') as {
|
|
15
|
+
configs: {
|
|
16
|
+
'flat/recommended': {
|
|
17
|
+
plugins: Record<string, unknown>
|
|
18
|
+
rules: Record<string, unknown>
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
23
|
+
const security = require('eslint-plugin-security') as {
|
|
24
|
+
configs: {
|
|
25
|
+
recommended: {
|
|
26
|
+
plugins: Record<string, unknown>
|
|
27
|
+
rules: Record<string, unknown>
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type SharedConfigOptions = {
|
|
33
|
+
tsconfigRootDir?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Root-level config files (eslint.config.ts, knip.config.ts, and any other
|
|
37
|
+
// *.config.{ts,mjs,js}) sit outside a template tsconfig's `include: ["src"]`,
|
|
38
|
+
// so the project service rejects them with "was not found by the project
|
|
39
|
+
// service" the moment lefthook lints a staged file that touches one.
|
|
40
|
+
// Reused below for both `allowDefaultProject` (so these globs get linted at
|
|
41
|
+
// all) and `disableTypeChecked` (so they get linted with rules that don't
|
|
42
|
+
// need real type information - see the comment at that block for why).
|
|
43
|
+
const DEFAULT_PROJECT_FILE_GLOBS = [
|
|
44
|
+
'*.config.ts',
|
|
45
|
+
'*.config.mjs',
|
|
46
|
+
'*.config.js',
|
|
47
|
+
'eslint.config.ts',
|
|
48
|
+
'knip.config.ts',
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
export const createBaseConfig = (options: SharedConfigOptions = {}) => {
|
|
52
|
+
const tsconfigRootDir = options.tsconfigRootDir ?? process.cwd()
|
|
53
|
+
|
|
54
|
+
return [
|
|
55
|
+
globalIgnores([
|
|
56
|
+
'**/node_modules/**',
|
|
57
|
+
'**/dist/**',
|
|
58
|
+
'**/build/**',
|
|
59
|
+
'**/coverage/**',
|
|
60
|
+
// Stryker's mutant sandbox: a copy of src with instrumentation woven
|
|
61
|
+
// through it. Linting it reports the instrumentation, not the code.
|
|
62
|
+
'**/.stryker-tmp/**',
|
|
63
|
+
'**/.next/**',
|
|
64
|
+
'**/.astro/**',
|
|
65
|
+
'**/.lighthouseci/**',
|
|
66
|
+
'**/out/**',
|
|
67
|
+
// Test fixtures are inputs, not code: a rule that reads the filesystem
|
|
68
|
+
// needs deliberately malformed sample files on disk, and linting them
|
|
69
|
+
// reports the very violations they exist to reproduce.
|
|
70
|
+
'**/tests/fixtures/**',
|
|
71
|
+
'**/__fixtures__/**',
|
|
72
|
+
]),
|
|
73
|
+
// Scoped to JavaScript and TypeScript. Unscoped, ESLint's core
|
|
74
|
+
// recommendations apply to every file a config touches, including the
|
|
75
|
+
// Markdown, JSON and YAML that `data-files` adds - where a core rule that
|
|
76
|
+
// reaches for `sourceCode.getAllComments()` throws rather than reporting,
|
|
77
|
+
// taking the whole lint run down with a stack trace.
|
|
78
|
+
{ ...js.configs.recommended, files: ['**/*.{js,jsx,ts,tsx,mjs,cjs}'] },
|
|
79
|
+
...tseslint.configs.strictTypeChecked.map((config) => ({
|
|
80
|
+
...config,
|
|
81
|
+
files: ['**/*.{ts,tsx}'],
|
|
82
|
+
})),
|
|
83
|
+
{
|
|
84
|
+
files: ['**/*.{ts,tsx}'],
|
|
85
|
+
languageOptions: {
|
|
86
|
+
parser: tseslint.parser,
|
|
87
|
+
ecmaVersion: 2024,
|
|
88
|
+
sourceType: 'module',
|
|
89
|
+
parserOptions: {
|
|
90
|
+
// allowDefaultProject lints DEFAULT_PROJECT_FILE_GLOBS with the
|
|
91
|
+
// default compiler options instead of a real tsconfig project.
|
|
92
|
+
// Entries must not contain `**` and are capped at 8 matched files
|
|
93
|
+
// by typescript-eslint - both satisfied by these root-level globs.
|
|
94
|
+
projectService: {
|
|
95
|
+
allowDefaultProject: DEFAULT_PROJECT_FILE_GLOBS,
|
|
96
|
+
},
|
|
97
|
+
tsconfigRootDir,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
rules: {
|
|
101
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
102
|
+
'@typescript-eslint/no-non-null-assertion': 'error',
|
|
103
|
+
'@typescript-eslint/no-floating-promises': 'error',
|
|
104
|
+
'@typescript-eslint/no-misused-promises': 'error',
|
|
105
|
+
'@typescript-eslint/consistent-type-imports': [
|
|
106
|
+
'error',
|
|
107
|
+
{ prefer: 'type-imports', fixStyle: 'separate-type-imports' },
|
|
108
|
+
],
|
|
109
|
+
'@typescript-eslint/switch-exhaustiveness-check': 'error',
|
|
110
|
+
// Prefer `type` over `interface` — consistent with the types-in-own-file convention
|
|
111
|
+
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
|
|
112
|
+
// Class properties that are never reassigned should be readonly
|
|
113
|
+
'@typescript-eslint/prefer-readonly': 'error',
|
|
114
|
+
// A function that returns a promise on one path and a value on
|
|
115
|
+
// another is awaited correctly by accident. Marking it `async`
|
|
116
|
+
// makes both paths a promise, which is what every caller assumes.
|
|
117
|
+
'@typescript-eslint/promise-function-async': 'error',
|
|
118
|
+
'no-unused-vars': 'off',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
// The default compiler options behind allowDefaultProject don't carry
|
|
123
|
+
// the real tsconfig's module resolution and lib settings, so ordinary
|
|
124
|
+
// imports here (even Node builtins like `node:path`) come back as
|
|
125
|
+
// unresolved "error" types rather than their real types. Every rule
|
|
126
|
+
// above that needs real type information (no-floating-promises,
|
|
127
|
+
// no-misused-promises, switch-exhaustiveness-check, prefer-readonly,
|
|
128
|
+
// and the strictTypeChecked rules from the block above this one) would
|
|
129
|
+
// otherwise misfire on every import in these files. disableTypeChecked
|
|
130
|
+
// only turns off rules that require type information; the purely
|
|
131
|
+
// syntactic ones (no-explicit-any, consistent-type-imports, ...) still
|
|
132
|
+
// apply.
|
|
133
|
+
files: DEFAULT_PROJECT_FILE_GLOBS,
|
|
134
|
+
...tseslint.configs.disableTypeChecked,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
files: ['**/*.{js,jsx,ts,tsx,mjs,cjs}'],
|
|
138
|
+
plugins: {
|
|
139
|
+
import: importPlugin,
|
|
140
|
+
'unused-imports': unusedImports,
|
|
141
|
+
},
|
|
142
|
+
settings: {
|
|
143
|
+
'import/resolver': { typescript: true },
|
|
144
|
+
// eslint-plugin-import defaults to ['.js', '.mjs', '.cjs'] for its
|
|
145
|
+
// own cross-file export-map resolution (separate from the parser
|
|
146
|
+
// ESLint itself uses). Without .ts/.tsx/.jsx listed here, every rule
|
|
147
|
+
// that needs that resolution -- import/no-cycle included -- silently
|
|
148
|
+
// treats every non-JS file as unresolvable and reports nothing.
|
|
149
|
+
// Mirrors the `files` glob on this config block.
|
|
150
|
+
'import/extensions': ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx'],
|
|
151
|
+
},
|
|
152
|
+
rules: {
|
|
153
|
+
// Neither is in eslint:recommended, and neither is in
|
|
154
|
+
// strictTypeChecked: `==` against anything but null is a coercion
|
|
155
|
+
// nobody wrote on purpose, and a console.log is a debugging statement
|
|
156
|
+
// that reached production. `warn` and `error` stay allowed - they are
|
|
157
|
+
// the ones that are meant to ship.
|
|
158
|
+
eqeqeq: ['error', 'smart'],
|
|
159
|
+
'no-console': ['error', { allow: ['warn', 'error'] }],
|
|
160
|
+
'import/first': 'error',
|
|
161
|
+
'import/newline-after-import': 'error',
|
|
162
|
+
'import/no-duplicates': 'error',
|
|
163
|
+
'import/no-self-import': 'error',
|
|
164
|
+
// No maxDepth: shallow caps miss exactly the long cycles that tangle
|
|
165
|
+
// large codebases. `allowUnsafeDynamicCyclicDependency` stays off.
|
|
166
|
+
'import/no-cycle': ['error', { ignoreExternal: true }],
|
|
167
|
+
'unused-imports/no-unused-imports': 'error',
|
|
168
|
+
'@typescript-eslint/no-unused-vars': 'off',
|
|
169
|
+
'unused-imports/no-unused-vars': [
|
|
170
|
+
'error',
|
|
171
|
+
{
|
|
172
|
+
args: 'after-used',
|
|
173
|
+
argsIgnorePattern: '^_',
|
|
174
|
+
varsIgnorePattern: '^_',
|
|
175
|
+
ignoreRestSiblings: true,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
// ── Promise best practices ──────────────────────────────────────────────
|
|
181
|
+
// (no-floating-promises in TS block already covers unhandled Promises)
|
|
182
|
+
{
|
|
183
|
+
files: ['**/*.{js,jsx,ts,tsx,mjs,cjs}'],
|
|
184
|
+
plugins: { promise: promisePlugin },
|
|
185
|
+
rules: {
|
|
186
|
+
// prefer async/await over .then() chaining
|
|
187
|
+
'promise/prefer-await-to-then': 'warn',
|
|
188
|
+
// prefer async/await over callback patterns
|
|
189
|
+
'promise/prefer-await-to-callbacks': 'warn',
|
|
190
|
+
// no Promise inside .then() / .catch() (Promise hell)
|
|
191
|
+
'promise/no-nesting': 'error',
|
|
192
|
+
// no return Promise.resolve(x) inside .then() — redundant
|
|
193
|
+
'promise/no-return-wrap': 'error',
|
|
194
|
+
// resolve/reject param naming convention
|
|
195
|
+
'promise/param-names': 'error',
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
// ── Regular expressions ─────────────────────────────────────────────────
|
|
199
|
+
// `security/detect-non-literal-regexp` below only notices that a pattern
|
|
200
|
+
// came from a variable. This reads the pattern itself: catastrophic
|
|
201
|
+
// backtracking (ReDoS) written as a literal, an always-true assertion, a
|
|
202
|
+
// character class that does not match what it looks like, a useless flag.
|
|
203
|
+
// A real security class, statically decidable, and with a low enough
|
|
204
|
+
// false-positive rate that the recommended set is adopted as-is.
|
|
205
|
+
{
|
|
206
|
+
files: ['**/*.{js,jsx,ts,tsx,mjs,cjs}'],
|
|
207
|
+
plugins: {
|
|
208
|
+
regexp: regexp.configs['flat/recommended'].plugins['regexp'],
|
|
209
|
+
},
|
|
210
|
+
rules: { ...regexp.configs['flat/recommended'].rules },
|
|
211
|
+
},
|
|
212
|
+
// ── Security best practices ─────────────────────────────────────────────
|
|
213
|
+
{
|
|
214
|
+
files: ['**/*.{js,jsx,ts,tsx,mjs,cjs}'],
|
|
215
|
+
plugins: { security: security.configs.recommended.plugins['security'] },
|
|
216
|
+
rules: {
|
|
217
|
+
// eval() with a variable — always dangerous
|
|
218
|
+
'security/detect-eval-with-expression': 'error',
|
|
219
|
+
// non-literal RegExp — potential ReDoS
|
|
220
|
+
'security/detect-non-literal-regexp': 'warn',
|
|
221
|
+
// fs calls with non-literal paths — potential path traversal
|
|
222
|
+
'security/detect-non-literal-fs-filename': 'warn',
|
|
223
|
+
// timing-attack-prone equality checks (passwords, tokens)
|
|
224
|
+
'security/detect-possible-timing-attacks': 'warn',
|
|
225
|
+
// Math.random() for security purposes
|
|
226
|
+
'security/detect-pseudoRandomBytes': 'error',
|
|
227
|
+
// detect-object-injection intentionally OFF — too many false positives
|
|
228
|
+
// with normal bracket-notation array/object access
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
// ── dependency-cruiser's CommonJS config ────────────────────────────────
|
|
232
|
+
// Every repo adopting the baseline's quality gates writes a
|
|
233
|
+
// `.dependency-cruiser.cjs`: the tool loads CommonJS only, while the
|
|
234
|
+
// shared factory is TypeScript, so the file is CJS in an otherwise ESM
|
|
235
|
+
// project and reaches the factory through jiti. Without this it fails on
|
|
236
|
+
// `__filename`/`require`/`module` as undefined globals. The matching
|
|
237
|
+
// rule relaxation lives in the code-policy rule itself, so no order of
|
|
238
|
+
// config composition can defeat it.
|
|
239
|
+
{
|
|
240
|
+
files: ['**/.dependency-cruiser.cjs'],
|
|
241
|
+
languageOptions: {
|
|
242
|
+
globals: {
|
|
243
|
+
__filename: 'readonly',
|
|
244
|
+
__dirname: 'readonly',
|
|
245
|
+
module: 'writable',
|
|
246
|
+
require: 'readonly',
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
prettier,
|
|
251
|
+
]
|
|
252
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import sonarjs from 'eslint-plugin-sonarjs'
|
|
2
|
+
|
|
3
|
+
/** Duplication and smell rules from eslint-plugin-sonarjs. Composed by code-quality. */
|
|
4
|
+
export const createCodeQualitySonarConfig = () => [
|
|
5
|
+
{
|
|
6
|
+
files: ['**/*.{ts,tsx,js,jsx}'],
|
|
7
|
+
plugins: { sonarjs },
|
|
8
|
+
rules: {
|
|
9
|
+
'sonarjs/no-identical-functions': 'error',
|
|
10
|
+
'sonarjs/no-duplicated-branches': 'error',
|
|
11
|
+
'sonarjs/no-duplicate-string': ['warn', { threshold: 4 }],
|
|
12
|
+
// Cyclomatic `complexity` counts branches; this counts how hard the
|
|
13
|
+
// branching is to hold in your head, so nesting costs more than a flat
|
|
14
|
+
// sequence of guards. The two disagree in the direction that matters:
|
|
15
|
+
// a function with ten early returns is easy and scores 10 on
|
|
16
|
+
// cyclomatic, while three nested loops with a condition each is hard
|
|
17
|
+
// and scores 4. 15 is SonarSource's own default.
|
|
18
|
+
// `error`, not `warn`, and the difference is not severity: every lint
|
|
19
|
+
// script runs with --max-warnings 0, so a warning fails the build
|
|
20
|
+
// anyway. ESLint's bulk suppressions only apply to errors, and existing
|
|
21
|
+
// debt has to be expressible as `eslint-suppressions.json` - a file
|
|
22
|
+
// review can see and `lint:prune` can shrink - rather than as a raised
|
|
23
|
+
// threshold nobody ever lowers again.
|
|
24
|
+
'sonarjs/cognitive-complexity': ['error', 15],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural enforcement: atomic files, no inline types, view/hook split, import policy.
|
|
3
|
+
* Complexity + size: max-lines (100 hard cap), max-lines-per-function, cyclomatic complexity.
|
|
4
|
+
*
|
|
5
|
+
* Full rationale: docs/standards/code-quality.md and
|
|
6
|
+
* docs/standards/typescript-frontend-architecture.md
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import codePolicy from 'eslint-plugin-code-policy'
|
|
10
|
+
|
|
11
|
+
import { createCodeQualitySonarConfig } from './code-quality-sonar'
|
|
12
|
+
import { createTestingConfig } from './testing'
|
|
13
|
+
|
|
14
|
+
export const createCodeQualityConfig = () => [
|
|
15
|
+
codePolicy.configs.recommended,
|
|
16
|
+
{
|
|
17
|
+
files: ['**/*.{ts,tsx,js,jsx}'],
|
|
18
|
+
rules: {
|
|
19
|
+
'max-lines': [
|
|
20
|
+
'error',
|
|
21
|
+
{ max: 100, skipBlankLines: true, skipComments: true },
|
|
22
|
+
],
|
|
23
|
+
'max-lines-per-function': [
|
|
24
|
+
'warn',
|
|
25
|
+
{ max: 50, skipBlankLines: true, skipComments: true, IIFEs: true },
|
|
26
|
+
],
|
|
27
|
+
complexity: ['warn', { max: 10 }],
|
|
28
|
+
'max-depth': ['warn', { max: 4 }],
|
|
29
|
+
'max-params': ['warn', { max: 4 }],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
...createCodeQualitySonarConfig(),
|
|
33
|
+
...createTestingConfig(),
|
|
34
|
+
// Tooling and framework entrypoints are allowed to exceed the default file budget.
|
|
35
|
+
{
|
|
36
|
+
files: [
|
|
37
|
+
'**/*.config.{ts,js,mjs,cjs}',
|
|
38
|
+
'**/eslint.config.*',
|
|
39
|
+
'**/*.setup.{ts,tsx}',
|
|
40
|
+
'**/next-env.d.ts',
|
|
41
|
+
'**/vitest.config.*',
|
|
42
|
+
'**/playwright.config.*',
|
|
43
|
+
],
|
|
44
|
+
rules: {
|
|
45
|
+
'max-lines': 'off',
|
|
46
|
+
'max-lines-per-function': 'off',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
// Globs match createTestingConfig()'s: a helper under `tests/` or `test/`
|
|
51
|
+
// that is not itself named `*.test.ts` is still test scaffolding and must
|
|
52
|
+
// be judged by the same policy, not by the production one.
|
|
53
|
+
files: [
|
|
54
|
+
'**/*.{test,spec}.{ts,tsx}',
|
|
55
|
+
'**/__tests__/**/*.{ts,tsx}',
|
|
56
|
+
'**/tests/**/*.{ts,tsx}',
|
|
57
|
+
'**/test/**/*.{ts,tsx}',
|
|
58
|
+
],
|
|
59
|
+
rules: {
|
|
60
|
+
// Tests get a real budget, not an exemption. 200 is deliberately looser
|
|
61
|
+
// than production's 100 - a test file carries arrange scaffolding its
|
|
62
|
+
// subject does not - but it is still a budget: past 200 lines a test
|
|
63
|
+
// file is covering more than one behaviour and should be split by
|
|
64
|
+
// behaviour, which is also what makes a failure easy to locate.
|
|
65
|
+
'max-lines': [
|
|
66
|
+
'error',
|
|
67
|
+
{ max: 200, skipBlankLines: true, skipComments: true },
|
|
68
|
+
],
|
|
69
|
+
// Off, not relaxed. In a test file the longest "function" is the
|
|
70
|
+
// top-level `describe` callback, so this rule measures the wrapper
|
|
71
|
+
// rather than any real complexity: 20 trivial `it` cases already report
|
|
72
|
+
// a 62-line arrow. Leaving it on would contradict the 200-line budget
|
|
73
|
+
// above and push authors to split `describe` blocks for no reason.
|
|
74
|
+
// File size is governed by max-lines; per-case size by review.
|
|
75
|
+
'max-lines-per-function': 'off',
|
|
76
|
+
// Placement stays enforced: it costs no extra code and it is what keeps
|
|
77
|
+
// a shared fixture findable. Detection is camelCase-prefix based
|
|
78
|
+
// (`useX`, `formatX`, `mapX`, ...), so a test colocated with its
|
|
79
|
+
// subject inherits the subject's folder and passes; what this actually
|
|
80
|
+
// forbids is the `tests/utils/` + `tests/helpers/` junk drawer.
|
|
81
|
+
'code-policy/file-kind-placement': 'error',
|
|
82
|
+
// Test files legitimately colocate inline fixture types, builders, and
|
|
83
|
+
// local helpers next to the cases that use them; the atomic-file/one-unit
|
|
84
|
+
// discipline targets production architecture, not test scaffolding.
|
|
85
|
+
// Enforcing these three would mean writing twice the code for the same
|
|
86
|
+
// tests - every local builder would have to be exported or extracted.
|
|
87
|
+
'code-policy/no-inline-types-in-runtime-files': 'off',
|
|
88
|
+
'code-policy/no-hidden-top-level-declarations': 'off',
|
|
89
|
+
'code-policy/one-primary-unit': 'off',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
// Next.js App Router special files often coordinate wiring and metadata.
|
|
93
|
+
{
|
|
94
|
+
files: [
|
|
95
|
+
'**/app/**/page.tsx',
|
|
96
|
+
'**/app/**/layout.tsx',
|
|
97
|
+
'**/app/**/loading.tsx',
|
|
98
|
+
'**/app/**/error.tsx',
|
|
99
|
+
'**/app/**/not-found.tsx',
|
|
100
|
+
'**/app/**/route.ts',
|
|
101
|
+
'**/app/**/template.tsx',
|
|
102
|
+
'**/app/**/default.tsx',
|
|
103
|
+
'**/src/app/**/page.tsx',
|
|
104
|
+
'**/src/app/**/layout.tsx',
|
|
105
|
+
'**/src/app/**/loading.tsx',
|
|
106
|
+
'**/src/app/**/error.tsx',
|
|
107
|
+
'**/src/app/**/not-found.tsx',
|
|
108
|
+
'**/src/app/**/route.ts',
|
|
109
|
+
'**/src/app/**/template.tsx',
|
|
110
|
+
'**/src/app/**/default.tsx',
|
|
111
|
+
],
|
|
112
|
+
rules: {
|
|
113
|
+
'max-lines': [
|
|
114
|
+
'warn',
|
|
115
|
+
{ max: 120, skipBlankLines: true, skipComments: true },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import markdown from '@eslint/markdown'
|
|
2
|
+
import jsonc from 'eslint-plugin-jsonc'
|
|
3
|
+
import yml from 'eslint-plugin-yml'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Correctness rules for the files that are not TypeScript.
|
|
7
|
+
*
|
|
8
|
+
* Prettier already makes JSON, YAML and Markdown *consistent*. Nothing made
|
|
9
|
+
* them *correct*: a duplicate key in a JSON config silently wins or loses
|
|
10
|
+
* depending on the parser, a YAML value that reads as a boolean because it is
|
|
11
|
+
* spelled `no`, a Markdown link that points at a heading that was renamed.
|
|
12
|
+
* None of that is a formatting question and none of it was caught anywhere in
|
|
13
|
+
* the estate.
|
|
14
|
+
*
|
|
15
|
+
* Formatting rules are deliberately absent from all three sets - Prettier owns
|
|
16
|
+
* layout, and a lint rule that disagrees with it produces a fight no one wins.
|
|
17
|
+
*
|
|
18
|
+
* Composed separately from `base` rather than added to it: these need their
|
|
19
|
+
* own parsers, and a project that has no YAML should not pay for one.
|
|
20
|
+
*/
|
|
21
|
+
export const createDataFilesConfig = () => [
|
|
22
|
+
...jsonc.configs['flat/recommended-with-json'].map((config) => ({
|
|
23
|
+
...config,
|
|
24
|
+
files: ['**/*.json'],
|
|
25
|
+
})),
|
|
26
|
+
...jsonc.configs['flat/recommended-with-jsonc'].map((config) => ({
|
|
27
|
+
...config,
|
|
28
|
+
files: ['**/*.jsonc', '**/tsconfig*.json', '**/.vscode/*.json'],
|
|
29
|
+
})),
|
|
30
|
+
{
|
|
31
|
+
// JSON with comments, by TypeScript's own definition: `tsconfig.json` is
|
|
32
|
+
// JSONC, and the block above sets the right parser for it but does not
|
|
33
|
+
// undo the `no-comments` the plain-JSON set turned on for `**/*.json`.
|
|
34
|
+
files: ['**/*.jsonc', '**/tsconfig*.json', '**/.vscode/*.json'],
|
|
35
|
+
rules: { 'jsonc/no-comments': 'off' },
|
|
36
|
+
},
|
|
37
|
+
...yml.configs['flat/recommended'].map((config) => ({
|
|
38
|
+
...config,
|
|
39
|
+
files: ['**/*.{yml,yaml}'],
|
|
40
|
+
})),
|
|
41
|
+
{
|
|
42
|
+
files: ['**/*.{yml,yaml}'],
|
|
43
|
+
rules: {
|
|
44
|
+
// `no`, `off`, `yes` and `on` are booleans in YAML 1.1 and strings in
|
|
45
|
+
// 1.2, and which one a reader gets depends on the parser. GitHub Actions
|
|
46
|
+
// reads `on:` as a key, Docker Compose does not. Quote the ambiguous
|
|
47
|
+
// ones and the question stops existing.
|
|
48
|
+
'yml/no-irregular-whitespace': 'error',
|
|
49
|
+
'yml/require-string-key': 'error',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
...markdown.configs.recommended.map((config) => ({
|
|
53
|
+
...config,
|
|
54
|
+
files: ['**/*.md'],
|
|
55
|
+
})),
|
|
56
|
+
{
|
|
57
|
+
files: ['**/*.md'],
|
|
58
|
+
rules: {
|
|
59
|
+
// A heading level that jumps from 2 to 4 breaks every table of contents
|
|
60
|
+
// and every screen reader's outline. Both rules are structural, not
|
|
61
|
+
// stylistic, which is why they are on and the rest of the set is not.
|
|
62
|
+
'markdown/heading-increment': 'error',
|
|
63
|
+
'markdown/no-empty-links': 'error',
|
|
64
|
+
// Off: this repository's TODO files use `- [ ]` and `- [x]` task
|
|
65
|
+
// checkboxes, which the rule reads as shortcut reference links to
|
|
66
|
+
// undefined labels. Every hit is one of those, so the rule is pure
|
|
67
|
+
// noise here rather than wrong in general.
|
|
68
|
+
'markdown/no-missing-label-refs': 'off',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import boundaries from 'eslint-plugin-boundaries'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Layered import boundaries for frontend apps (Next.js App Router, Vite React,
|
|
5
|
+
* Vite Vue, Astro with React islands).
|
|
6
|
+
*
|
|
7
|
+
* Intent:
|
|
8
|
+
* - `components/**` stays UI-oriented: may import shared code, not service internals directly.
|
|
9
|
+
* - `hooks`, `composables`, `types`, `utils`, `const`, `lib`, `store` are shared ownership layers.
|
|
10
|
+
* - `services/**` holds integrations; may import shared utilities/types, not `components`.
|
|
11
|
+
* - Root/shared code may call into `services` so hooks can orchestrate providers without
|
|
12
|
+
* leaking SDK details into TSX.
|
|
13
|
+
*
|
|
14
|
+
* @see docs/standards/typescript-frontend-architecture.md
|
|
15
|
+
*
|
|
16
|
+
* Migration note (eslint-plugin-boundaries v7):
|
|
17
|
+
* - Element patterns are folder prefixes with `partialMatch: false` (anchored at the
|
|
18
|
+
* project root); this replaces v6's `mode: "full"` + `<pattern>/**\/*` full-path globs.
|
|
19
|
+
* - The per-rule option is `policies` (renamed from `rules` in v7).
|
|
20
|
+
* - Selectors use the entity model: `{ from: { element: { types } } }` and
|
|
21
|
+
* `{ allow: { to: { element: { types: { anyOf } } } } }`.
|
|
22
|
+
*/
|
|
23
|
+
export const createFrontendBoundariesConfig = () => [
|
|
24
|
+
{
|
|
25
|
+
files: ['**/*.{js,jsx,ts,tsx,vue,mjs,cjs}'],
|
|
26
|
+
plugins: { boundaries },
|
|
27
|
+
settings: {
|
|
28
|
+
// Ensure TypeScript path aliases (e.g. @/*) resolve correctly for all file
|
|
29
|
+
// types including .vue and .astro, which are not covered by the base config's
|
|
30
|
+
// import/resolver setting (that only applies to .ts/.tsx/.js etc.).
|
|
31
|
+
'import/resolver': { typescript: true },
|
|
32
|
+
'boundaries/elements': [
|
|
33
|
+
// app layer
|
|
34
|
+
{ type: 'app', pattern: ['app', 'src/app'], partialMatch: false },
|
|
35
|
+
// components layer
|
|
36
|
+
{
|
|
37
|
+
type: 'components',
|
|
38
|
+
pattern: ['components', 'src/components'],
|
|
39
|
+
partialMatch: false,
|
|
40
|
+
},
|
|
41
|
+
// shared layer: hooks, composables, types, lib, utils, const, constants, store, stores
|
|
42
|
+
{
|
|
43
|
+
type: 'shared',
|
|
44
|
+
pattern: [
|
|
45
|
+
'hooks',
|
|
46
|
+
'src/hooks',
|
|
47
|
+
'composables',
|
|
48
|
+
'src/composables',
|
|
49
|
+
'types',
|
|
50
|
+
'src/types',
|
|
51
|
+
'lib',
|
|
52
|
+
'src/lib',
|
|
53
|
+
'utils',
|
|
54
|
+
'src/utils',
|
|
55
|
+
'const',
|
|
56
|
+
'src/const',
|
|
57
|
+
'constants',
|
|
58
|
+
'src/constants',
|
|
59
|
+
'store',
|
|
60
|
+
'src/store',
|
|
61
|
+
'stores',
|
|
62
|
+
'src/stores',
|
|
63
|
+
],
|
|
64
|
+
partialMatch: false,
|
|
65
|
+
},
|
|
66
|
+
// services layer: services, actions
|
|
67
|
+
{
|
|
68
|
+
type: 'services',
|
|
69
|
+
pattern: ['services', 'src/services', 'actions', 'src/actions'],
|
|
70
|
+
partialMatch: false,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
rules: {
|
|
75
|
+
'boundaries/dependencies': [
|
|
76
|
+
'error',
|
|
77
|
+
{
|
|
78
|
+
default: 'disallow',
|
|
79
|
+
policies: Object.entries({
|
|
80
|
+
app: ['app', 'components', 'shared', 'services'],
|
|
81
|
+
components: ['components', 'shared'],
|
|
82
|
+
shared: ['shared', 'services'],
|
|
83
|
+
services: ['services', 'shared'],
|
|
84
|
+
}).map(([from, to]) => ({
|
|
85
|
+
from: { element: { types: from } },
|
|
86
|
+
allow: { to: { element: { types: { anyOf: to } } } },
|
|
87
|
+
})),
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
]
|
package/src/nestjs.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createNodeConfig } from './node'
|
|
2
|
+
|
|
3
|
+
// NestJS service layer. Compose it like the other framework presets:
|
|
4
|
+
// createBaseConfig() + createNestjsConfig() + createCodeQualityConfig()
|
|
5
|
+
// It builds on the node preset (Node globals + prefer-node-protocol) and only
|
|
6
|
+
// adds the decorator-aware rule tweaks NestJS needs on top of the strict base.
|
|
7
|
+
export const createNestjsConfig = () => [
|
|
8
|
+
...createNodeConfig(),
|
|
9
|
+
{
|
|
10
|
+
files: ['**/*.ts'],
|
|
11
|
+
rules: {
|
|
12
|
+
// @Module(), @Injectable() and friends are intentionally empty classes
|
|
13
|
+
// whose only job is to carry a decorator; the strict base flags these as
|
|
14
|
+
// extraneous, so allow classes that exist solely to be decorated.
|
|
15
|
+
'@typescript-eslint/no-extraneous-class': [
|
|
16
|
+
'error',
|
|
17
|
+
{ allowWithDecorator: true },
|
|
18
|
+
],
|
|
19
|
+
// NestJS DI reads constructor parameter types at runtime via
|
|
20
|
+
// emitDecoratorMetadata. Forcing type-only imports would erase those
|
|
21
|
+
// class references and break injection, so keep value imports here.
|
|
22
|
+
'@typescript-eslint/consistent-type-imports': 'off',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
]
|