oxlint-plugin-arch 0.6.2 → 0.7.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,1570 @@
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 presetNextHandlerMethods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
422
+ var presetNextSegmentConfig = [
423
+ "generateStaticParams",
424
+ "dynamic",
425
+ "dynamicParams",
426
+ "revalidate",
427
+ "fetchCache",
428
+ "runtime",
429
+ "preferredRegion",
430
+ "maxDuration"
431
+ ];
432
+ var presetNextSegmentExports = [
433
+ "generateMetadata",
434
+ "metadata",
435
+ "generateViewport",
436
+ "viewport",
437
+ ...presetNextSegmentConfig
438
+ ];
439
+ var presetExportPrefix = {
440
+ stem: "before-first-dot",
441
+ trailingRoles: ["constants", "utils", "handler", "service", "types"],
442
+ roleSeparators: [".", "-"],
443
+ normalize: "remove-separators",
444
+ allowPattern: "^(make[A-Z]|\\*$)"
445
+ };
446
+ var presetGeneratedFiles = [
447
+ "**/*.generated.*",
448
+ "**/*.gen.*",
449
+ "**/*.d.ts",
450
+ "**/routes.tree.ts",
451
+ "**/routeTree.gen.ts",
452
+ "**/paraglide/**"
453
+ ];
454
+
455
+ // src/utils/helpers/preset.ts
456
+ function presetScopes(roots, suffix = `**/${presetExtensions}`) {
457
+ return roots.map((root) => root === "." ? suffix : `${root}/${suffix}`);
458
+ }
459
+ function presetOverride(files, rules, excludeFiles = []) {
460
+ return {
461
+ files,
462
+ excludeFiles: [...presetGeneratedFiles, ...excludeFiles],
463
+ rules
464
+ };
465
+ }
466
+ function presetRuleSeverities(rules, severities) {
467
+ const result = structuredClone(rules);
468
+ for (const [name, level] of Object.entries(severities)) {
469
+ const value = result[name];
470
+ const current = Array.isArray(value) ? value[0] : value;
471
+ if (value === undefined || current === "off" || current === "allow" || current === 0) {
472
+ continue;
473
+ }
474
+ result[name] = Array.isArray(value) && level !== "off" ? [level, ...value.slice(1)] : level;
475
+ }
476
+ return result;
477
+ }
478
+ function presetPackageName(root, directory) {
479
+ try {
480
+ const manifest = JSON.parse(readFileSync(resolve(root, directory, "package.json"), "utf8"));
481
+ return typeof manifest.name === "string" ? manifest.name : undefined;
482
+ } catch {
483
+ return;
484
+ }
485
+ }
486
+ function presetSeverity(rules, level) {
487
+ const result = structuredClone(rules);
488
+ for (const [name, value] of Object.entries(result)) {
489
+ const current = Array.isArray(value) ? value[0] : value;
490
+ if (current === "off" || current === "allow" || current === 0) {
491
+ continue;
492
+ }
493
+ result[name] = Array.isArray(value) ? [level, ...value.slice(1)] : level;
494
+ }
495
+ return result;
496
+ }
497
+
498
+ // src/configs/source.ts
499
+ function presetSourceConfig(context) {
500
+ const { options, web, scripts, app, appFiles, backendFiles, ui, safetyFiles, publicEntrypoints, baseImports } = context;
501
+ const policies = {};
502
+ const { semi = "always", quotes = "single" } = options.formatting ?? {};
503
+ policies.formatting = [
504
+ presetOverride([`**/${presetExtensions}`], {
505
+ ...presetFormattingRules,
506
+ "eslint-js/semi": ["error", semi],
507
+ "eslint-js/quotes": ["error", quotes, { avoidEscape: true }]
508
+ })
509
+ ];
510
+ policies.statementLayout = [
511
+ presetOverride([`**/${presetExtensions}`], {
512
+ "arch/padding-between-statements": ["error", { multilineVariables: true }],
513
+ "arch/object-multiline": ["error", { minProperties: 3, scope: "call-args" }],
514
+ "arch/call-array-multiline": ["error", { callees: ["Promise.all"], minElements: 2 }],
515
+ "arch/key-value-same-line": "error"
516
+ })
517
+ ];
518
+ policies.imports = [
519
+ presetOverride([`**/${presetExtensions}`], {
520
+ ...presetImportRules,
521
+ "arch/prefer-namespace-type-import": ["error", { max: 3 }],
522
+ "no-restricted-imports": ["error", { patterns: baseImports }],
523
+ "import-extra/no-relative-packages": "error",
524
+ "perfectionist/sort-imports": [
525
+ "error",
526
+ {
527
+ type: "natural",
528
+ ignoreCase: true,
529
+ newlinesBetween: 0,
530
+ environment: "bun",
531
+ internalPattern: options.imports?.internalSortPatterns ?? [
532
+ "^@/.*",
533
+ "^#.*",
534
+ "^~icons/.*",
535
+ ...publicEntrypoints.map((path) => `^${path.split("/").slice(0, 2).join("/")}/`)
536
+ ],
537
+ groups: [["builtin", "external"], "internal", ["parent", "sibling", "index"], "type"]
538
+ }
539
+ ]
540
+ })
541
+ ];
542
+ policies.comments = [
543
+ presetOverride(appFiles, {
544
+ "arch/no-comments": ["error", { allowWhy: false, allowPatterns: ["^\\s*SAFETY: "] }]
545
+ }, ui),
546
+ presetOverride(presetScopes(app, "**/*.{types,constants}.ts"), {
547
+ "arch/no-comments": ["error", { allowWhy: false }]
548
+ }),
549
+ presetOverride([`**/${presetExtensions}`], {
550
+ "modules/reasoned-directives": "error"
551
+ })
552
+ ];
553
+ policies.trivialFunctions = [
554
+ presetOverride(appFiles, {
555
+ "arch/no-trivial-functions": ["error", { mode: "precise", allowAsync: false, allowCallees: [] }]
556
+ }, ui)
557
+ ];
558
+ policies.trivialFunctions.push(presetOverride([...presetScopes(web, "services/**/*.{client,server,rsc}.ts"), ...presetScopes(web, "services/**/*.rsc.tsx")], {
559
+ "arch/no-trivial-functions": [
560
+ "error",
561
+ { mode: "precise", allowAsync: false, allowCallees: [], checkServiceMethods: true }
562
+ ]
563
+ }, ui));
564
+ policies.mutableState = [
565
+ presetOverride(safetyFiles, { "arch/no-module-mutable-state": ["error", { kinds: ["let", "var"] }] }, ui)
566
+ ];
567
+ policies.backend = [
568
+ presetOverride(backendFiles, { "modules/backend-switch": "error" }, options.cliFiles),
569
+ presetOverride([`**/${presetExtensions}`], { "no-console": "error" }, [
570
+ ...presetScopes(scripts, "**"),
571
+ ...options.cliFiles ?? []
572
+ ]),
573
+ presetOverride(safetyFiles, { "modules/double-negation": "error" }, ui)
574
+ ];
575
+ return policies;
576
+ }
577
+
578
+ // src/configs/types.ts
579
+ function presetTypesConfig(context) {
580
+ const {
581
+ options,
582
+ web,
583
+ backend,
584
+ app,
585
+ appFiles,
586
+ webFiles,
587
+ services,
588
+ utilities,
589
+ types,
590
+ ui,
591
+ testFiles,
592
+ safetyFiles,
593
+ components,
594
+ hooks,
595
+ publicEntrypoints,
596
+ tanstackWeb,
597
+ nextWeb
598
+ } = context;
599
+ const schemaOwners = backend.length ? backend : nextWeb;
600
+ const runtimeFreeTypes = backend.length ? web : tanstackWeb;
601
+ const colocatedTypes = options.modules?.colocatedTypeFiles ?? [];
602
+ const policies = {};
603
+ policies.typePlacement = [
604
+ presetOverride(appFiles, { "arch/no-type-declarations": "error" }, [
605
+ ...types,
606
+ ...colocatedTypes,
607
+ ...components,
608
+ ...hooks,
609
+ ...services,
610
+ ...ui,
611
+ ...testFiles
612
+ ]),
613
+ presetOverride(services, { "modules/service-types": "error" }, [...utilities, ...testFiles]),
614
+ presetOverride([...components, ...hooks], {
615
+ "modules/local-type-alias": "error",
616
+ "modules/pascal-interface": "error",
617
+ "arch/declaration-name": [
618
+ "error",
619
+ {
620
+ kinds: ["interface"],
621
+ stem: "before-first-dot",
622
+ normalize: "remove-separators"
623
+ }
624
+ ]
625
+ }, [...ui, ...testFiles]),
626
+ ...options.react?.componentProps === false ? [] : [presetOverride(components, { "arch/component-props": ["error", { filePrefix: true }] }, [...ui, ...testFiles])],
627
+ presetOverride(presetScopes(app, "**/*.types.ts"), {
628
+ "arch/no-restricted-files": ["error", { message: "Domain types belong in src/types." }]
629
+ }, [...types, ...colocatedTypes, ...ui]),
630
+ presetOverride(presetScopes(app, "**/*.{utils,constants}.ts"), {
631
+ "arch/no-type-declarations": "error"
632
+ }, [...ui, ...testFiles]),
633
+ presetOverride([...presetScopes(schemaOwners, "types/**/*.types.ts"), ...colocatedTypes, ...presetScopes(app, "**/*.constants.ts")], {
634
+ "arch/no-top-level-functions": ["error", { banReExports: false }]
635
+ }),
636
+ ...runtimeFreeTypes.length ? [
637
+ presetOverride(presetScopes(runtimeFreeTypes, "types/**/*.types.ts"), {
638
+ "arch/no-runtime-in-types": ["error", { runtimeImports: "ban", allowImportSources: [], banReExports: true }]
639
+ })
640
+ ] : []
641
+ ];
642
+ policies.typeSafety = [
643
+ presetOverride(safetyFiles, {
644
+ "typescript/no-explicit-any": "error",
645
+ "typescript/ban-ts-comment": [
646
+ "error",
647
+ {
648
+ "ts-ignore": true,
649
+ "ts-nocheck": true,
650
+ "ts-expect-error": true,
651
+ "ts-check": false
652
+ }
653
+ ],
654
+ "modules/no-unknown": "error",
655
+ "modules/shape-suffix": "error",
656
+ "arch/no-imported-type-alias": "error",
657
+ "arch/no-literal-in": "error",
658
+ "arch/no-promise-all-mutation": "error",
659
+ "arch/no-restricted-constructor": ["error", { constructors: ["Error"], checkCalls: true }],
660
+ "dillon-anti-slop/no-chained-type-assertions": "error",
661
+ "dillon-anti-slop/no-conditional-empty-object-spread": "error",
662
+ "dillon-anti-slop/no-known-value-widening": "error",
663
+ "dillon-anti-slop/no-reflect-apply": "error",
664
+ "dillon-anti-slop/no-reflect-get": "error",
665
+ "dillon-anti-slop/no-runtime-typeof": "error",
666
+ "dillon-anti-slop/no-unknown-parameters": "error",
667
+ "dillon-anti-slop/no-unknown-returns": "error",
668
+ "dillon-anti-slop/no-unknown-type-aliases": "error",
669
+ "dillon-anti-slop/no-unsafe-dictionary-type": "error",
670
+ "dillon-anti-slop/no-widen-then-assert": "error",
671
+ "dillon-anti-slop/require-safety-comment-for-type-assertion": "error"
672
+ })
673
+ ];
674
+ policies.schemas = [
675
+ presetOverride(appFiles, {
676
+ "arch/no-local-schema-construction": ["error", { packages: ["zod"], namespaces: ["z"] }]
677
+ }, [...presetScopes(schemaOwners, "types/**"), ...colocatedTypes, ...ui]),
678
+ presetOverride(webFiles, {
679
+ "arch/no-rederive-schema": [
680
+ "error",
681
+ {
682
+ from: publicEntrypoints,
683
+ namespaces: ["z"],
684
+ operators: ["infer", "input"]
685
+ }
686
+ ]
687
+ }),
688
+ presetOverride(presetScopes(backend, "services/**/*.ts"), {
689
+ "arch/no-unescaped-like": [
690
+ "error",
691
+ {
692
+ methods: ["like", "ilike"],
693
+ operatorMethods: ["where"],
694
+ sanitizers: options.sql?.likeSanitizers ?? ["escapeLikeWildcards"],
695
+ allowSanitizedBindings: true
696
+ }
697
+ ]
698
+ }),
699
+ ...options.orpc?.publicProcedureFiles?.length ? [
700
+ presetOverride(options.orpc?.publicProcedureFiles, {
701
+ "arch/require-orpc-output": [
702
+ "error",
703
+ {
704
+ handlerMethod: "handler",
705
+ outputMethod: "output",
706
+ composers: options.orpc?.outputSchemaComposers ?? []
707
+ }
708
+ ]
709
+ })
710
+ ] : []
711
+ ];
712
+ return policies;
713
+ }
714
+
715
+ // src/configs/modules.ts
716
+ import { posix } from "node:path";
717
+ function presetModulesConfig(context) {
718
+ const { web, nextWeb, backend, app, services, utilities, ui, testFiles, components, hooks, options } = context;
719
+ const conceptFiles = (options.modules?.customFileRoles ?? []).map((role) => `**/*.${role}.ts`);
720
+ const migrations = options.modules?.migrationFiles ?? [];
721
+ const frontendEntries = [
722
+ ...presetScopes(web, "services/**/*.{client,server,rsc}.ts"),
723
+ ...presetScopes(web, "services/**/*.rsc.tsx")
724
+ ];
725
+ const policies = {};
726
+ policies.fileLayout = [
727
+ ...context.folders.flatMap(({ path, folder, mode }) => [
728
+ presetOverride([
729
+ mode === "flat" ? `${path}/*/**/${presetExtensions}` : `${path}/${presetExtensions}`,
730
+ ...mode === "domain" ? [`${path}/*/*/**/${presetExtensions}`] : []
731
+ ], {
732
+ "arch/no-restricted-files": [
733
+ "error",
734
+ {
735
+ 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."
736
+ }
737
+ ]
738
+ }, migrations),
739
+ ...mode === "domain" ? [
740
+ presetOverride([`${path}/*/${presetExtensions}`], {
741
+ "arch/folder-prefix": [
742
+ "error",
743
+ {
744
+ singularize: folder === "components" ? "trailing-s" : "none",
745
+ separators: [".", "-"],
746
+ ...folder === "hooks" ? { stripPrefixes: ["use-"] } : {}
747
+ }
748
+ ]
749
+ })
750
+ ] : []
751
+ ]),
752
+ presetOverride(presetScopes(web.filter((path) => !context.folders.some((scope) => scope.path === posix.join(path, "components"))), `components/*/*/**/${presetExtensions}`), {
753
+ "arch/no-restricted-files": ["error", { message: "Components allow one group folder." }]
754
+ }, ui),
755
+ presetOverride([...presetScopes(web, "components/**/*.ts"), ...presetScopes(web, "hooks/**/*.tsx")], {
756
+ "arch/no-restricted-files": ["error", { message: "Components use TSX; hooks use .ts files." }]
757
+ }, ui),
758
+ presetOverride(presetScopes(web, "**/use-*.{ts,tsx}"), {
759
+ "arch/no-restricted-files": ["error", { message: "Hooks belong in src/hooks." }]
760
+ }, [...presetScopes(web, "hooks/**"), ...ui]),
761
+ presetOverride(presetScopes(web, "services/**/*.{ts,tsx}"), {
762
+ "arch/no-restricted-files": [
763
+ "error",
764
+ {
765
+ message: "Frontend services use .client.ts, .server.ts or .rsc.{ts,tsx}; shared domain helpers use .utils.ts and values use .constants.ts."
766
+ }
767
+ ]
768
+ }, [
769
+ "**/*.{client,server,rsc,utils,constants}.ts",
770
+ "**/*.rsc.tsx",
771
+ ...presetScopes(nextWeb, "services/**/*.action.ts"),
772
+ ...conceptFiles,
773
+ ...options.modules?.colocatedTypeFiles ?? []
774
+ ])
775
+ ];
776
+ policies.serviceStructure = [
777
+ presetOverride(services, {
778
+ "modules/service-functions": "error",
779
+ "arch/no-inline-types": [
780
+ "error",
781
+ {
782
+ parameters: true,
783
+ returns: true,
784
+ functionTypes: true,
785
+ minMembers: 1
786
+ }
787
+ ]
788
+ }, [...utilities, ...testFiles, ...conceptFiles, ...migrations]),
789
+ presetOverride(frontendEntries, {
790
+ "modules/service-functions": ["error", { frontend: true }]
791
+ }, testFiles),
792
+ ...conceptFiles.map((pattern) => presetOverride(presetScopes(app, `services/${pattern}`), {
793
+ "modules/service-functions": "off",
794
+ "arch/export-file-prefix": [
795
+ "error",
796
+ {
797
+ stem: "full-basename",
798
+ allFunctions: true
799
+ }
800
+ ]
801
+ }, testFiles)),
802
+ presetOverride([
803
+ ...presetScopes(app, "services/**/*-{action,query}.*.ts"),
804
+ ...presetScopes(web, "services/**/*-{action,query}.*.rsc.tsx")
805
+ ], {
806
+ "modules/service-functions": [
807
+ "error",
808
+ {
809
+ allowLocalHelpers: true,
810
+ singleExport: true,
811
+ 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."
812
+ }
813
+ ]
814
+ }, testFiles),
815
+ presetOverride(presetScopes(backend, "services/**/*.ts"), { "modules/domain-constants": "error" }, [
816
+ ...utilities,
817
+ ...testFiles
818
+ ]),
819
+ presetOverride(presetScopes(web, "services/**/*.{ts,tsx}"), {
820
+ "modules/domain-constants": ["error", { includeData: true }]
821
+ }, ["**/*.constants.ts", ...testFiles]),
822
+ presetOverride(frontendEntries, {
823
+ "modules/domain-constants": ["error", { includeData: true, allowServiceMethods: true }]
824
+ }, testFiles),
825
+ presetOverride([...presetScopes(backend, "services/**/*-{action,query}.*.ts"), ...frontendEntries], { "arch/require-object-params": ["error", { maxParams: 2 }] }, testFiles),
826
+ presetOverride(frontendEntries, {
827
+ "arch/require-object-params": ["error", { maxParams: 2, serviceMethods: true }]
828
+ }, testFiles),
829
+ presetOverride(presetScopes(backend, "services/**/*.service.ts"), {
830
+ "arch/no-extra-exports": [
831
+ "error",
832
+ {
833
+ names: ["make{Domain}Service", "{Domain}Service"],
834
+ domainStem: "before-first-dot",
835
+ allowTypeExports: false
836
+ }
837
+ ]
838
+ }, testFiles),
839
+ presetOverride(presetScopes(app, "services/**/{actions,queries}/**/*.ts"), {
840
+ "arch/no-restricted-files": [
841
+ "error",
842
+ { message: "Use domain-action.name.ts or domain-query.name.ts." }
843
+ ]
844
+ })
845
+ ];
846
+ policies.naming = [
847
+ presetOverride([`**/${presetExtensions}`], presetNamingRules),
848
+ presetOverride(presetScopes(app, "types/**/*.ts"), {
849
+ "arch/filename-match": [
850
+ "error",
851
+ {
852
+ pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*[.]types[.]ts$",
853
+ message: "Use domain.types.ts filenames."
854
+ }
855
+ ]
856
+ }, ["**/*.constants.ts"]),
857
+ presetOverride(presetScopes(app, "**/*.{constants,utils,handler,service,types,client,server,rsc}.ts"), { "arch/export-file-prefix": ["error", presetExportPrefix] }, [...ui, ...presetScopes(backend, "entry.*.ts")]),
858
+ presetOverride(frontendEntries, {
859
+ "arch/export-file-prefix": [
860
+ "error",
861
+ { ...presetExportPrefix, allFunctions: true, serviceMethods: true, allowPattern: "^\\*$" }
862
+ ]
863
+ }, testFiles),
864
+ presetOverride(presetScopes(web, "services/**/*.constants.ts"), {
865
+ "arch/export-file-prefix": [
866
+ "error",
867
+ { ...presetExportPrefix, allDeclarations: true, allowPattern: "^\\*$" }
868
+ ]
869
+ }, testFiles),
870
+ presetOverride(presetScopes(app, "**/*.utils.ts"), {
871
+ "arch/export-file-prefix": [
872
+ "error",
873
+ {
874
+ ...presetExportPrefix,
875
+ allFunctions: true,
876
+ allowPattern: "^\\*$"
877
+ }
878
+ ]
879
+ }, ui),
880
+ presetOverride(hooks, {
881
+ "arch/filename-match": [
882
+ "error",
883
+ {
884
+ pattern: "^use-[a-z0-9]+(?:-[a-z0-9]+)*[.]ts$",
885
+ message: "Hooks use use-domain.ts filenames."
886
+ }
887
+ ],
888
+ "arch/export-file-prefix": "error",
889
+ "arch/export-name-pattern": ["error", { pattern: "^use[A-Z]", ignoreTypeExports: true }]
890
+ }),
891
+ presetOverride(components, {
892
+ "arch/only-export-components": ["error", { matchFileName: true, allowTypeExports: true, trailingRoles: ["illustration"] }]
893
+ }, [...ui, ...testFiles]),
894
+ presetOverride(presetScopes(web.filter((path) => !context.folders.some((scope) => scope.path === posix.join(path, "components"))), "components/**/*.tsx"), {
895
+ "arch/folder-prefix": ["error", { singularize: "trailing-s", separators: ["-"] }]
896
+ }, [...ui, ...testFiles]),
897
+ ...["action", "query"].map((role) => presetOverride([
898
+ ...presetScopes(app, `services/**/*-${role}.*.ts`),
899
+ ...presetScopes(web, `services/**/*-${role}.*.rsc.tsx`)
900
+ ], {
901
+ "arch/filename-match": [
902
+ "error",
903
+ {
904
+ pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*-${role}\\.[a-z0-9]+(?:-[a-z0-9]+)*\\.ts$`,
905
+ message: "Use domain-action.name.ts or domain-query.name.ts."
906
+ }
907
+ ],
908
+ "arch/export-file-prefix": [
909
+ "error",
910
+ {
911
+ ...presetExportPrefix,
912
+ trailingRoles: [role],
913
+ allFunctions: true,
914
+ allowPattern: "^\\*$"
915
+ }
916
+ ],
917
+ "arch/filename-export-name": [
918
+ "error",
919
+ {
920
+ file: `{domain}-${role}.{name}.ts`,
921
+ export: `{domain}${role === "action" ? "Action" : "Query"}{Name}`,
922
+ placeholderPattern: "[a-z0-9-]+",
923
+ camelCase: true
924
+ }
925
+ ]
926
+ })),
927
+ ...["action", "query"].flatMap((role) => ["client.ts", "server.ts", "rsc.ts", "rsc.tsx"].map((suffix) => presetOverride(presetScopes(web, `services/**/*-${role}.*.${suffix}`), {
928
+ "arch/filename-match": [
929
+ "error",
930
+ {
931
+ pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*-${role}\\.[a-z0-9]+(?:-[a-z0-9]+)*\\.${suffix.replace(".", "\\.")}$`,
932
+ message: "Frontend operations retain a client, server or rsc runtime suffix."
933
+ }
934
+ ],
935
+ "arch/filename-export-name": [
936
+ "error",
937
+ {
938
+ file: `{domain}-${role}.{name}.${suffix}`,
939
+ export: `{domain}${role === "action" ? "Action" : "Query"}{Name}`,
940
+ placeholderPattern: "[a-z0-9-]+",
941
+ camelCase: true
942
+ }
943
+ ]
944
+ }))),
945
+ presetOverride(presetScopes(web, "services/**/*.rsc.{ts,tsx}"), {
946
+ "arch/export-name-pattern": ["error", { pattern: "^[a-z][a-z0-9]*Rsc[A-Z][a-zA-Z0-9]*$" }]
947
+ }, ["**/*-{action,query}.*.rsc.{ts,tsx}"])
948
+ ];
949
+ return policies;
950
+ }
951
+
952
+ // src/configs/tests.ts
953
+ function presetTestsConfig(context) {
954
+ const { backendRoots, testFiles, baseImports, options } = context;
955
+ const profile = options.tests?.profile ?? "fixtures";
956
+ if (profile !== "standard" && profile !== "fixtures")
957
+ throw new Error("Unknown test profile.");
958
+ const testFunctions = ["test", "it", ...options.tests?.additionalTestFunctions ?? []];
959
+ const assertionFunctions = options.tests?.additionalAssertionFunctions ?? [];
960
+ if (testFunctions.some((name) => !/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(name)))
961
+ throw new Error("Additional test functions must be literal identifiers or dotted names.");
962
+ if (assertionFunctions.some((name) => !/^[A-Za-z_$][\w$]*(?:\.[\w$*]+)*$/.test(name)))
963
+ throw new Error("Assertion functions must be rooted dotted names or patterns.");
964
+ const policies = {};
965
+ policies.tests = [
966
+ presetOverride(testFiles, {
967
+ ...presetTestRules,
968
+ "dillon-anti-slop/no-module-mocking": "error",
969
+ "modules/test-modifiers": [
970
+ "error",
971
+ { additionalTestFunctions: options.tests?.additionalTestFunctions ?? [] }
972
+ ],
973
+ "modules/test-assertions": [
974
+ "error",
975
+ {
976
+ additionalTestFunctions: options.tests?.additionalTestFunctions ?? [],
977
+ additionalAssertionFunctions: assertionFunctions
978
+ }
979
+ ],
980
+ "arch/test-title-pattern": [
981
+ "error",
982
+ { callees: ["describe", ...testFunctions], forbid: "^should\\b", flags: "i" }
983
+ ]
984
+ }),
985
+ presetOverride(presetScopes(backendRoots, "tests/**/*.ts"), {
986
+ "modules/concurrent-db": "error",
987
+ ...profile === "fixtures" ? {
988
+ "no-restricted-imports": [
989
+ "error",
990
+ {
991
+ patterns: [
992
+ ...baseImports,
993
+ {
994
+ group: ["**/helpers/**"],
995
+ message: "Use named fixtures or tests/support."
996
+ }
997
+ ]
998
+ }
999
+ ]
1000
+ } : {}
1001
+ })
1002
+ ];
1003
+ return policies;
1004
+ }
1005
+
1006
+ // src/configs/react.ts
1007
+ function presetReactConfig(context) {
1008
+ const { options, web, tanstackWeb, nextWeb, webFiles, ui, testFiles, components, css } = context;
1009
+ const policies = {};
1010
+ if (web.length) {
1011
+ policies.react = [
1012
+ presetOverride(webFiles, {
1013
+ ...presetReactRules,
1014
+ ...options.react?.compiler === false ? {
1015
+ "react-perf/jsx-no-new-object-as-prop": "error",
1016
+ "react-perf/jsx-no-new-array-as-prop": "error",
1017
+ "react-perf/jsx-no-new-function-as-prop": "error",
1018
+ "react-perf/jsx-no-jsx-as-prop": "error"
1019
+ } : {},
1020
+ "arch/jsx-attributes-multiline": ["error", { minAttributes: 3 }]
1021
+ }, ui),
1022
+ presetOverride(components, {
1023
+ "arch/no-file-level-helpers": ["error", { detectComponents: true, hookPattern: "^use[A-Z]" }]
1024
+ }, [...ui, ...testFiles])
1025
+ ];
1026
+ }
1027
+ if (web.length) {
1028
+ policies.effects = [presetOverride(webFiles, { ...presetEffectsRules, "modules/empty-effect": "error" }, ui)];
1029
+ }
1030
+ const nextPage = { bannedHooks: ["useState", "useEffect", "useMutation"], inspectExports: true };
1031
+ const routes = [
1032
+ ...tanstackWeb.length ? [
1033
+ presetOverride(presetScopes(tanstackWeb, "routes/**/*.{ts,tsx}"), {
1034
+ "arch/route-surface": [
1035
+ "error",
1036
+ {
1037
+ exportName: "Route",
1038
+ bannedHooks: ["useState", "useEffect", "useMutation"],
1039
+ banIntrinsicJsx: true
1040
+ }
1041
+ ]
1042
+ }, testFiles),
1043
+ presetOverride(presetScopes(tanstackWeb, "routes/handlers/**/*.ts"), {
1044
+ "arch/no-file-level-helpers": [
1045
+ "error",
1046
+ {
1047
+ detectComponents: false,
1048
+ 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."
1049
+ }
1050
+ ]
1051
+ })
1052
+ ] : [],
1053
+ ...nextWeb.length ? [
1054
+ presetOverride(presetScopes(nextWeb, "app/**/{page,template,default,not-found,forbidden,unauthorized,loading}.tsx"), {
1055
+ "arch/route-surface": [
1056
+ "error",
1057
+ { exportName: "default", allowedExports: presetNextSegmentExports, banIntrinsicJsx: true, ...nextPage }
1058
+ ]
1059
+ }, [...testFiles, ...presetScopes(nextWeb, "app/not-found.tsx")]),
1060
+ presetOverride([...presetScopes(nextWeb, "app/**/{layout,global-not-found}.tsx"), ...presetScopes(nextWeb, "app/not-found.tsx")], {
1061
+ "arch/route-surface": [
1062
+ "error",
1063
+ { exportName: "default", allowedExports: presetNextSegmentExports, banIntrinsicJsx: false, ...nextPage }
1064
+ ]
1065
+ }, testFiles),
1066
+ presetOverride(presetScopes(nextWeb, "app/**/route.ts"), {
1067
+ "arch/route-surface": [
1068
+ "error",
1069
+ {
1070
+ exportName: "GET",
1071
+ allowedExports: [...presetNextHandlerMethods.slice(1), ...presetNextSegmentConfig],
1072
+ bannedHooks: [],
1073
+ banIntrinsicJsx: false
1074
+ }
1075
+ ],
1076
+ "arch/no-file-level-helpers": [
1077
+ "error",
1078
+ {
1079
+ detectComponents: false,
1080
+ allowPattern: `^(${[...presetNextHandlerMethods, ...presetNextSegmentConfig].join("|")})$`,
1081
+ message: "Nest this helper in its owning route handler, or move genuine domain logic to a service. Do not create a component, hook or public helper to bypass this rule."
1082
+ }
1083
+ ]
1084
+ })
1085
+ ] : []
1086
+ ];
1087
+ if (routes.length) {
1088
+ policies.routes = routes;
1089
+ }
1090
+ if (web.length) {
1091
+ policies.forms = [
1092
+ presetOverride(webFiles, {
1093
+ "arch/require-paired-call": [
1094
+ "error",
1095
+ {
1096
+ when: "useForm",
1097
+ require: options.forms?.schemaResolver ?? "standardSchemaResolver"
1098
+ }
1099
+ ]
1100
+ }, ui)
1101
+ ];
1102
+ }
1103
+ if (web.length && options.react?.compiler !== false) {
1104
+ policies.memoization = [presetOverride(webFiles, { "modules/memoization": "error" }, ui)];
1105
+ }
1106
+ if (web.length) {
1107
+ policies.rpcClientOwnership = [
1108
+ presetOverride(webFiles, {
1109
+ "arch/no-restricted-token": [
1110
+ "error",
1111
+ {
1112
+ restrictions: [
1113
+ {
1114
+ token: "RouterClient",
1115
+ allowIn: [...options.orpc?.clientOwnerFile ? [options.orpc?.clientOwnerFile] : context.rpcClients]
1116
+ },
1117
+ {
1118
+ token: "createContext",
1119
+ allowPathPatterns: [...presetScopes(web, "context/"), ...presetScopes(web, "components/ui/")]
1120
+ }
1121
+ ]
1122
+ }
1123
+ ]
1124
+ })
1125
+ ];
1126
+ }
1127
+ if (Object.keys(css).length) {
1128
+ policies.tailwind = Object.entries(css).map(([path, entryPoint]) => presetOverride(presetScopes([path]), Object.fromEntries(Object.entries(presetTailwindRules).map(([rule, severity]) => [
1129
+ rule,
1130
+ rule === "tailwindcss/no-duplicate-classes" || rule === "tailwindcss/no-unnecessary-whitespace" ? severity : [severity, { entryPoint }]
1131
+ ]))));
1132
+ }
1133
+ if (Object.keys(css).length && options.shadcn !== false) {
1134
+ const allow = ["layout", "opacity", "truncate"];
1135
+ const padding = ["p", "px", "py", "ps", "pe", "pt", "pr", "pb", "pl"];
1136
+ const gap = ["gap", "gap-x", "gap-y"];
1137
+ const safeArea = padding.flatMap((group) => ["top", "right", "bottom", "left"].flatMap((side) => [
1138
+ `${group}-[env(safe-area-inset-${side})]`,
1139
+ ...group === "p" ? [] : [`${group}-[max(--spacing(*),env(safe-area-inset-${side}))]`]
1140
+ ]));
1141
+ const illustrationPalette = [
1142
+ "text-color",
1143
+ "bg-color",
1144
+ "gradient-from",
1145
+ "gradient-via",
1146
+ "gradient-to",
1147
+ "border-color",
1148
+ "divide-color",
1149
+ "outline-color",
1150
+ "ring-color",
1151
+ "ring-offset-color",
1152
+ "shadow",
1153
+ "shadow-color",
1154
+ "inset-shadow",
1155
+ "inset-shadow-color",
1156
+ "drop-shadow",
1157
+ "drop-shadow-color",
1158
+ "fill",
1159
+ "stroke",
1160
+ "bg-image",
1161
+ "bg-size",
1162
+ "bg-position",
1163
+ "mask-image",
1164
+ "blur",
1165
+ "saturate"
1166
+ ];
1167
+ const restyle = (base) => ({
1168
+ allow: base,
1169
+ contracts: [
1170
+ { pattern: "^(CardContent|PopoverContent)$", allow: [...base, ...padding] },
1171
+ { pattern: "^TabsContent$", allow: [...base, ...gap, ...padding] },
1172
+ { pattern: "^(Tabs|HoverCardContent|SidebarHeader|BreadcrumbList)$", allow: [...base, ...gap] }
1173
+ ]
1174
+ });
1175
+ policies.shadcn = [
1176
+ presetOverride(webFiles, {
1177
+ "shadcn/no-restyle": ["error", restyle(allow)],
1178
+ "shadcn/no-arbitrary-values": ["error", { allow: ["layout", ...safeArea] }],
1179
+ "shadcn/require-static-classes": "error",
1180
+ "modules/dynamic-classes": "error"
1181
+ }, ui),
1182
+ presetOverride(presetScopes(web, "components/**/*.illustration.tsx"), {
1183
+ "shadcn/no-restyle": ["error", restyle([...allow, ...illustrationPalette])],
1184
+ "shadcn/no-arbitrary-values": ["error", { allow: ["layout", ...safeArea, ...illustrationPalette] }]
1185
+ }, ui),
1186
+ presetOverride(webFiles, {
1187
+ "shadcn/no-unknown-classes": ["error", { allow: [] }]
1188
+ }),
1189
+ presetOverride(presetScopes(web, "components/ui/sonner.tsx"), {
1190
+ "shadcn/no-unknown-classes": ["error", { allow: ["toaster", "toast"] }]
1191
+ })
1192
+ ];
1193
+ }
1194
+ return policies;
1195
+ }
1196
+
1197
+ // src/configs/boundaries.ts
1198
+ import { resolve as resolve2 } from "node:path";
1199
+ function presetBoundariesConfig(context) {
1200
+ const { options, root, architecture, packages, web, tanstackWeb, nextWeb, backend, appFiles, publicEntrypoints, baseImports } = context;
1201
+ const runtimeBoundary = {
1202
+ web: web.map((path) => resolve2(root, path)),
1203
+ backend: backend.map((path) => resolve2(root, path)),
1204
+ aliases: Object.fromEntries(Object.entries(options.imports?.aliases ?? {}).map(([alias, path]) => [alias.replace(/\/$/, ""), resolve2(root, path)])),
1205
+ backendPackages: Object.entries(architecture).flatMap(([directory, { role }]) => {
1206
+ const name = role === "server" || role === "runner" ? presetPackageName(root, directory) : undefined;
1207
+ return name ? [name] : [];
1208
+ })
1209
+ };
1210
+ const policies = {};
1211
+ policies.boundaries = [
1212
+ presetOverride([...appFiles, ...presetScopes(packages, "**/" + presetExtensions)], {
1213
+ "modules/import-boundaries": [
1214
+ "error",
1215
+ {
1216
+ web: web.map((path) => resolve2(root, path)),
1217
+ backend: backend.map((path) => resolve2(root, path)),
1218
+ packages: packages.map((path) => resolve2(root, path)),
1219
+ fileRoles: options.modules?.customFileRoles ?? [],
1220
+ portableLib: true,
1221
+ aliases: Object.fromEntries(Object.entries(options.imports?.aliases ?? {}).map(([alias, path]) => [
1222
+ alias.replace(/\/$/, ""),
1223
+ resolve2(root, path)
1224
+ ])),
1225
+ appPackages: Object.entries(architecture).flatMap(([directory, { role }]) => {
1226
+ const name = role === "web" || role === "server" || role === "runner" ? presetPackageName(root, directory) : undefined;
1227
+ return name ? [name] : [];
1228
+ }),
1229
+ backendPackages: Object.entries(architecture).flatMap(([directory, { role }]) => {
1230
+ const name = role === "server" || role === "runner" ? presetPackageName(root, directory) : undefined;
1231
+ return name ? [name] : [];
1232
+ }),
1233
+ publicEntrypoints
1234
+ }
1235
+ ]
1236
+ }),
1237
+ presetOverride([...presetScopes(tanstackWeb, "services/**/*.{ts,tsx}"), ...presetScopes(tanstackWeb, "lib/**/*.{ts,tsx}")], {
1238
+ "modules/tanstack-runtime": [
1239
+ "error",
1240
+ {
1241
+ ...runtimeBoundary,
1242
+ rpcClients: ((options.orpc?.clientOwnerFile) ? [options.orpc?.clientOwnerFile] : context.rpcClients).map((path) => resolve2(root, path)),
1243
+ serverImports: options.tanstackStart?.additionalServerOnlyImports ?? [],
1244
+ clientImports: options.tanstackStart?.additionalClientOnlyImports ?? [],
1245
+ allowComputedImportsIn: options.tanstackStart?.computedImportAllowedFiles?.map((path) => resolve2(root, path)) ?? []
1246
+ }
1247
+ ]
1248
+ }),
1249
+ presetOverride([...presetScopes(nextWeb, "services/**/*.{ts,tsx}"), ...presetScopes(nextWeb, "lib/**/*.{ts,tsx}")], {
1250
+ "modules/next-runtime": [
1251
+ "error",
1252
+ {
1253
+ ...runtimeBoundary,
1254
+ serverImports: options.next?.additionalServerOnlyImports ?? [],
1255
+ clientImports: options.next?.additionalClientOnlyImports ?? [],
1256
+ allowComputedImportsIn: options.next?.computedImportAllowedFiles?.map((path) => resolve2(root, path)) ?? []
1257
+ }
1258
+ ]
1259
+ }),
1260
+ presetOverride(presetScopes(backend, "rpc/**/*.ts"), {
1261
+ "modules/rpc-database": "error",
1262
+ "no-restricted-imports": ["error", { paths: ["kysely"], patterns: baseImports }]
1263
+ })
1264
+ ];
1265
+ return policies;
1266
+ }
1267
+
1268
+ // src/configs/plugins.ts
1269
+ import { createRequire } from "node:module";
1270
+ var require2 = createRequire(import.meta.url);
1271
+ function presetPluginsConfig(context) {
1272
+ const { options, web, css } = context;
1273
+ const jsPlugins = [
1274
+ { name: "eslint-js", specifier: require2.resolve("oxlint-plugin-eslint") },
1275
+ {
1276
+ name: "import-extra",
1277
+ specifier: require2.resolve("eslint-plugin-import")
1278
+ },
1279
+ require2.resolve("@regru/eslint-plugin-prefer-early-return"),
1280
+ require2.resolve("eslint-plugin-unused-imports"),
1281
+ require2.resolve("eslint-plugin-perfectionist"),
1282
+ require2.resolve("eslint-plugin-tsdoc"),
1283
+ require2.resolve("@eslint-community/eslint-plugin-eslint-comments"),
1284
+ {
1285
+ name: "arch",
1286
+ specifier: require2.resolve("oxlint-plugin-arch")
1287
+ },
1288
+ {
1289
+ name: "dillon-anti-slop",
1290
+ specifier: require2.resolve("oxlint-plugin-arch/rules/dillon-anti-slop")
1291
+ },
1292
+ {
1293
+ name: "modules",
1294
+ specifier: require2.resolve("oxlint-plugin-arch/rules/module-policy")
1295
+ }
1296
+ ];
1297
+ const settings = {
1298
+ react: { version: "19" },
1299
+ "import/resolver": { node: { extensions: [".js", ".jsx", ".ts", ".tsx", ".mts", ".cts"] } }
1300
+ };
1301
+ if (web.length) {
1302
+ jsPlugins.push({
1303
+ name: "react-extra",
1304
+ specifier: require2.resolve("eslint-plugin-react")
1305
+ }, require2.resolve("eslint-plugin-react-you-might-not-need-an-effect"), require2.resolve("eslint-plugin-svg-jsx"));
1306
+ }
1307
+ if (Object.keys(css).length && options.tailwind !== false) {
1308
+ jsPlugins.push(require2.resolve("oxlint-tailwindcss"));
1309
+ settings.tailwindcss = { rootFontSize: options.tailwind?.rootFontSize ?? 16 };
1310
+ if (options.shadcn !== false) {
1311
+ jsPlugins.push(require2.resolve("@shadcn/lint"));
1312
+ settings.shadcn = {
1313
+ ui: options.shadcn?.uiImportPath ?? "@/components/ui",
1314
+ componentImports: options.shadcn?.componentImportSources ?? ["^@/components/"]
1315
+ };
1316
+ }
1317
+ }
1318
+ return { jsPlugins, settings };
1319
+ }
1320
+
1321
+ // src/services/preset-context.ts
1322
+ import { resolve as resolve3, posix as posix2 } from "node:path";
1323
+ function presetResolveContext(options) {
1324
+ const generated = presetGeneratedFiles;
1325
+ const root = resolve3(options.root ?? process.cwd());
1326
+ const configured = options.architecture ?? {
1327
+ "apps/web": "web",
1328
+ "apps/server": "server",
1329
+ scripts: "scripts",
1330
+ packages: "packages"
1331
+ };
1332
+ const entries = Object.entries(configured).map(([path, configuredRoot]) => {
1333
+ const entry = typeof configuredRoot === "string" ? { role: configuredRoot } : configuredRoot;
1334
+ if (!entry || typeof entry !== "object")
1335
+ throw new Error("Architecture entries must be roles or role/layout objects.");
1336
+ if (Object.keys(entry).some((key) => !["role", "layout", "framework", "sourceDir"].includes(key)))
1337
+ throw new Error("Unknown architecture entry option.");
1338
+ const role = entry.role;
1339
+ if ("framework" in entry && entry.framework !== undefined && role !== "web")
1340
+ throw new Error("Only web architecture roots accept a framework.");
1341
+ const framework = role === "web" ? entry.framework ?? options.framework ?? "tanstack-start" : undefined;
1342
+ if (framework !== undefined && framework !== "tanstack-start" && framework !== "next")
1343
+ throw new Error(`Unknown framework: ${framework}`);
1344
+ const sourced = role === "web" || role === "server" || role === "runner";
1345
+ if ("sourceDir" in entry && entry.sourceDir !== undefined && !sourced)
1346
+ throw new Error("Only web, server and runner architecture roots accept a sourceDir.");
1347
+ const sourceDir = entry.sourceDir ?? "src";
1348
+ if (typeof sourceDir !== "string" || sourceDir !== "." && sourceDir.split("/").some((segment) => !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(segment)))
1349
+ throw new Error("Source folders must be `.` or literal root-relative paths without escapes, globs or empty segments.");
1350
+ const portable = path.replaceAll("\\", "/");
1351
+ if (!portable.trim() || posix2.isAbsolute(portable) || /^[a-z]:/i.test(portable))
1352
+ throw new Error("Architecture roots must be nonempty project-relative paths.");
1353
+ const normalized = posix2.normalize(portable).replace(/\/$/, "");
1354
+ if (normalized === ".." || normalized.startsWith("../"))
1355
+ throw new Error("Architecture roots must stay inside the project.");
1356
+ if (!["web", "server", "runner", "scripts", "packages"].includes(role))
1357
+ throw new Error(`Unknown architecture role: ${role}`);
1358
+ if (entry.layout !== undefined && (!entry.layout || typeof entry.layout !== "object" || Array.isArray(entry.layout)))
1359
+ throw new Error("Folder layout must be a path-to-mode object.");
1360
+ const layout = {};
1361
+ if (role === "web" || role === "server" || role === "runner") {
1362
+ layout.services = "domain";
1363
+ }
1364
+ if (role === "web") {
1365
+ layout.hooks = "flat";
1366
+ }
1367
+ for (const [folder, mode] of Object.entries(entry.layout ?? {})) {
1368
+ if (folder.split("/").some((segment) => !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(segment)))
1369
+ throw new Error("Layout folders must be literal source-relative paths without escapes, globs or empty segments.");
1370
+ if (mode !== "flat" && mode !== "domain")
1371
+ throw new Error("Folder layout mode must be flat or domain.");
1372
+ layout[folder] = mode;
1373
+ }
1374
+ const folders2 = Object.keys(layout);
1375
+ for (const [index, folder] of folders2.entries()) {
1376
+ const overlaps = folders2.slice(0, index).some((other) => folder.startsWith(other + "/") || other.startsWith(folder + "/"));
1377
+ if (overlaps) {
1378
+ throw new Error("Layout folders must not overlap.");
1379
+ }
1380
+ }
1381
+ return [
1382
+ normalized,
1383
+ {
1384
+ role,
1385
+ layout,
1386
+ ...framework ? { framework } : {},
1387
+ ...entry.sourceDir === undefined ? {} : { sourceDir }
1388
+ }
1389
+ ];
1390
+ });
1391
+ for (const [index, [path]] of entries.entries()) {
1392
+ const overlaps = entries.slice(0, index).some(([other]) => other === path || other === "." || path === "." || other.startsWith(path + "/") || path.startsWith(other + "/"));
1393
+ if (overlaps) {
1394
+ throw new Error("Architecture roots must not overlap.");
1395
+ }
1396
+ }
1397
+ const architecture = Object.fromEntries(entries);
1398
+ if (options.modules?.customFileRoles?.some((role) => !/^[a-z][a-z0-9-]*$/.test(role)))
1399
+ throw new Error("File roles must be lowercase kebab-case names.");
1400
+ if (options.modules?.customFileRoles?.some((role) => [
1401
+ "service",
1402
+ "client",
1403
+ "server",
1404
+ "rsc",
1405
+ "utils",
1406
+ "action",
1407
+ "query",
1408
+ "types",
1409
+ "constants",
1410
+ "handler",
1411
+ "test",
1412
+ "spec"
1413
+ ].includes(role)))
1414
+ throw new Error("File roles must not replace built-in roles.");
1415
+ if (options.framework !== undefined && options.framework !== "tanstack-start" && options.framework !== "next")
1416
+ throw new Error(`Unknown framework: ${options.framework}`);
1417
+ const source = (path, entry) => posix2.join(path, entry.sourceDir ?? "src");
1418
+ const web = entries.filter(([, { role }]) => role === "web").map(([path, entry]) => source(path, entry));
1419
+ const webFor = (framework) => entries.filter(([, entry]) => entry.framework === framework).map(([path, entry]) => source(path, entry));
1420
+ const tanstackWeb = webFor("tanstack-start");
1421
+ const nextWeb = webFor("next");
1422
+ const backendEntries = entries.filter(([, { role }]) => role === "server" || role === "runner");
1423
+ const backendRoots = backendEntries.map(([path]) => path);
1424
+ const backend = backendEntries.map(([path, entry]) => source(path, entry));
1425
+ const packages = entries.filter(([, { role }]) => role === "packages").map(([path]) => path);
1426
+ const scripts = entries.filter(([, { role }]) => role === "scripts").map(([path]) => path);
1427
+ const folders = entries.flatMap(([path, entry]) => Object.entries(entry.layout).map(([folder, mode]) => ({
1428
+ path: posix2.join(["web", "server", "runner"].includes(entry.role) ? source(path, entry) : path, folder),
1429
+ folder,
1430
+ mode
1431
+ })));
1432
+ const rpcClients = entries.filter(([, entry]) => entry.role === "web").map(([path, entry]) => posix2.join(source(path, entry), "services", entry.layout.services === "domain" ? "rpc/rpc.client.ts" : "rpc.client.ts"));
1433
+ const app = [...web, ...backend];
1434
+ const appFiles = presetScopes(app);
1435
+ const webFiles = presetScopes(web);
1436
+ const backendFiles = presetScopes(backend);
1437
+ const services = presetScopes(app, `services/**/${presetExtensions}`);
1438
+ const utilities = ["**/*.utils.ts", "**/*.constants.ts", "**/*.types.ts"];
1439
+ const types = presetScopes(app, "types/**");
1440
+ const ui = presetScopes(web, "components/ui/**");
1441
+ const testFiles = ["**/{test,tests,e2e}/**/*.{ts,tsx,mts,cts}", "**/*.{test,spec}.{ts,tsx,mts,cts}"];
1442
+ const safetyFiles = [...appFiles, ...testFiles];
1443
+ const components = presetScopes(web, "components/**/*.tsx");
1444
+ const hooks = presetScopes(web, "hooks/**/*.ts");
1445
+ const level = options.severity ?? "error";
1446
+ const publicEntrypoints = options.imports?.backendEntryPoints ?? backendRoots.flatMap((directory) => {
1447
+ const name = presetPackageName(root, directory);
1448
+ return name ? [`${name}/client`] : [];
1449
+ });
1450
+ const baseImports = [
1451
+ {
1452
+ group: ["zod/v4", "zod/v4/**"],
1453
+ message: "Import from zod, not its transition entry point."
1454
+ }
1455
+ ];
1456
+ const tailwind = options.tailwind;
1457
+ const entryPoints = Object.fromEntries(Object.entries(tailwind === false ? {} : tailwind?.cssEntryPointsByRoot ?? {}).map(([path, entry]) => [
1458
+ posix2.normalize(path.replaceAll("\\", "/")).replace(/\/$/, ""),
1459
+ entry
1460
+ ]));
1461
+ const css = tailwind === false ? {} : Object.fromEntries(entries.filter(([, entry]) => entry.role === "web").map(([path, entry]) => {
1462
+ const sourcePath = source(path, entry);
1463
+ const fallback = entry.framework === "next" ? "app/globals.css" : "application/styles.css";
1464
+ return [
1465
+ sourcePath,
1466
+ resolve3(root, entryPoints[path] ?? tailwind?.cssEntryPoint ?? posix2.join(sourcePath, fallback))
1467
+ ];
1468
+ }));
1469
+ return {
1470
+ options,
1471
+ root,
1472
+ architecture,
1473
+ folders,
1474
+ rpcClients,
1475
+ packages,
1476
+ scripts,
1477
+ web,
1478
+ tanstackWeb,
1479
+ nextWeb,
1480
+ backend,
1481
+ backendRoots,
1482
+ app,
1483
+ appFiles,
1484
+ webFiles,
1485
+ backendFiles,
1486
+ services,
1487
+ utilities,
1488
+ types,
1489
+ ui,
1490
+ generated,
1491
+ testFiles,
1492
+ safetyFiles,
1493
+ components,
1494
+ hooks,
1495
+ level,
1496
+ publicEntrypoints,
1497
+ baseImports,
1498
+ css
1499
+ };
1500
+ }
1501
+
1502
+ // src/services/preset-compose.ts
1503
+ function presetCompose(context, policies, plugins) {
1504
+ const { options, level, safetyFiles, ui, generated } = context;
1505
+ const severities = options.ruleSeverities ?? {};
1506
+ const levels = (rules) => presetRuleSeverities(presetSeverity(rules, level), severities);
1507
+ const overrideList = [];
1508
+ for (const name of presetPolicyOrder) {
1509
+ const setting = options.policies?.[name];
1510
+ if (setting === false || !policies[name]) {
1511
+ continue;
1512
+ }
1513
+ const defaults = structuredClone(policies[name]);
1514
+ const entries = typeof setting === "function" ? setting(defaults) : defaults;
1515
+ for (const entry of entries) {
1516
+ overrideList.push({
1517
+ ...entry,
1518
+ rules: entry.rules ? levels(entry.rules) : undefined
1519
+ });
1520
+ }
1521
+ }
1522
+ overrideList.push(presetOverride(safetyFiles, levels({
1523
+ "max-lines": ["error", { max: options.limits?.maxFileLines ?? 400, skipBlankLines: true, skipComments: true }],
1524
+ complexity: ["error", { max: options.limits?.maxFunctionComplexity ?? 32 }]
1525
+ }), ui));
1526
+ const named = new Set([...Object.keys(presetCommonRules), ...overrideList.flatMap((entry) => Object.keys(entry.rules ?? {}))]);
1527
+ const unnamedLevels = Object.fromEntries(Object.entries(severities).filter(([rule]) => !named.has(rule)));
1528
+ for (const [rule, files] of Object.entries(options.ruleExclusions ?? {})) {
1529
+ overrideList.push({ files, rules: { [rule]: "off" } });
1530
+ }
1531
+ return {
1532
+ options: { typeAware: true },
1533
+ categories: { correctness: level },
1534
+ plugins: ["typescript", "react", "react-perf", "jsx-a11y", "import", "unicorn", "promise", "vitest", "node", "oxc"],
1535
+ ignorePatterns: [
1536
+ "**/node_modules/**",
1537
+ "**/dist/**",
1538
+ "**/build/**",
1539
+ "**/coverage/**",
1540
+ "**/.output/**",
1541
+ ...context.nextWeb.length ? ["**/.next/**", "**/next-env.d.ts"] : [],
1542
+ ...generated,
1543
+ ...options.ignorePatterns ?? []
1544
+ ],
1545
+ ...plugins,
1546
+ rules: { ...levels(presetCommonRules), ...unnamedLevels },
1547
+ overrides: overrideList.filter((entry) => entry.files.length > 0)
1548
+ };
1549
+ }
1550
+
1551
+ // src/presets/react-modules-preset.ts
1552
+ function reactModulesPreset(options = {}) {
1553
+ const context = presetResolveContext(options);
1554
+ return presetCompose(context, {
1555
+ ...presetSourceConfig(context),
1556
+ ...presetTypesConfig(context),
1557
+ ...presetModulesConfig(context),
1558
+ ...presetTestsConfig(context),
1559
+ ...presetReactConfig(context),
1560
+ ...presetBoundariesConfig(context)
1561
+ }, presetPluginsConfig(context));
1562
+ }
1563
+
1564
+ // src/presets/next-react-modules-preset.ts
1565
+ function nextReactModulesPreset(options = {}) {
1566
+ return reactModulesPreset({ ...options, framework: options.framework ?? "next" });
1567
+ }
1568
+ export {
1569
+ nextReactModulesPreset as default
1570
+ };