@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/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,2 @@
1
+ declare module 'eslint-config-next/core-web-vitals'
2
+ declare module 'eslint-config-next/typescript'
@@ -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
+ ]