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