oxlint-plugin-arch 0.4.0 → 0.5.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.
@@ -0,0 +1,1354 @@
1
+ // src/utils/helpers/preset.ts
2
+ import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+
5
+ // src/constants/preset.constants.ts
6
+ var presetExtensions = "*.{ts,tsx,mts,cts}";
7
+ var presetPolicyOrder = [
8
+ "formatting",
9
+ "statementLayout",
10
+ "imports",
11
+ "typePlacement",
12
+ "typeSafety",
13
+ "fileLayout",
14
+ "serviceStructure",
15
+ "naming",
16
+ "comments",
17
+ "trivialFunctions",
18
+ "mutableState",
19
+ "backend",
20
+ "tests",
21
+ "schemas",
22
+ "react",
23
+ "effects",
24
+ "routes",
25
+ "forms",
26
+ "memoization",
27
+ "boundaries",
28
+ "rpcClientOwnership",
29
+ "tailwind",
30
+ "shadcn"
31
+ ];
32
+ var presetCommonRules = {
33
+ "jsx-a11y/no-noninteractive-tabindex": "error",
34
+ "no-case-declarations": "error",
35
+ "no-empty": "error",
36
+ "no-fallthrough": "error",
37
+ "no-prototype-builtins": "error",
38
+ "no-regex-spaces": "error",
39
+ "array-callback-return": "error",
40
+ "default-case-last": "error",
41
+ eqeqeq: "error",
42
+ "no-alert": "error",
43
+ "no-constructor-return": "error",
44
+ "no-else-return": "error",
45
+ "no-extend-native": "error",
46
+ "no-extra-bind": "error",
47
+ "no-extra-label": "error",
48
+ "no-labels": "error",
49
+ "no-lone-blocks": "error",
50
+ "no-new": "error",
51
+ "no-new-func": "error",
52
+ "no-new-wrappers": "error",
53
+ "no-param-reassign": "error",
54
+ "no-proto": "error",
55
+ "no-return-assign": "error",
56
+ "no-script-url": "error",
57
+ "no-self-compare": "error",
58
+ "no-sequences": "error",
59
+ "no-useless-call": "error",
60
+ "no-useless-concat": "error",
61
+ "no-useless-return": "error",
62
+ "no-useless-computed-key": "error",
63
+ "no-var": "error",
64
+ "object-shorthand": "error",
65
+ "prefer-const": "error",
66
+ "prefer-numeric-literals": "error",
67
+ "prefer-rest-params": "error",
68
+ "prefer-spread": "error",
69
+ "prefer-template": "error",
70
+ "symbol-description": "error",
71
+ "no-promise-executor-return": "error",
72
+ "no-template-curly-in-string": "error",
73
+ "new-cap": [
74
+ "error",
75
+ {
76
+ capIsNew: false
77
+ }
78
+ ],
79
+ "no-lonely-if": "error",
80
+ "no-multi-assign": "error",
81
+ "no-nested-ternary": "error",
82
+ "no-unneeded-ternary": "error",
83
+ "prefer-object-spread": "error",
84
+ "no-label-var": "error",
85
+ "no-await-in-loop": "error",
86
+ curly: "error",
87
+ "typescript/no-array-constructor": "error",
88
+ "typescript/no-dynamic-delete": "error",
89
+ "typescript/no-empty-object-type": "error",
90
+ "typescript/no-invalid-void-type": "error",
91
+ "typescript/no-mixed-enums": "error",
92
+ "typescript/no-namespace": "error",
93
+ "typescript/no-non-null-asserted-nullish-coalescing": "error",
94
+ "typescript/no-non-null-assertion": "error",
95
+ "typescript/no-require-imports": "error",
96
+ "typescript/no-unnecessary-boolean-literal-compare": "error",
97
+ "typescript/no-unnecessary-condition": "error",
98
+ "typescript/no-unnecessary-template-expression": "error",
99
+ "typescript/no-unnecessary-type-arguments": "error",
100
+ "typescript/no-unnecessary-type-assertion": "error",
101
+ "typescript/no-unnecessary-type-constraint": "error",
102
+ "typescript/no-unsafe-enum-comparison": "error",
103
+ "typescript/no-unsafe-function-type": "error",
104
+ "typescript/no-unused-expressions": "error",
105
+ "no-unused-vars": [
106
+ "error",
107
+ {
108
+ args: "after-used",
109
+ argsIgnorePattern: "^_",
110
+ ignoreRestSiblings: false,
111
+ vars: "all",
112
+ varsIgnorePattern: "^_"
113
+ }
114
+ ],
115
+ "typescript/no-useless-constructor": "error",
116
+ "typescript/prefer-literal-enum-member": "error",
117
+ "typescript/prefer-promise-reject-errors": "error",
118
+ "typescript/prefer-reduce-type-parameter": "error",
119
+ "typescript/prefer-return-this-type": "error",
120
+ "typescript/restrict-plus-operands": "error",
121
+ "typescript/return-await": ["error", "error-handling-correctness-only"],
122
+ "typescript/unbound-method": "error",
123
+ "typescript/use-unknown-in-catch-callback-variable": "error",
124
+ "typescript/unified-signatures": "error",
125
+ "typescript/adjacent-overload-signatures": "error",
126
+ "typescript/array-type": "error",
127
+ "typescript/ban-tslint-comment": "error",
128
+ "typescript/class-literal-property-style": "error",
129
+ "typescript/consistent-generic-constructors": "error",
130
+ "typescript/consistent-indexed-object-style": "error",
131
+ "typescript/consistent-type-assertions": "error",
132
+ "typescript/consistent-type-definitions": "error",
133
+ "typescript/consistent-type-imports": [
134
+ "error",
135
+ {
136
+ prefer: "type-imports",
137
+ disallowTypeAnnotations: true,
138
+ fixStyle: "separate-type-imports"
139
+ }
140
+ ],
141
+ "typescript/no-confusing-non-null-assertion": "error",
142
+ "typescript/no-empty-function": "error",
143
+ "typescript/no-inferrable-types": "error",
144
+ "typescript/non-nullable-type-assertion-style": "error",
145
+ "typescript/prefer-find": "error",
146
+ "typescript/prefer-for-of": "error",
147
+ "typescript/prefer-function-type": "error",
148
+ "typescript/prefer-includes": "error",
149
+ "typescript/prefer-nullish-coalescing": "error",
150
+ "typescript/prefer-optional-chain": "error",
151
+ "typescript/prefer-regexp-exec": "error",
152
+ "typescript/prefer-string-starts-ends-with": "error",
153
+ "typescript/consistent-type-exports": "error",
154
+ "typescript/switch-exhaustiveness-check": "error",
155
+ "typescript/default-param-last": "error",
156
+ "typescript/no-loop-func": "error",
157
+ "typescript/no-shadow": "error",
158
+ "unicorn/prefer-at": "error",
159
+ "unicorn/prefer-node-protocol": "error",
160
+ "react-hooks/rules-of-hooks": "error",
161
+ "react-hooks/exhaustive-deps": "error",
162
+ "oxc/no-accumulating-spread": "error",
163
+ "@regru/prefer-early-return/prefer-early-return": [
164
+ "error",
165
+ {
166
+ maximumStatements: 1
167
+ }
168
+ ],
169
+ "tsdoc/syntax": "error",
170
+ "@eslint-community/eslint-comments/no-aggregating-enable": "error",
171
+ "@eslint-community/eslint-comments/no-duplicate-disable": "error",
172
+ "@eslint-community/eslint-comments/no-unused-enable": "error"
173
+ };
174
+ var presetFormattingRules = {
175
+ "eslint-js/padding-line-between-statements": [
176
+ "error",
177
+ {
178
+ blankLine: "always",
179
+ prev: ["multiline-const", "multiline-let"],
180
+ next: "*"
181
+ },
182
+ {
183
+ blankLine: "always",
184
+ prev: "*",
185
+ next: ["multiline-const", "multiline-let"]
186
+ }
187
+ ],
188
+ "eslint-js/indent": [
189
+ "error",
190
+ 2,
191
+ {
192
+ MemberExpression: 1,
193
+ SwitchCase: 1,
194
+ flatTernaryExpressions: true
195
+ }
196
+ ],
197
+ "eslint-js/semi": ["error", "always"],
198
+ "eslint-js/quotes": [
199
+ "error",
200
+ "single",
201
+ {
202
+ avoidEscape: true
203
+ }
204
+ ],
205
+ "eslint-js/comma-dangle": ["error", "always-multiline"],
206
+ "eslint-js/no-trailing-spaces": "error",
207
+ "eslint-js/no-multiple-empty-lines": [
208
+ "error",
209
+ {
210
+ max: 1
211
+ }
212
+ ],
213
+ "eslint-js/eol-last": ["error", "always"],
214
+ "eslint-js/no-multi-spaces": "error",
215
+ "eslint-js/arrow-spacing": "error",
216
+ "eslint-js/block-spacing": "error",
217
+ "eslint-js/comma-spacing": [
218
+ "error",
219
+ {
220
+ before: false,
221
+ after: true
222
+ }
223
+ ],
224
+ "eslint-js/key-spacing": [
225
+ "error",
226
+ {
227
+ beforeColon: false,
228
+ afterColon: true
229
+ }
230
+ ],
231
+ "eslint-js/keyword-spacing": [
232
+ "error",
233
+ {
234
+ before: true,
235
+ after: true
236
+ }
237
+ ],
238
+ "eslint-js/object-curly-spacing": ["error", "always"],
239
+ "eslint-js/semi-spacing": [
240
+ "error",
241
+ {
242
+ before: false,
243
+ after: true
244
+ }
245
+ ],
246
+ "eslint-js/space-before-blocks": "error",
247
+ "eslint-js/space-before-function-paren": [
248
+ "error",
249
+ {
250
+ anonymous: "always",
251
+ named: "never",
252
+ asyncArrow: "always"
253
+ }
254
+ ],
255
+ "eslint-js/space-infix-ops": "error",
256
+ "eslint-js/space-in-parens": ["error", "never"],
257
+ "eslint-js/template-curly-spacing": ["error", "never"],
258
+ "eslint-js/rest-spread-spacing": ["error", "never"],
259
+ "eslint-js/computed-property-spacing": ["error", "never"],
260
+ "eslint-js/brace-style": [
261
+ "error",
262
+ "1tbs",
263
+ {
264
+ allowSingleLine: false
265
+ }
266
+ ],
267
+ "eslint-js/arrow-parens": ["error", "always"],
268
+ "eslint-js/no-extra-semi": "error",
269
+ "eslint-js/no-floating-decimal": "error",
270
+ "eslint-js/dot-location": ["error", "property"],
271
+ "eslint-js/no-whitespace-before-property": "error",
272
+ "eslint-js/object-curly-newline": [
273
+ "error",
274
+ {
275
+ consistent: true
276
+ }
277
+ ],
278
+ "eslint-js/object-property-newline": [
279
+ "error",
280
+ {
281
+ allowAllPropertiesOnSameLine: true
282
+ }
283
+ ],
284
+ "eslint-js/max-len": [
285
+ "error",
286
+ {
287
+ code: 160,
288
+ ignoreUrls: true,
289
+ ignoreStrings: true,
290
+ ignoreTemplateLiterals: true,
291
+ ignoreRegExpLiterals: true,
292
+ ignoreComments: true
293
+ }
294
+ ]
295
+ };
296
+ var presetReactRules = {
297
+ "react/display-name": "error",
298
+ "react/jsx-no-comment-textnodes": "error",
299
+ "react/jsx-no-target-blank": "error",
300
+ "react/no-unknown-property": "error",
301
+ "react/button-has-type": "error",
302
+ "react/jsx-boolean-value": "error",
303
+ "react/jsx-curly-brace-presence": "error",
304
+ "react/jsx-fragments": "error",
305
+ "react/jsx-no-useless-fragment": "error",
306
+ "react/jsx-pascal-case": "error",
307
+ "react/self-closing-comp": "error",
308
+ "react-perf/jsx-no-new-object-as-prop": "off",
309
+ "react-perf/jsx-no-new-array-as-prop": "off",
310
+ "react-perf/jsx-no-new-function-as-prop": "off",
311
+ "react-perf/jsx-no-jsx-as-prop": "off",
312
+ "react-extra/jsx-sort-props": [
313
+ "error",
314
+ {
315
+ ignoreCase: true,
316
+ callbacksLast: true,
317
+ shorthandFirst: true,
318
+ multiline: "ignore",
319
+ locale: "auto"
320
+ }
321
+ ],
322
+ "react-extra/jsx-first-prop-new-line": ["error", "multiline"],
323
+ "react-extra/jsx-max-props-per-line": [
324
+ "error",
325
+ {
326
+ when: "multiline",
327
+ maximum: 1
328
+ }
329
+ ],
330
+ "react-extra/jsx-indent-props": ["error", 2],
331
+ "react-extra/jsx-closing-bracket-location": ["error", "line-aligned"],
332
+ "react-extra/jsx-wrap-multilines": [
333
+ "error",
334
+ {
335
+ declaration: "parens-new-line",
336
+ assignment: "parens-new-line",
337
+ return: "parens-new-line",
338
+ arrow: "parens-new-line",
339
+ condition: "parens-new-line",
340
+ logical: "parens-new-line",
341
+ prop: "parens-new-line"
342
+ }
343
+ ],
344
+ "react-extra/jsx-curly-newline": ["error", "consistent"],
345
+ "react-extra/jsx-no-leaked-render": "error",
346
+ "react-extra/no-unstable-nested-components": "error",
347
+ "react-extra/function-component-definition": [
348
+ "error",
349
+ {
350
+ namedComponents: "function-declaration"
351
+ }
352
+ ],
353
+ "react-extra/hook-use-state": "error",
354
+ "react-extra/no-deprecated": "error",
355
+ "react-extra/jsx-uses-vars": "off",
356
+ "react-extra/require-render-return": "error",
357
+ "svg-jsx/camel-case-colon": "error",
358
+ "svg-jsx/no-style-string": "error"
359
+ };
360
+ var presetEffectsRules = {
361
+ "react-you-might-not-need-an-effect/no-adjust-state-on-prop-change": "error",
362
+ "react-you-might-not-need-an-effect/no-reset-all-state-on-prop-change": "error",
363
+ "react-you-might-not-need-an-effect/no-event-handler": "error",
364
+ "react-you-might-not-need-an-effect/no-pass-live-state-to-parent": "error",
365
+ "react-you-might-not-need-an-effect/no-pass-data-to-parent": "error",
366
+ "react-you-might-not-need-an-effect/no-initialize-state": "error",
367
+ "react-you-might-not-need-an-effect/no-chain-state-updates": "error",
368
+ "react-you-might-not-need-an-effect/no-derived-state": "error"
369
+ };
370
+ var presetTailwindRules = {
371
+ "tailwindcss/enforce-canonical": "error",
372
+ "tailwindcss/enforce-sort-order": "error",
373
+ "tailwindcss/enforce-shorthand": "error",
374
+ "tailwindcss/no-conflicting-classes": "error",
375
+ "tailwindcss/no-duplicate-classes": "error",
376
+ "tailwindcss/no-unknown-classes": "off",
377
+ "tailwindcss/no-unnecessary-whitespace": "error",
378
+ "tailwindcss/no-deprecated-classes": "error"
379
+ };
380
+ var presetImportRules = {
381
+ "import/first": "error",
382
+ "import/no-absolute-path": "error",
383
+ "import/no-mutable-exports": "error",
384
+ "import/no-self-import": "error",
385
+ "import/no-duplicates": "error",
386
+ "import/no-named-as-default": "error",
387
+ "import/no-named-as-default-member": "error",
388
+ "import/consistent-type-specifier-style": "error",
389
+ "unused-imports/no-unused-imports": "error",
390
+ "import-extra/no-useless-path-segments": "error",
391
+ "import-extra/no-relative-packages": "error",
392
+ "import-extra/newline-after-import": "error",
393
+ "perfectionist/sort-named-imports": [
394
+ "error",
395
+ {
396
+ type: "natural",
397
+ ignoreCase: true
398
+ }
399
+ ]
400
+ };
401
+ var presetNamingRules = {
402
+ "unicorn/filename-case": [
403
+ "error",
404
+ {
405
+ case: "kebabCase"
406
+ }
407
+ ]
408
+ };
409
+ var presetTestRules = {
410
+ "jest/expect-expect": "off",
411
+ "jest/no-conditional-expect": "off",
412
+ "jest/no-disabled-tests": "off",
413
+ "jest/no-export": "off",
414
+ "jest/no-standalone-expect": "off",
415
+ "jest/require-to-throw-message": "off",
416
+ "vitest/no-standalone-expect": [
417
+ "error",
418
+ {
419
+ additionalTestBlockFunctions: ["test"]
420
+ }
421
+ ],
422
+ "vitest/expect-expect": [
423
+ "error",
424
+ {
425
+ additionalTestBlockFunctions: ["test"]
426
+ }
427
+ ],
428
+ "vitest/require-mock-type-parameters": "error"
429
+ };
430
+ var presetExportPrefix = {
431
+ stem: "before-first-dot",
432
+ trailingRoles: ["constants", "utils", "handler", "service", "types"],
433
+ roleSeparators: [".", "-"],
434
+ normalize: "remove-separators",
435
+ allowPattern: "^(make[A-Z]|\\*$)"
436
+ };
437
+ var presetGeneratedFiles = [
438
+ "**/*.generated.*",
439
+ "**/*.gen.*",
440
+ "**/*.d.ts",
441
+ "**/routes.tree.ts",
442
+ "**/routeTree.gen.ts",
443
+ "**/paraglide/**"
444
+ ];
445
+
446
+ // src/utils/helpers/preset.ts
447
+ function presetScopes(roots, suffix = `**/${presetExtensions}`) {
448
+ return roots.map((root) => `${root}/${suffix}`);
449
+ }
450
+ function presetOverride(files, rules, excludeFiles = []) {
451
+ return {
452
+ files,
453
+ excludeFiles: [...presetGeneratedFiles, ...excludeFiles],
454
+ rules
455
+ };
456
+ }
457
+ function presetPackageName(root, directory) {
458
+ try {
459
+ const manifest = JSON.parse(readFileSync(resolve(root, directory, "package.json"), "utf8"));
460
+ return typeof manifest.name === "string" ? manifest.name : undefined;
461
+ } catch {
462
+ return;
463
+ }
464
+ }
465
+ function presetSeverity(rules, level) {
466
+ const result = structuredClone(rules);
467
+ for (const [name, value] of Object.entries(result)) {
468
+ const current = Array.isArray(value) ? value[0] : value;
469
+ if (current === "off" || current === "allow" || current === 0) {
470
+ continue;
471
+ }
472
+ result[name] = Array.isArray(value) ? [level, ...value.slice(1)] : level;
473
+ }
474
+ return result;
475
+ }
476
+
477
+ // src/configs/source.ts
478
+ function presetSourceConfig(context) {
479
+ const { options, web, scripts, app, appFiles, backendFiles, ui, safetyFiles, publicEntrypoints, baseImports } = context;
480
+ const policies = {};
481
+ policies.formatting = [presetOverride([`**/${presetExtensions}`], presetFormattingRules)];
482
+ policies.statementLayout = [
483
+ presetOverride([`**/${presetExtensions}`], {
484
+ "arch/padding-between-statements": ["error", { multilineVariables: true }],
485
+ "arch/object-multiline": ["error", { minProperties: 3, scope: "call-args" }],
486
+ "arch/call-array-multiline": ["error", { callees: ["Promise.all"], minElements: 2 }],
487
+ "arch/key-value-same-line": "error"
488
+ })
489
+ ];
490
+ policies.imports = [
491
+ presetOverride([`**/${presetExtensions}`], {
492
+ ...presetImportRules,
493
+ "arch/prefer-namespace-type-import": ["error", { max: 3 }],
494
+ "no-restricted-imports": ["error", { patterns: baseImports }],
495
+ "import-extra/no-relative-packages": "error",
496
+ "perfectionist/sort-imports": [
497
+ "error",
498
+ {
499
+ type: "natural",
500
+ ignoreCase: true,
501
+ newlinesBetween: 0,
502
+ environment: "bun",
503
+ internalPattern: options.imports?.internalSortPatterns ?? [
504
+ "^@/.*",
505
+ "^#.*",
506
+ "^~icons/.*",
507
+ ...publicEntrypoints.map((path) => `^${path.split("/").slice(0, 2).join("/")}/`)
508
+ ],
509
+ groups: [["builtin", "external"], "internal", ["parent", "sibling", "index"], "type"]
510
+ }
511
+ ]
512
+ })
513
+ ];
514
+ policies.comments = [
515
+ presetOverride(appFiles, {
516
+ "arch/no-comments": ["error", { allowWhy: false, allowPatterns: ["^\\s*SAFETY: "] }]
517
+ }, ui),
518
+ presetOverride(presetScopes(app, "**/*.{types,constants}.ts"), {
519
+ "arch/no-comments": ["error", { allowWhy: false }]
520
+ }),
521
+ presetOverride([`**/${presetExtensions}`], {
522
+ "modules/reasoned-directives": "error"
523
+ })
524
+ ];
525
+ policies.trivialFunctions = [
526
+ presetOverride(appFiles, {
527
+ "arch/no-trivial-functions": ["error", { mode: "precise", allowAsync: false, allowCallees: [] }]
528
+ }, ui)
529
+ ];
530
+ policies.trivialFunctions.push(presetOverride([...presetScopes(web, "services/**/*.{client,server,rsc}.ts"), ...presetScopes(web, "services/**/*.rsc.tsx")], {
531
+ "arch/no-trivial-functions": [
532
+ "error",
533
+ { mode: "precise", allowAsync: false, allowCallees: [], checkServiceMethods: true }
534
+ ]
535
+ }, ui));
536
+ policies.mutableState = [
537
+ presetOverride(safetyFiles, { "arch/no-module-mutable-state": ["error", { kinds: ["let", "var"] }] }, ui)
538
+ ];
539
+ policies.backend = [
540
+ presetOverride(backendFiles, { "modules/backend-switch": "error" }, options.cliFiles),
541
+ presetOverride([`**/${presetExtensions}`], { "no-console": "error" }, [
542
+ ...presetScopes(scripts, "**"),
543
+ ...options.cliFiles ?? []
544
+ ]),
545
+ presetOverride(safetyFiles, { "modules/double-negation": "error" }, ui)
546
+ ];
547
+ return policies;
548
+ }
549
+
550
+ // src/configs/types.ts
551
+ function presetTypesConfig(context) {
552
+ const {
553
+ options,
554
+ web,
555
+ backend,
556
+ app,
557
+ appFiles,
558
+ webFiles,
559
+ services,
560
+ utilities,
561
+ types,
562
+ ui,
563
+ testFiles,
564
+ safetyFiles,
565
+ components,
566
+ hooks,
567
+ publicEntrypoints
568
+ } = context;
569
+ const policies = {};
570
+ policies.typePlacement = [
571
+ presetOverride(appFiles, { "arch/no-type-declarations": "error" }, [
572
+ ...types,
573
+ ...components,
574
+ ...hooks,
575
+ ...services,
576
+ ...ui,
577
+ ...testFiles
578
+ ]),
579
+ presetOverride(services, { "modules/service-types": "error" }, [...utilities, ...testFiles]),
580
+ presetOverride([...components, ...hooks], {
581
+ "modules/local-type-alias": "error",
582
+ "modules/pascal-interface": "error",
583
+ "arch/declaration-name": [
584
+ "error",
585
+ {
586
+ kinds: ["interface"],
587
+ stem: "before-first-dot",
588
+ normalize: "remove-separators"
589
+ }
590
+ ]
591
+ }, [...ui, ...testFiles]),
592
+ presetOverride(presetScopes(app, "**/*.types.ts"), {
593
+ "arch/no-restricted-files": ["error", { message: "Domain types belong in src/types." }]
594
+ }, [...types, ...ui]),
595
+ presetOverride(presetScopes(app, "**/*.{utils,constants}.ts"), {
596
+ "arch/no-type-declarations": "error"
597
+ }, [...ui, ...testFiles]),
598
+ presetOverride([...presetScopes(backend, "types/**/*.types.ts"), ...presetScopes(app, "**/*.constants.ts")], {
599
+ "arch/no-top-level-functions": ["error", { banReExports: false }]
600
+ }),
601
+ presetOverride(presetScopes(web, "types/**/*.types.ts"), {
602
+ "arch/no-runtime-in-types": ["error", { runtimeImports: "ban", allowImportSources: [], banReExports: true }]
603
+ })
604
+ ];
605
+ policies.typeSafety = [
606
+ presetOverride(safetyFiles, {
607
+ "typescript/no-explicit-any": "error",
608
+ "typescript/ban-ts-comment": [
609
+ "error",
610
+ {
611
+ "ts-ignore": true,
612
+ "ts-nocheck": true,
613
+ "ts-expect-error": true,
614
+ "ts-check": false
615
+ }
616
+ ],
617
+ "modules/no-unknown": "error",
618
+ "modules/shape-suffix": "error",
619
+ "arch/no-imported-type-alias": "error",
620
+ "arch/no-literal-in": "error",
621
+ "arch/no-promise-all-mutation": "error",
622
+ "arch/no-restricted-constructor": ["error", { constructors: ["Error"], checkCalls: true }],
623
+ "dillon-anti-slop/no-chained-type-assertions": "error",
624
+ "dillon-anti-slop/no-conditional-empty-object-spread": "error",
625
+ "dillon-anti-slop/no-known-value-widening": "error",
626
+ "dillon-anti-slop/no-reflect-apply": "error",
627
+ "dillon-anti-slop/no-reflect-get": "error",
628
+ "dillon-anti-slop/no-runtime-typeof": "error",
629
+ "dillon-anti-slop/no-unknown-parameters": "error",
630
+ "dillon-anti-slop/no-unknown-returns": "error",
631
+ "dillon-anti-slop/no-unknown-type-aliases": "error",
632
+ "dillon-anti-slop/no-unsafe-dictionary-type": "error",
633
+ "dillon-anti-slop/no-widen-then-assert": "error",
634
+ "dillon-anti-slop/require-safety-comment-for-type-assertion": "error"
635
+ })
636
+ ];
637
+ policies.schemas = [
638
+ presetOverride(appFiles, {
639
+ "arch/no-local-schema-construction": ["error", { packages: ["zod"], namespaces: ["z"] }]
640
+ }, [...presetScopes(backend, "types/**"), ...ui]),
641
+ presetOverride(webFiles, {
642
+ "arch/no-rederive-schema": [
643
+ "error",
644
+ {
645
+ from: publicEntrypoints,
646
+ namespaces: ["z"],
647
+ operators: ["infer", "input"]
648
+ }
649
+ ]
650
+ }),
651
+ presetOverride(presetScopes(backend, "services/**/*.ts"), {
652
+ "arch/no-unescaped-like": [
653
+ "error",
654
+ {
655
+ methods: ["like", "ilike"],
656
+ operatorMethods: ["where"],
657
+ sanitizers: options.sql?.likeSanitizers ?? ["escapeLikeWildcards"],
658
+ allowSanitizedBindings: true
659
+ }
660
+ ]
661
+ }),
662
+ ...options.orpc?.publicProcedureFiles?.length ? [
663
+ presetOverride(options.orpc?.publicProcedureFiles, {
664
+ "arch/require-orpc-output": [
665
+ "error",
666
+ {
667
+ handlerMethod: "handler",
668
+ outputMethod: "output",
669
+ composers: options.orpc?.outputSchemaComposers ?? []
670
+ }
671
+ ]
672
+ })
673
+ ] : []
674
+ ];
675
+ return policies;
676
+ }
677
+
678
+ // src/configs/modules.ts
679
+ function presetModulesConfig(context) {
680
+ const { web, backend, app, services, utilities, ui, testFiles, components, hooks, options } = context;
681
+ const conceptFiles = (options.modules?.customFileRoles ?? []).map((role) => `**/*.${role}.ts`);
682
+ const frontendEntries = [
683
+ ...presetScopes(web, "services/**/*.{client,server,rsc}.ts"),
684
+ ...presetScopes(web, "services/**/*.rsc.tsx")
685
+ ];
686
+ const policies = {};
687
+ policies.fileLayout = [
688
+ ...context.folders.flatMap(({ path, folder, mode }) => [
689
+ presetOverride([
690
+ mode === "flat" ? `${path}/*/**/${presetExtensions}` : `${path}/${presetExtensions}`,
691
+ ...mode === "domain" ? [`${path}/*/*/**/${presetExtensions}`] : []
692
+ ], {
693
+ "arch/no-restricted-files": [
694
+ "error",
695
+ {
696
+ message: mode === "flat" ? "This folder uses a flat layout; subfolders are forbidden." : "This folder uses exactly one owning domain directory; loose and deeper files are forbidden."
697
+ }
698
+ ]
699
+ }),
700
+ ...mode === "domain" ? [
701
+ presetOverride([`${path}/*/${presetExtensions}`], {
702
+ "arch/folder-prefix": [
703
+ "error",
704
+ {
705
+ singularize: folder === "components" ? "trailing-s" : "none",
706
+ separators: [".", "-"],
707
+ ...folder === "hooks" ? { stripPrefixes: ["use-"] } : {}
708
+ }
709
+ ]
710
+ })
711
+ ] : []
712
+ ]),
713
+ presetOverride(presetScopes(web.filter((path) => !context.folders.some((scope) => scope.path === path + "/components")), `components/*/*/**/${presetExtensions}`), {
714
+ "arch/no-restricted-files": ["error", { message: "Components allow one group folder." }]
715
+ }, ui),
716
+ presetOverride([...presetScopes(web, "components/**/*.ts"), ...presetScopes(web, "hooks/**/*.tsx")], {
717
+ "arch/no-restricted-files": ["error", { message: "Components use TSX; hooks use .ts files." }]
718
+ }, ui),
719
+ presetOverride(presetScopes(web, "**/use-*.{ts,tsx}"), {
720
+ "arch/no-restricted-files": ["error", { message: "Hooks belong in src/hooks." }]
721
+ }, [...presetScopes(web, "hooks/**"), ...ui]),
722
+ presetOverride(presetScopes(web, "services/**/*.{ts,tsx}"), {
723
+ "arch/no-restricted-files": [
724
+ "error",
725
+ {
726
+ message: "Frontend services use .client.ts, .server.ts or .rsc.{ts,tsx}; shared domain helpers use .utils.ts and values use .constants.ts."
727
+ }
728
+ ]
729
+ }, ["**/*.{client,server,rsc,utils,constants}.ts", "**/*.rsc.tsx", ...conceptFiles])
730
+ ];
731
+ policies.serviceStructure = [
732
+ presetOverride(services, {
733
+ "modules/service-functions": "error",
734
+ "arch/no-inline-types": [
735
+ "error",
736
+ {
737
+ parameters: true,
738
+ returns: true,
739
+ functionTypes: true,
740
+ minMembers: 1
741
+ }
742
+ ]
743
+ }, [...utilities, ...testFiles, ...conceptFiles]),
744
+ presetOverride(frontendEntries, {
745
+ "modules/service-functions": ["error", { frontend: true }]
746
+ }, testFiles),
747
+ ...conceptFiles.map((pattern) => presetOverride(presetScopes(app, `services/${pattern}`), {
748
+ "modules/service-functions": "off",
749
+ "arch/export-file-prefix": [
750
+ "error",
751
+ {
752
+ stem: "full-basename",
753
+ allFunctions: true
754
+ }
755
+ ]
756
+ }, testFiles)),
757
+ presetOverride([
758
+ ...presetScopes(app, "services/**/*-{action,query}.*.ts"),
759
+ ...presetScopes(web, "services/**/*-{action,query}.*.rsc.tsx")
760
+ ], {
761
+ "modules/service-functions": [
762
+ "error",
763
+ {
764
+ allowLocalHelpers: true,
765
+ singleExport: true,
766
+ message: "Action/query files export exactly one named operation. Keep helpers used only by this operation private inside its function; do not export them. Move a helper to the module's .utils.ts only when it is genuinely shared, not merely to satisfy lint."
767
+ }
768
+ ]
769
+ }, testFiles),
770
+ presetOverride(presetScopes(backend, "services/**/*.ts"), { "modules/domain-constants": "error" }, [
771
+ ...utilities,
772
+ ...testFiles
773
+ ]),
774
+ presetOverride(presetScopes(web, "services/**/*.{ts,tsx}"), {
775
+ "modules/domain-constants": ["error", { includeData: true }]
776
+ }, ["**/*.constants.ts", ...testFiles]),
777
+ presetOverride(frontendEntries, {
778
+ "modules/domain-constants": ["error", { includeData: true, allowServiceMethods: true }]
779
+ }, testFiles),
780
+ presetOverride([...presetScopes(backend, "services/**/*-{action,query}.*.ts"), ...frontendEntries], { "arch/require-object-params": ["error", { maxParams: 2 }] }, testFiles),
781
+ presetOverride(frontendEntries, {
782
+ "arch/require-object-params": ["error", { maxParams: 2, serviceMethods: true }]
783
+ }, testFiles),
784
+ presetOverride(presetScopes(backend, "services/**/*.service.ts"), {
785
+ "arch/no-extra-exports": [
786
+ "error",
787
+ {
788
+ names: ["make{Domain}Service", "{Domain}Service"],
789
+ domainStem: "before-first-dot",
790
+ allowTypeExports: false
791
+ }
792
+ ]
793
+ }, testFiles),
794
+ presetOverride(presetScopes(app, "services/**/{actions,queries}/**/*.ts"), {
795
+ "arch/no-restricted-files": ["error", { message: "Use domain-action.name.ts or domain-query.name.ts." }]
796
+ })
797
+ ];
798
+ policies.naming = [
799
+ presetOverride([`**/${presetExtensions}`], presetNamingRules),
800
+ presetOverride(presetScopes(app, "types/**/*.ts"), {
801
+ "arch/filename-match": [
802
+ "error",
803
+ {
804
+ pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*[.]types[.]ts$",
805
+ message: "Use domain.types.ts filenames."
806
+ }
807
+ ]
808
+ }, ["**/*.constants.ts"]),
809
+ presetOverride(presetScopes(app, "**/*.{constants,utils,handler,service,types,client,server,rsc}.ts"), { "arch/export-file-prefix": ["error", presetExportPrefix] }, [...ui, ...presetScopes(backend, "entry.*.ts")]),
810
+ presetOverride(frontendEntries, {
811
+ "arch/export-file-prefix": [
812
+ "error",
813
+ { ...presetExportPrefix, allFunctions: true, serviceMethods: true, allowPattern: "^\\*$" }
814
+ ]
815
+ }, testFiles),
816
+ presetOverride(presetScopes(web, "services/**/*.constants.ts"), {
817
+ "arch/export-file-prefix": ["error", { ...presetExportPrefix, allDeclarations: true, allowPattern: "^\\*$" }]
818
+ }, testFiles),
819
+ presetOverride(presetScopes(app, "**/*.utils.ts"), {
820
+ "arch/export-file-prefix": [
821
+ "error",
822
+ {
823
+ ...presetExportPrefix,
824
+ allFunctions: true,
825
+ allowPattern: "^\\*$"
826
+ }
827
+ ]
828
+ }, ui),
829
+ presetOverride(hooks, {
830
+ "arch/filename-match": [
831
+ "error",
832
+ {
833
+ pattern: "^use-[a-z0-9]+(?:-[a-z0-9]+)*[.]ts$",
834
+ message: "Hooks use use-domain.ts filenames."
835
+ }
836
+ ],
837
+ "arch/export-file-prefix": "error",
838
+ "arch/export-name-pattern": ["error", { pattern: "^use[A-Z]", ignoreTypeExports: true }]
839
+ }),
840
+ presetOverride(components, {
841
+ "arch/only-export-components": ["error", { matchFileName: true, allowTypeExports: true }]
842
+ }, [...ui, ...testFiles]),
843
+ presetOverride(presetScopes(web.filter((path) => !context.folders.some((scope) => scope.path === path + "/components")), "components/**/*.tsx"), {
844
+ "arch/folder-prefix": ["error", { singularize: "trailing-s", separators: ["-"] }]
845
+ }, [...ui, ...testFiles]),
846
+ ...["action", "query"].map((role) => presetOverride([...presetScopes(app, `services/**/*-${role}.*.ts`), ...presetScopes(web, `services/**/*-${role}.*.rsc.tsx`)], {
847
+ "arch/filename-match": [
848
+ "error",
849
+ {
850
+ pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*-${role}\\.[a-z0-9]+(?:-[a-z0-9]+)*\\.ts$`,
851
+ message: "Use domain-action.name.ts or domain-query.name.ts."
852
+ }
853
+ ],
854
+ "arch/export-file-prefix": [
855
+ "error",
856
+ {
857
+ ...presetExportPrefix,
858
+ trailingRoles: [role],
859
+ allFunctions: true,
860
+ allowPattern: "^\\*$"
861
+ }
862
+ ],
863
+ "arch/filename-export-name": [
864
+ "error",
865
+ {
866
+ file: `{domain}-${role}.{name}.ts`,
867
+ export: `{domain}${role === "action" ? "Action" : "Query"}{Name}`,
868
+ placeholderPattern: "[a-z0-9-]+",
869
+ camelCase: true
870
+ }
871
+ ]
872
+ })),
873
+ ...["action", "query"].flatMap((role) => ["client.ts", "server.ts", "rsc.ts", "rsc.tsx"].map((suffix) => presetOverride(presetScopes(web, `services/**/*-${role}.*.${suffix}`), {
874
+ "arch/filename-match": [
875
+ "error",
876
+ {
877
+ pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*-${role}\\.[a-z0-9]+(?:-[a-z0-9]+)*\\.${suffix.replace(".", "\\.")}$`,
878
+ message: "Frontend operations retain a client, server or rsc runtime suffix."
879
+ }
880
+ ],
881
+ "arch/filename-export-name": [
882
+ "error",
883
+ {
884
+ file: `{domain}-${role}.{name}.${suffix}`,
885
+ export: `{domain}${role === "action" ? "Action" : "Query"}{Name}`,
886
+ placeholderPattern: "[a-z0-9-]+",
887
+ camelCase: true
888
+ }
889
+ ]
890
+ }))),
891
+ presetOverride(presetScopes(web, "services/**/*.rsc.{ts,tsx}"), {
892
+ "arch/export-name-pattern": ["error", { pattern: "^[a-z][a-z0-9]*Rsc[A-Z][a-zA-Z0-9]*$" }]
893
+ }, ["**/*-{action,query}.*.rsc.{ts,tsx}"])
894
+ ];
895
+ return policies;
896
+ }
897
+
898
+ // src/configs/tests.ts
899
+ function presetTestsConfig(context) {
900
+ const { backend, testFiles, baseImports } = context;
901
+ const policies = {};
902
+ policies.tests = [
903
+ presetOverride(testFiles, {
904
+ ...presetTestRules,
905
+ "dillon-anti-slop/no-module-mocking": "error",
906
+ "modules/test-modifiers": "error",
907
+ "arch/test-title-pattern": ["error", { forbid: "^should\\b", flags: "i" }]
908
+ }),
909
+ presetOverride(presetScopes(backend.map((path) => path.replace(/\/src$/, "")), "tests/**/*.ts"), {
910
+ "modules/concurrent-db": "error",
911
+ "no-restricted-imports": [
912
+ "error",
913
+ {
914
+ patterns: [
915
+ ...baseImports,
916
+ {
917
+ group: ["**/helpers/**"],
918
+ message: "Use named fixtures or tests/support."
919
+ }
920
+ ]
921
+ }
922
+ ]
923
+ })
924
+ ];
925
+ return policies;
926
+ }
927
+
928
+ // src/configs/react.ts
929
+ function presetReactConfig(context) {
930
+ const { options, web, webFiles, ui, testFiles, components, css } = context;
931
+ const policies = {};
932
+ if (web.length) {
933
+ policies.react = [
934
+ presetOverride(webFiles, {
935
+ ...presetReactRules,
936
+ ...options.react?.compiler === false ? {
937
+ "react-perf/jsx-no-new-object-as-prop": "error",
938
+ "react-perf/jsx-no-new-array-as-prop": "error",
939
+ "react-perf/jsx-no-new-function-as-prop": "error",
940
+ "react-perf/jsx-no-jsx-as-prop": "error"
941
+ } : {},
942
+ "arch/jsx-attributes-multiline": ["error", { minAttributes: 3 }]
943
+ }, ui),
944
+ presetOverride(components, {
945
+ "arch/no-file-level-helpers": ["error", { detectComponents: true, hookPattern: "^use[A-Z]" }]
946
+ }, [...ui, ...testFiles])
947
+ ];
948
+ }
949
+ if (web.length) {
950
+ policies.effects = [presetOverride(webFiles, { ...presetEffectsRules, "modules/empty-effect": "error" }, ui)];
951
+ }
952
+ if (web.length) {
953
+ policies.routes = [
954
+ presetOverride(presetScopes(web, "routes/**/*.{ts,tsx}"), {
955
+ "arch/route-surface": [
956
+ "error",
957
+ {
958
+ exportName: "Route",
959
+ bannedHooks: ["useState", "useEffect", "useMutation"],
960
+ banIntrinsicJsx: true
961
+ }
962
+ ]
963
+ }, testFiles),
964
+ presetOverride(presetScopes(web, "routes/handlers/**/*.ts"), {
965
+ "arch/no-file-level-helpers": [
966
+ "error",
967
+ {
968
+ detectComponents: false,
969
+ message: "Nest this helper in its owning route handler callback, or move genuine domain logic to a service. Do not create a component, hook or public helper to bypass this rule."
970
+ }
971
+ ]
972
+ })
973
+ ];
974
+ }
975
+ if (web.length) {
976
+ policies.forms = [
977
+ presetOverride(webFiles, {
978
+ "arch/require-paired-call": [
979
+ "error",
980
+ {
981
+ when: "useForm",
982
+ require: options.forms?.schemaResolver ?? "standardSchemaResolver"
983
+ }
984
+ ]
985
+ }, ui)
986
+ ];
987
+ }
988
+ if (web.length && options.react?.compiler !== false) {
989
+ policies.memoization = [presetOverride(webFiles, { "modules/memoization": "error" }, ui)];
990
+ }
991
+ if (web.length) {
992
+ policies.rpcClientOwnership = [
993
+ presetOverride(webFiles, {
994
+ "arch/no-restricted-token": [
995
+ "error",
996
+ {
997
+ restrictions: [
998
+ {
999
+ token: "RouterClient",
1000
+ allowIn: [...options.orpc?.clientOwnerFile ? [options.orpc?.clientOwnerFile] : context.rpcClients]
1001
+ },
1002
+ {
1003
+ token: "createContext",
1004
+ allowPathPatterns: [...presetScopes(web, "context/"), ...presetScopes(web, "components/ui/")]
1005
+ }
1006
+ ]
1007
+ }
1008
+ ]
1009
+ })
1010
+ ];
1011
+ }
1012
+ if (Object.keys(css).length) {
1013
+ policies.tailwind = Object.entries(css).map(([path, entryPoint]) => presetOverride(presetScopes([path]), Object.fromEntries(Object.entries(presetTailwindRules).map(([rule, severity]) => [
1014
+ rule,
1015
+ rule === "tailwindcss/no-duplicate-classes" || rule === "tailwindcss/no-unnecessary-whitespace" ? severity : [severity, { entryPoint }]
1016
+ ]))));
1017
+ }
1018
+ if (Object.keys(css).length && options.shadcn !== false) {
1019
+ policies.shadcn = [
1020
+ presetOverride(webFiles, {
1021
+ "shadcn/no-restyle": ["error", { allow: ["layout", "opacity"] }],
1022
+ "shadcn/no-arbitrary-values": ["error", { allow: ["layout"] }],
1023
+ "shadcn/require-static-classes": "error",
1024
+ "modules/dynamic-classes": "error"
1025
+ }, ui),
1026
+ presetOverride(webFiles, {
1027
+ "shadcn/no-unknown-classes": ["error", { allow: [] }]
1028
+ })
1029
+ ];
1030
+ }
1031
+ return policies;
1032
+ }
1033
+
1034
+ // src/configs/boundaries.ts
1035
+ import { resolve as resolve2 } from "node:path";
1036
+ function presetBoundariesConfig(context) {
1037
+ const { options, root, architecture, packages, web, backend, appFiles, publicEntrypoints, baseImports } = context;
1038
+ const policies = {};
1039
+ policies.boundaries = [
1040
+ presetOverride([...appFiles, ...presetScopes(packages, "**/" + presetExtensions)], {
1041
+ "modules/import-boundaries": [
1042
+ "error",
1043
+ {
1044
+ web: web.map((path) => resolve2(root, path)),
1045
+ backend: backend.map((path) => resolve2(root, path)),
1046
+ packages: packages.map((path) => resolve2(root, path)),
1047
+ fileRoles: options.modules?.customFileRoles ?? [],
1048
+ portableLib: true,
1049
+ aliases: Object.fromEntries(Object.entries(options.imports?.aliases ?? {}).map(([alias, path]) => [
1050
+ alias.replace(/\/$/, ""),
1051
+ resolve2(root, path)
1052
+ ])),
1053
+ appPackages: Object.entries(architecture).flatMap(([directory, { role }]) => {
1054
+ const name = role === "web" || role === "server" || role === "runner" ? presetPackageName(root, directory) : undefined;
1055
+ return name ? [name] : [];
1056
+ }),
1057
+ backendPackages: Object.entries(architecture).flatMap(([directory, { role }]) => {
1058
+ const name = role === "server" || role === "runner" ? presetPackageName(root, directory) : undefined;
1059
+ return name ? [name] : [];
1060
+ }),
1061
+ publicEntrypoints
1062
+ }
1063
+ ]
1064
+ }),
1065
+ presetOverride([...presetScopes(web, "services/**/*.{ts,tsx}"), ...presetScopes(web, "lib/**/*.{ts,tsx}")], {
1066
+ "modules/tanstack-runtime": [
1067
+ "error",
1068
+ {
1069
+ web: web.map((path) => resolve2(root, path)),
1070
+ backend: backend.map((path) => resolve2(root, path)),
1071
+ aliases: Object.fromEntries(Object.entries(options.imports?.aliases ?? {}).map(([alias, path]) => [
1072
+ alias.replace(/\/$/, ""),
1073
+ resolve2(root, path)
1074
+ ])),
1075
+ backendPackages: Object.entries(architecture).flatMap(([directory, { role }]) => {
1076
+ const name = role === "server" || role === "runner" ? presetPackageName(root, directory) : undefined;
1077
+ return name ? [name] : [];
1078
+ }),
1079
+ rpcClients: ((options.orpc?.clientOwnerFile) ? [options.orpc?.clientOwnerFile] : context.rpcClients).map((path) => resolve2(root, path)),
1080
+ serverImports: options.tanstackStart?.additionalServerOnlyImports ?? [],
1081
+ clientImports: options.tanstackStart?.additionalClientOnlyImports ?? [],
1082
+ allowComputedImportsIn: options.tanstackStart?.computedImportAllowedFiles?.map((path) => resolve2(root, path)) ?? []
1083
+ }
1084
+ ]
1085
+ }),
1086
+ presetOverride(presetScopes(backend, "rpc/**/*.ts"), {
1087
+ "modules/rpc-database": "error",
1088
+ "no-restricted-imports": ["error", { paths: ["kysely"], patterns: baseImports }]
1089
+ })
1090
+ ];
1091
+ return policies;
1092
+ }
1093
+
1094
+ // src/configs/plugins.ts
1095
+ import { createRequire } from "node:module";
1096
+ import { fileURLToPath } from "node:url";
1097
+ var require2 = createRequire(import.meta.url);
1098
+ function presetPluginsConfig(context) {
1099
+ const { options, web, css } = context;
1100
+ const jsPlugins = [
1101
+ { name: "eslint-js", specifier: require2.resolve("oxlint-plugin-eslint") },
1102
+ {
1103
+ name: "import-extra",
1104
+ specifier: require2.resolve("eslint-plugin-import")
1105
+ },
1106
+ require2.resolve("@regru/eslint-plugin-prefer-early-return"),
1107
+ require2.resolve("eslint-plugin-unused-imports"),
1108
+ require2.resolve("eslint-plugin-perfectionist"),
1109
+ require2.resolve("eslint-plugin-tsdoc"),
1110
+ require2.resolve("@eslint-community/eslint-plugin-eslint-comments"),
1111
+ {
1112
+ name: "arch",
1113
+ specifier: fileURLToPath(new URL("../index.js", import.meta.url))
1114
+ },
1115
+ {
1116
+ name: "dillon-anti-slop",
1117
+ specifier: fileURLToPath(new URL("../rules/dillon-anti-slop/index.js", import.meta.url))
1118
+ },
1119
+ {
1120
+ name: "modules",
1121
+ specifier: fileURLToPath(new URL("../rules/module-policy/index.js", import.meta.url))
1122
+ }
1123
+ ];
1124
+ const settings = {
1125
+ react: { version: "19" },
1126
+ "import/resolver": { node: { extensions: [".js", ".jsx", ".ts", ".tsx", ".mts", ".cts"] } }
1127
+ };
1128
+ if (web.length) {
1129
+ jsPlugins.push({
1130
+ name: "react-extra",
1131
+ specifier: require2.resolve("eslint-plugin-react")
1132
+ }, require2.resolve("eslint-plugin-react-you-might-not-need-an-effect"), require2.resolve("eslint-plugin-svg-jsx"));
1133
+ }
1134
+ if (Object.keys(css).length && options.tailwind !== false) {
1135
+ jsPlugins.push(require2.resolve("oxlint-tailwindcss"));
1136
+ settings.tailwindcss = { rootFontSize: options.tailwind?.rootFontSize ?? 16 };
1137
+ if (options.shadcn !== false) {
1138
+ jsPlugins.push(require2.resolve("@shadcn/lint"));
1139
+ settings.shadcn = {
1140
+ ui: options.shadcn?.uiImportPath ?? "@/components/ui",
1141
+ componentImports: options.shadcn?.componentImportSources ?? ["^@/components/"]
1142
+ };
1143
+ }
1144
+ }
1145
+ return { jsPlugins, settings };
1146
+ }
1147
+
1148
+ // src/services/preset-context.ts
1149
+ import { resolve as resolve3, posix } from "node:path";
1150
+ function presetResolveContext(options) {
1151
+ const generated = presetGeneratedFiles;
1152
+ const root = resolve3(options.root ?? process.cwd());
1153
+ const configured = options.architecture ?? {
1154
+ "apps/web": "web",
1155
+ "apps/server": "server",
1156
+ scripts: "scripts",
1157
+ packages: "packages"
1158
+ };
1159
+ const entries = Object.entries(configured).map(([path, configuredRoot]) => {
1160
+ const entry = typeof configuredRoot === "string" ? { role: configuredRoot } : configuredRoot;
1161
+ if (!entry || typeof entry !== "object")
1162
+ throw new Error("Architecture entries must be roles or role/layout objects.");
1163
+ if (Object.keys(entry).some((key) => !["role", "layout"].includes(key)))
1164
+ throw new Error("Unknown architecture entry option.");
1165
+ const role = entry.role;
1166
+ const portable = path.replaceAll("\\", "/");
1167
+ if (!portable.trim() || posix.isAbsolute(portable) || /^[a-z]:/i.test(portable))
1168
+ throw new Error("Architecture roots must be nonempty project-relative paths.");
1169
+ const normalized = posix.normalize(portable).replace(/\/$/, "");
1170
+ if (normalized === ".." || normalized.startsWith("../"))
1171
+ throw new Error("Architecture roots must stay inside the project.");
1172
+ if (!["web", "server", "runner", "scripts", "packages"].includes(role))
1173
+ throw new Error(`Unknown architecture role: ${role}`);
1174
+ if (entry.layout !== undefined && (!entry.layout || typeof entry.layout !== "object" || Array.isArray(entry.layout)))
1175
+ throw new Error("Folder layout must be a path-to-mode object.");
1176
+ const layout = {};
1177
+ if (role === "web" || role === "server" || role === "runner") {
1178
+ layout.services = "domain";
1179
+ }
1180
+ if (role === "web") {
1181
+ layout.hooks = "flat";
1182
+ }
1183
+ for (const [folder, mode] of Object.entries(entry.layout ?? {})) {
1184
+ if (folder.split("/").some((segment) => !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(segment)))
1185
+ throw new Error("Layout folders must be literal source-relative paths without escapes, globs or empty segments.");
1186
+ if (mode !== "flat" && mode !== "domain")
1187
+ throw new Error("Folder layout mode must be flat or domain.");
1188
+ layout[folder] = mode;
1189
+ }
1190
+ const folders = Object.keys(layout);
1191
+ for (const [index, folder] of folders.entries()) {
1192
+ const overlaps = folders.slice(0, index).some((other) => folder.startsWith(other + "/") || other.startsWith(folder + "/"));
1193
+ if (overlaps) {
1194
+ throw new Error("Layout folders must not overlap.");
1195
+ }
1196
+ }
1197
+ return [normalized, { role, layout }];
1198
+ });
1199
+ for (const [index, [path]] of entries.entries()) {
1200
+ const overlaps = entries.slice(0, index).some(([other]) => other === path || other === "." || path === "." || other.startsWith(path + "/") || path.startsWith(other + "/"));
1201
+ if (overlaps) {
1202
+ throw new Error("Architecture roots must not overlap.");
1203
+ }
1204
+ }
1205
+ const architecture = Object.fromEntries(entries);
1206
+ if (options.modules?.customFileRoles?.some((role) => !/^[a-z][a-z0-9-]*$/.test(role)))
1207
+ throw new Error("File roles must be lowercase kebab-case names.");
1208
+ if (options.modules?.customFileRoles?.some((role) => [
1209
+ "service",
1210
+ "client",
1211
+ "server",
1212
+ "rsc",
1213
+ "utils",
1214
+ "action",
1215
+ "query",
1216
+ "types",
1217
+ "constants",
1218
+ "handler",
1219
+ "test",
1220
+ "spec"
1221
+ ].includes(role)))
1222
+ throw new Error("File roles must not replace built-in roles.");
1223
+ const web = entries.filter(([, { role }]) => role === "web").map(([path]) => posix.join(path, "src"));
1224
+ const backendRoots = entries.filter(([, { role }]) => role === "server" || role === "runner").map(([path]) => path);
1225
+ const backend = backendRoots.map((path) => posix.join(path, "src"));
1226
+ const packages = entries.filter(([, { role }]) => role === "packages").map(([path]) => path);
1227
+ const scripts = entries.filter(([, { role }]) => role === "scripts").map(([path]) => path);
1228
+ const folders = entries.flatMap(([path, entry]) => Object.entries(entry.layout).map(([folder, mode]) => ({
1229
+ path: posix.join(path, ["web", "server", "runner"].includes(entry.role) ? "src" : "", folder),
1230
+ folder,
1231
+ mode
1232
+ })));
1233
+ const rpcClients = entries.filter(([, entry]) => entry.role === "web").map(([path, entry]) => posix.join(path, "src/services", entry.layout.services === "domain" ? "rpc/rpc.client.ts" : "rpc.client.ts"));
1234
+ const app = [...web, ...backend];
1235
+ const appFiles = presetScopes(app);
1236
+ const webFiles = presetScopes(web);
1237
+ const backendFiles = presetScopes(backend);
1238
+ const services = presetScopes(app, `services/**/${presetExtensions}`);
1239
+ const utilities = ["**/*.utils.ts", "**/*.constants.ts", "**/*.types.ts"];
1240
+ const types = presetScopes(app, "types/**");
1241
+ const ui = presetScopes(web, "components/ui/**");
1242
+ const testFiles = ["**/{test,tests,e2e}/**/*.{ts,tsx,mts,cts}", "**/*.{test,spec}.{ts,tsx,mts,cts}"];
1243
+ const safetyFiles = [...appFiles, ...testFiles];
1244
+ const components = presetScopes(web, "components/**/*.tsx");
1245
+ const hooks = presetScopes(web, "hooks/**/*.ts");
1246
+ const level = options.severity ?? "error";
1247
+ const publicEntrypoints = options.imports?.backendEntryPoints ?? backendRoots.flatMap((directory) => {
1248
+ const name = presetPackageName(root, directory);
1249
+ return name ? [`${name}/client`] : [];
1250
+ });
1251
+ const baseImports = [
1252
+ {
1253
+ group: ["zod/v4", "zod/v4/**"],
1254
+ message: "Import from zod, not its transition entry point."
1255
+ }
1256
+ ];
1257
+ const tailwind = options.tailwind;
1258
+ const entryPoints = Object.fromEntries(Object.entries(tailwind === false ? {} : tailwind?.cssEntryPointsByRoot ?? {}).map(([path, entry]) => [
1259
+ posix.normalize(path.replaceAll("\\", "/")).replace(/\/$/, ""),
1260
+ entry
1261
+ ]));
1262
+ const css = tailwind === false ? {} : Object.fromEntries(web.map((path) => [
1263
+ path,
1264
+ resolve3(root, entryPoints[posix.dirname(path)] ?? tailwind?.cssEntryPoint ?? `${path}/application/styles.css`)
1265
+ ]));
1266
+ return {
1267
+ options,
1268
+ root,
1269
+ architecture,
1270
+ folders,
1271
+ rpcClients,
1272
+ packages,
1273
+ scripts,
1274
+ web,
1275
+ backend,
1276
+ app,
1277
+ appFiles,
1278
+ webFiles,
1279
+ backendFiles,
1280
+ services,
1281
+ utilities,
1282
+ types,
1283
+ ui,
1284
+ generated,
1285
+ testFiles,
1286
+ safetyFiles,
1287
+ components,
1288
+ hooks,
1289
+ level,
1290
+ publicEntrypoints,
1291
+ baseImports,
1292
+ css
1293
+ };
1294
+ }
1295
+
1296
+ // src/services/preset-compose.ts
1297
+ function presetCompose(context, policies, plugins) {
1298
+ const { options, level, safetyFiles, ui, generated } = context;
1299
+ const overrideList = [];
1300
+ for (const name of presetPolicyOrder) {
1301
+ const setting = options.policies?.[name];
1302
+ if (setting === false || !policies[name]) {
1303
+ continue;
1304
+ }
1305
+ const defaults = structuredClone(policies[name]);
1306
+ const entries = typeof setting === "function" ? setting(defaults) : defaults;
1307
+ for (const entry of entries) {
1308
+ overrideList.push({
1309
+ ...entry,
1310
+ rules: entry.rules ? presetSeverity(entry.rules, level) : undefined
1311
+ });
1312
+ }
1313
+ }
1314
+ overrideList.push(presetOverride(safetyFiles, {
1315
+ "max-lines": [level, { max: options.limits?.maxFileLines ?? 400, skipBlankLines: true, skipComments: true }],
1316
+ complexity: [level, { max: options.limits?.maxFunctionComplexity ?? 32 }]
1317
+ }, ui));
1318
+ for (const [rule, files] of Object.entries(options.ruleExclusions ?? {})) {
1319
+ overrideList.push({ files, rules: { [rule]: "off" } });
1320
+ }
1321
+ return {
1322
+ options: { typeAware: true },
1323
+ categories: { correctness: level },
1324
+ plugins: ["typescript", "react", "react-perf", "jsx-a11y", "import", "unicorn", "promise", "vitest", "node", "oxc"],
1325
+ ignorePatterns: [
1326
+ "**/node_modules/**",
1327
+ "**/dist/**",
1328
+ "**/build/**",
1329
+ "**/coverage/**",
1330
+ "**/.output/**",
1331
+ ...generated,
1332
+ ...options.ignorePatterns ?? []
1333
+ ],
1334
+ ...plugins,
1335
+ rules: presetSeverity(presetCommonRules, level),
1336
+ overrides: overrideList.filter((entry) => entry.files.length > 0)
1337
+ };
1338
+ }
1339
+
1340
+ // src/presets/tanstack-start-react-modules-preset.ts
1341
+ function tanstackStartReactModulesPreset(options = {}) {
1342
+ const context = presetResolveContext(options);
1343
+ return presetCompose(context, {
1344
+ ...presetSourceConfig(context),
1345
+ ...presetTypesConfig(context),
1346
+ ...presetModulesConfig(context),
1347
+ ...presetTestsConfig(context),
1348
+ ...presetReactConfig(context),
1349
+ ...presetBoundariesConfig(context)
1350
+ }, presetPluginsConfig(context));
1351
+ }
1352
+ export {
1353
+ tanstackStartReactModulesPreset as default
1354
+ };