oxlint-plugin-arch 0.11.0 → 0.12.1

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.
Files changed (221) hide show
  1. package/CHANGELOG.md +241 -0
  2. package/README.md +52 -218
  3. package/dist/index.js +5205 -4073
  4. package/dist/preset/groups/backend.d.ts +2 -0
  5. package/dist/preset/groups/basics.d.ts +2 -0
  6. package/dist/preset/groups/comments.d.ts +2 -0
  7. package/dist/preset/groups/console.d.ts +2 -0
  8. package/dist/preset/groups/deny.d.ts +2 -0
  9. package/dist/preset/groups/expo.d.ts +2 -0
  10. package/dist/preset/groups/externals.d.ts +2 -0
  11. package/dist/preset/groups/forms.d.ts +2 -0
  12. package/dist/preset/groups/imports.d.ts +2 -0
  13. package/dist/preset/groups/layout.d.ts +2 -0
  14. package/dist/preset/groups/limits.d.ts +2 -0
  15. package/dist/preset/groups/naming.d.ts +2 -0
  16. package/dist/preset/groups/next.d.ts +2 -0
  17. package/dist/preset/groups/orpc.d.ts +2 -0
  18. package/dist/preset/groups/react.d.ts +2 -0
  19. package/dist/preset/groups/schemas.d.ts +2 -0
  20. package/dist/preset/groups/shadcn.d.ts +2 -0
  21. package/dist/preset/groups/sql.d.ts +2 -0
  22. package/dist/preset/groups/style.d.ts +2 -0
  23. package/dist/preset/groups/tailwind.d.ts +2 -0
  24. package/dist/preset/groups/tanstack.d.ts +2 -0
  25. package/dist/preset/groups/tests.d.ts +2 -0
  26. package/dist/preset/groups/types.d.ts +2 -0
  27. package/dist/preset/preset-detect.d.ts +3 -0
  28. package/dist/preset/preset-folders.d.ts +2 -0
  29. package/dist/preset/preset-recipes.d.ts +82 -0
  30. package/dist/preset/preset-stylesheet.d.ts +1 -0
  31. package/dist/preset/preset-units.d.ts +2 -0
  32. package/dist/preset/preset.constants.d.ts +79 -0
  33. package/dist/preset/preset.d.ts +89 -0
  34. package/dist/preset/preset.js +2299 -0
  35. package/dist/preset/preset.utils.d.ts +14 -0
  36. package/dist/rules/backend/backend-orpc-output.d.ts +1 -0
  37. package/dist/rules/backend/backend-registry.d.ts +1 -0
  38. package/dist/rules/backend/backend-service-functions.d.ts +1 -0
  39. package/dist/rules/backend/backend-sql-like.d.ts +1 -0
  40. package/dist/rules/boundaries/boundaries-deny.d.ts +1 -0
  41. package/dist/rules/boundaries/boundaries-imports.d.ts +1 -0
  42. package/dist/rules/boundaries/boundaries-next.d.ts +1 -0
  43. package/dist/rules/boundaries/boundaries-rpc-database.d.ts +1 -0
  44. package/dist/rules/boundaries/boundaries-tanstack.d.ts +1 -0
  45. package/dist/rules/boundaries/boundaries-token-owner.d.ts +1 -0
  46. package/dist/rules/code/code-constructor.d.ts +1 -0
  47. package/dist/rules/code/code-double-negation.d.ts +1 -0
  48. package/dist/rules/code/code-literal-in.d.ts +1 -0
  49. package/dist/rules/code/code-mutable-state.d.ts +1 -0
  50. package/dist/rules/code/code-object-params.d.ts +1 -0
  51. package/dist/rules/code/code-paired-call.d.ts +1 -0
  52. package/dist/rules/code/code-promise-mutation.d.ts +1 -0
  53. package/dist/rules/code/code-switch.d.ts +1 -0
  54. package/dist/rules/code/code-trivial-functions.d.ts +1 -0
  55. package/dist/rules/comments/comments-code.d.ts +1 -0
  56. package/dist/rules/comments/comments-directives.d.ts +1 -0
  57. package/dist/rules/copy/copy-dashes.d.ts +1 -0
  58. package/dist/rules/files/files-components-only.d.ts +1 -0
  59. package/dist/rules/files/files-constants-only.d.ts +1 -0
  60. package/dist/rules/files/files-domain-constants.d.ts +1 -0
  61. package/dist/rules/files/files-exports.d.ts +1 -0
  62. package/dist/rules/files/files-externals.d.ts +1 -0
  63. package/dist/rules/files/files-factory-keys.d.ts +1 -0
  64. package/dist/rules/files/files-helpers.d.ts +1 -0
  65. package/dist/rules/files/files-no-functions.d.ts +1 -0
  66. package/dist/rules/files/files-restricted.d.ts +1 -0
  67. package/dist/rules/files/files-route.d.ts +1 -0
  68. package/dist/rules/files/files-types-only.d.ts +1 -0
  69. package/dist/rules/layout/layout-call-arguments.d.ts +1 -0
  70. package/dist/rules/layout/layout-call-array.d.ts +1 -0
  71. package/dist/rules/layout/layout-chain.d.ts +1 -0
  72. package/dist/rules/layout/layout-jsx-attributes.d.ts +1 -0
  73. package/dist/rules/layout/layout-key-value.d.ts +1 -0
  74. package/dist/rules/layout/layout-object.d.ts +1 -0
  75. package/dist/rules/layout/layout-padding.d.ts +1 -0
  76. package/dist/rules/layout/layout-ternary.d.ts +1 -0
  77. package/dist/rules/naming/naming-declarations.d.ts +1 -0
  78. package/dist/rules/naming/naming-exports.d.ts +1 -0
  79. package/dist/rules/naming/naming-file.d.ts +1 -0
  80. package/dist/rules/naming/naming-folder.d.ts +1 -0
  81. package/dist/rules/naming/naming-types.d.ts +1 -0
  82. package/dist/rules/react/react-dynamic-classes.d.ts +1 -0
  83. package/dist/rules/react/react-empty-effect.d.ts +1 -0
  84. package/dist/rules/react/react-memo.d.ts +1 -0
  85. package/dist/rules/react/react-props.d.ts +1 -0
  86. package/dist/rules/schemas/schemas-inline.d.ts +1 -0
  87. package/dist/rules/schemas/schemas-local.d.ts +1 -0
  88. package/dist/rules/schemas/schemas-rederive.d.ts +1 -0
  89. package/dist/rules/schemas/schemas-single-use.d.ts +1 -0
  90. package/dist/rules/tests/tests-assertions.d.ts +1 -0
  91. package/dist/rules/tests/tests-concurrent-db.d.ts +1 -0
  92. package/dist/rules/tests/tests-modifiers.d.ts +1 -0
  93. package/dist/rules/tests/tests-titles.d.ts +1 -0
  94. package/dist/rules/types/types-declarations.d.ts +1 -0
  95. package/dist/rules/types/types-imported-alias.d.ts +1 -0
  96. package/dist/rules/types/types-inline.d.ts +1 -0
  97. package/dist/rules/types/types-local-alias.d.ts +1 -0
  98. package/dist/rules/types/types-namespace-import.d.ts +1 -0
  99. package/dist/rules/types/types-service.d.ts +1 -0
  100. package/dist/rules/types/types-unknown.d.ts +1 -0
  101. package/dist/types/ast.types.d.ts +2 -0
  102. package/dist/types/backend.types.d.ts +24 -0
  103. package/dist/types/boundaries.types.d.ts +72 -0
  104. package/dist/types/code.types.d.ts +59 -0
  105. package/dist/types/comments.types.d.ts +12 -0
  106. package/dist/types/copy.types.d.ts +4 -0
  107. package/dist/types/declarations.types.d.ts +20 -0
  108. package/dist/types/dillon-anti-slop.types.d.ts +55 -0
  109. package/dist/types/exports.types.d.ts +11 -0
  110. package/dist/types/files.types.d.ts +85 -0
  111. package/dist/types/layout.types.d.ts +55 -0
  112. package/dist/types/naming.types.d.ts +52 -0
  113. package/dist/types/options.types.d.ts +4 -0
  114. package/dist/types/preset.types.d.ts +248 -129
  115. package/dist/types/react.types.d.ts +14 -0
  116. package/dist/types/schemas.types.d.ts +20 -0
  117. package/dist/types/source.types.d.ts +1 -0
  118. package/dist/types/tests.types.d.ts +39 -0
  119. package/dist/types/types.types.d.ts +20 -0
  120. package/dist/utils/ast.utils.d.ts +19 -0
  121. package/dist/utils/boundaries.utils.d.ts +85 -0
  122. package/dist/utils/declarations.utils.d.ts +21 -0
  123. package/dist/utils/exports.utils.d.ts +6 -0
  124. package/dist/utils/naming.utils.d.ts +11 -0
  125. package/dist/utils/options.utils.d.ts +11 -0
  126. package/dist/utils/react.utils.d.ts +3 -0
  127. package/dist/utils/schemas.utils.d.ts +36 -0
  128. package/dist/utils/source.utils.d.ts +18 -0
  129. package/dist/vendor/dillon-anti-slop/index.js +1770 -0
  130. package/dist/vendor/dillon-anti-slop/rules/no-known-value-widening.d.ts +2 -0
  131. package/dist/vendor/dillon-anti-slop/rules/no-module-mocking.d.ts +2 -0
  132. package/dist/vendor/dillon-anti-slop/shared/dictionary-types.d.ts +5 -0
  133. package/dist/vendor/dillon-anti-slop/shared/expressions.d.ts +5 -0
  134. package/dist/vendor/dillon-anti-slop/shared/lexical-type-parameters.d.ts +2 -0
  135. package/dist/vendor/dillon-anti-slop/shared/reflect-method.d.ts +1 -0
  136. package/dist/vendor/dillon-anti-slop/shared/resolve-variable.d.ts +2 -0
  137. package/dist/vendor/dillon-anti-slop/shared/signatures.d.ts +15 -0
  138. package/dist/vendor/dillon-anti-slop/shared/type-aliases.d.ts +4 -0
  139. package/dist/vendor/dillon-anti-slop/shared/type-environment.d.ts +9 -0
  140. package/dist/vendor/dillon-anti-slop/shared/type-reference-name.d.ts +2 -0
  141. package/dist/vendor/dillon-anti-slop/shared/widening-targets.d.ts +3 -0
  142. package/package.json +51 -43
  143. package/src/{rules → vendor}/dillon-anti-slop/UPSTREAM.md +9 -6
  144. package/dist/configs/boundaries.d.ts +0 -2
  145. package/dist/configs/modules.d.ts +0 -2
  146. package/dist/configs/plugins.d.ts +0 -3
  147. package/dist/configs/react.d.ts +0 -2
  148. package/dist/configs/source.d.ts +0 -2
  149. package/dist/configs/tests.d.ts +0 -2
  150. package/dist/configs/types.d.ts +0 -2
  151. package/dist/constants/preset.constants.d.ts +0 -38
  152. package/dist/constants/test.constants.d.ts +0 -1
  153. package/dist/presets/index.d.ts +0 -4
  154. package/dist/presets/index.js +0 -1736
  155. package/dist/presets/next-react-modules-preset.d.ts +0 -4
  156. package/dist/presets/next-react-modules-preset.js +0 -1729
  157. package/dist/presets/react-modules-preset.d.ts +0 -4
  158. package/dist/presets/react-modules-preset.js +0 -1724
  159. package/dist/presets/tanstack-start-react-modules-preset.d.ts +0 -4
  160. package/dist/presets/tanstack-start-react-modules-preset.js +0 -1729
  161. package/dist/rules/dillon-anti-slop/index.js +0 -2719
  162. package/dist/rules/dillon-anti-slop/rules/no-known-value-widening.d.ts +0 -1
  163. package/dist/rules/dillon-anti-slop/rules/no-module-mocking.d.ts +0 -8
  164. package/dist/rules/dillon-anti-slop/shared/dictionary-types.d.ts +0 -19
  165. package/dist/rules/dillon-anti-slop/shared/lexical-type-parameters.d.ts +0 -4
  166. package/dist/rules/dillon-anti-slop/shared/reflect-method.d.ts +0 -2
  167. package/dist/rules/module-policy/domain-constants.d.ts +0 -2
  168. package/dist/rules/module-policy/import-boundaries.d.ts +0 -2
  169. package/dist/rules/module-policy/index.js +0 -2266
  170. package/dist/rules/module-policy/memoization.d.ts +0 -2
  171. package/dist/rules/module-policy/next-runtime.d.ts +0 -7
  172. package/dist/rules/module-policy/no-unknown.d.ts +0 -6
  173. package/dist/rules/module-policy/reasoned-directives.d.ts +0 -2
  174. package/dist/rules/module-policy/service-functions.d.ts +0 -2
  175. package/dist/rules/module-policy/service-types.d.ts +0 -2
  176. package/dist/rules/module-policy/tanstack-runtime.d.ts +0 -2
  177. package/dist/rules/module-policy/test-assertions.d.ts +0 -2
  178. package/dist/rules/module-policy/test-modifiers.d.ts +0 -2
  179. package/dist/services/preset-compose.d.ts +0 -3
  180. package/dist/services/preset-context.d.ts +0 -2
  181. package/dist/types/module-policy.types.d.ts +0 -39
  182. package/dist/types/test-analysis.types.d.ts +0 -11
  183. package/dist/utils/helpers/ast.d.ts +0 -12
  184. package/dist/utils/helpers/boundaries.d.ts +0 -3
  185. package/dist/utils/helpers/comments.d.ts +0 -6
  186. package/dist/utils/helpers/declarations.d.ts +0 -35
  187. package/dist/utils/helpers/exports.d.ts +0 -18
  188. package/dist/utils/helpers/factories.d.ts +0 -9
  189. package/dist/utils/helpers/guards.d.ts +0 -11
  190. package/dist/utils/helpers/index.d.ts +0 -18
  191. package/dist/utils/helpers/layout.d.ts +0 -15
  192. package/dist/utils/helpers/mocking.d.ts +0 -2
  193. package/dist/utils/helpers/naming.d.ts +0 -13
  194. package/dist/utils/helpers/options.d.ts +0 -22
  195. package/dist/utils/helpers/orpc.d.ts +0 -3
  196. package/dist/utils/helpers/preset.d.ts +0 -9
  197. package/dist/utils/helpers/react-components.d.ts +0 -13
  198. package/dist/utils/helpers/routes.d.ts +0 -5
  199. package/dist/utils/helpers/runtime.d.ts +0 -14
  200. package/dist/utils/helpers/sanitizers.d.ts +0 -3
  201. package/dist/utils/helpers/schemas.d.ts +0 -10
  202. package/dist/utils/helpers/syntax.d.ts +0 -1
  203. package/dist/utils/helpers/test-tables.d.ts +0 -8
  204. package/dist/utils/helpers/trivial.d.ts +0 -6
  205. package/dist/utils/index.d.ts +0 -1
  206. /package/dist/{rules/module-policy/index.d.ts → index.d.ts} +0 -0
  207. /package/dist/{rules → vendor}/dillon-anti-slop/index.d.ts +0 -0
  208. /package/dist/{rules → vendor}/dillon-anti-slop/rules/no-chained-type-assertions.d.ts +0 -0
  209. /package/dist/{rules → vendor}/dillon-anti-slop/rules/no-conditional-empty-object-spread.d.ts +0 -0
  210. /package/dist/{rules → vendor}/dillon-anti-slop/rules/no-object-parameters.d.ts +0 -0
  211. /package/dist/{rules → vendor}/dillon-anti-slop/rules/no-reflect-apply.d.ts +0 -0
  212. /package/dist/{rules → vendor}/dillon-anti-slop/rules/no-reflect-get.d.ts +0 -0
  213. /package/dist/{rules → vendor}/dillon-anti-slop/rules/no-runtime-typeof.d.ts +0 -0
  214. /package/dist/{rules → vendor}/dillon-anti-slop/rules/no-shape-in-symbol-names.d.ts +0 -0
  215. /package/dist/{rules → vendor}/dillon-anti-slop/rules/no-unknown-parameters.d.ts +0 -0
  216. /package/dist/{rules → vendor}/dillon-anti-slop/rules/no-unknown-returns.d.ts +0 -0
  217. /package/dist/{rules → vendor}/dillon-anti-slop/rules/no-unknown-type-aliases.d.ts +0 -0
  218. /package/dist/{rules → vendor}/dillon-anti-slop/rules/no-unsafe-dictionary-type.d.ts +0 -0
  219. /package/dist/{rules → vendor}/dillon-anti-slop/rules/no-widen-then-assert.d.ts +0 -0
  220. /package/dist/{rules → vendor}/dillon-anti-slop/rules/require-safety-comment-for-type-assertion.d.ts +0 -0
  221. /package/src/{rules → vendor}/dillon-anti-slop/LICENSE +0 -0
@@ -0,0 +1,2299 @@
1
+ // src/preset/preset.ts
2
+ import { createRequire } from "node:module";
3
+
4
+ // src/preset/groups/backend.ts
5
+ import { posix as posix2, resolve as resolve3 } from "node:path";
6
+
7
+ // src/preset/preset-detect.ts
8
+ import { existsSync, readdirSync, readFileSync as readFileSync2 } from "node:fs";
9
+ import { matchesGlob, posix, resolve as resolve2 } from "node:path";
10
+
11
+ // src/preset/preset-recipes.ts
12
+ var presetUi = { react: true, kit: true, tailwind: true };
13
+ var presetRecipes = {
14
+ tanstack: { react: true, tanstack: true, tailwind: true, shadcn: true, schemas: true, forms: true },
15
+ next: { react: true, next: true, tailwind: true, shadcn: true, schemas: true, forms: true },
16
+ expo: { react: true, expo: true, schemas: true, forms: true },
17
+ backend: { backend: true, schemas: true, sql: true, layout: { strict: true } },
18
+ react: { react: true, tailwind: true, schemas: true },
19
+ desktop: { console: true },
20
+ library: { schemas: true },
21
+ ui: presetUi,
22
+ kit: presetUi,
23
+ cli: { console: true }
24
+ };
25
+ var presetTemplates = {
26
+ monorepo: {
27
+ "apps/web": presetRecipes.tanstack,
28
+ "apps/server": presetRecipes.backend,
29
+ "packages/*": presetRecipes.library,
30
+ scripts: presetRecipes.cli
31
+ }
32
+ };
33
+
34
+ // src/preset/preset.constants.ts
35
+ var presetExtensions = "*.{ts,tsx,mts,cts}";
36
+ var presetCommonRules = {
37
+ "vitest/expect-expect": "off",
38
+ "vitest/no-standalone-expect": "off",
39
+ "jsx-a11y/no-noninteractive-tabindex": "error",
40
+ "no-case-declarations": "error",
41
+ "no-empty": "error",
42
+ "no-fallthrough": "error",
43
+ "no-prototype-builtins": "error",
44
+ "no-regex-spaces": "error",
45
+ "array-callback-return": "error",
46
+ "default-case-last": "error",
47
+ eqeqeq: "error",
48
+ "no-alert": "error",
49
+ "no-constructor-return": "error",
50
+ "no-else-return": "error",
51
+ "no-extend-native": "error",
52
+ "no-extra-bind": "error",
53
+ "no-extra-label": "error",
54
+ "no-labels": "error",
55
+ "no-lone-blocks": "error",
56
+ "no-new": "error",
57
+ "no-new-func": "error",
58
+ "no-new-wrappers": "error",
59
+ "no-param-reassign": "error",
60
+ "no-proto": "error",
61
+ "no-return-assign": "error",
62
+ "no-script-url": "error",
63
+ "no-self-compare": "error",
64
+ "no-sequences": "error",
65
+ "no-useless-call": "error",
66
+ "no-useless-concat": "error",
67
+ "no-useless-return": "error",
68
+ "no-useless-computed-key": "error",
69
+ "no-var": "error",
70
+ "object-shorthand": "error",
71
+ "prefer-const": "error",
72
+ "prefer-numeric-literals": "error",
73
+ "prefer-rest-params": "error",
74
+ "prefer-spread": "error",
75
+ "prefer-template": "error",
76
+ "symbol-description": "error",
77
+ "no-promise-executor-return": "error",
78
+ "no-template-curly-in-string": "error",
79
+ "new-cap": ["error", { capIsNew: false }],
80
+ "no-lonely-if": "error",
81
+ "no-multi-assign": "error",
82
+ "no-nested-ternary": "error",
83
+ "no-unneeded-ternary": "error",
84
+ "prefer-object-spread": "error",
85
+ "no-label-var": "error",
86
+ "no-await-in-loop": "error",
87
+ curly: "error",
88
+ "typescript/no-array-constructor": "error",
89
+ "typescript/no-dynamic-delete": "error",
90
+ "typescript/no-empty-object-type": "error",
91
+ "typescript/no-invalid-void-type": "error",
92
+ "typescript/no-mixed-enums": "error",
93
+ "typescript/no-namespace": "error",
94
+ "typescript/no-non-null-asserted-nullish-coalescing": "error",
95
+ "typescript/no-non-null-assertion": "error",
96
+ "typescript/no-require-imports": "error",
97
+ "typescript/no-unnecessary-boolean-literal-compare": "error",
98
+ "typescript/no-unnecessary-condition": "error",
99
+ "typescript/no-unnecessary-template-expression": "error",
100
+ "typescript/no-unnecessary-type-arguments": "error",
101
+ "typescript/no-unnecessary-type-assertion": "error",
102
+ "typescript/no-unnecessary-type-constraint": "error",
103
+ "typescript/no-unsafe-enum-comparison": "error",
104
+ "typescript/no-unsafe-function-type": "error",
105
+ "typescript/no-unused-expressions": "error",
106
+ "no-unused-vars": [
107
+ "error",
108
+ { args: "after-used", argsIgnorePattern: "^_", ignoreRestSiblings: false, vars: "all", varsIgnorePattern: "^_" }
109
+ ],
110
+ "typescript/no-useless-constructor": "error",
111
+ "typescript/prefer-literal-enum-member": "error",
112
+ "typescript/prefer-promise-reject-errors": "error",
113
+ "typescript/prefer-reduce-type-parameter": "error",
114
+ "typescript/prefer-return-this-type": "error",
115
+ "typescript/restrict-plus-operands": "error",
116
+ "typescript/return-await": ["error", "error-handling-correctness-only"],
117
+ "typescript/unbound-method": "error",
118
+ "typescript/use-unknown-in-catch-callback-variable": "error",
119
+ "typescript/unified-signatures": "error",
120
+ "typescript/adjacent-overload-signatures": "error",
121
+ "typescript/array-type": "error",
122
+ "typescript/ban-tslint-comment": "error",
123
+ "typescript/class-literal-property-style": "error",
124
+ "typescript/consistent-generic-constructors": "error",
125
+ "typescript/consistent-indexed-object-style": "error",
126
+ "typescript/consistent-type-assertions": "error",
127
+ "typescript/consistent-type-definitions": "error",
128
+ "typescript/consistent-type-imports": [
129
+ "error",
130
+ { prefer: "type-imports", disallowTypeAnnotations: true, fixStyle: "separate-type-imports" }
131
+ ],
132
+ "typescript/no-confusing-non-null-assertion": "error",
133
+ "typescript/no-empty-function": "error",
134
+ "typescript/no-inferrable-types": "error",
135
+ "typescript/non-nullable-type-assertion-style": "error",
136
+ "typescript/prefer-find": "error",
137
+ "typescript/prefer-for-of": "error",
138
+ "typescript/prefer-function-type": "error",
139
+ "typescript/prefer-includes": "error",
140
+ "typescript/prefer-nullish-coalescing": "error",
141
+ "typescript/prefer-optional-chain": "error",
142
+ "typescript/prefer-regexp-exec": "error",
143
+ "typescript/prefer-string-starts-ends-with": "error",
144
+ "typescript/consistent-type-exports": "error",
145
+ "typescript/switch-exhaustiveness-check": "error",
146
+ "typescript/default-param-last": "error",
147
+ "typescript/no-loop-func": "error",
148
+ "typescript/no-shadow": "error",
149
+ "unicorn/prefer-at": "error",
150
+ "unicorn/prefer-node-protocol": "error",
151
+ "react-hooks/rules-of-hooks": "error",
152
+ "react-hooks/exhaustive-deps": "error",
153
+ "oxc/no-accumulating-spread": "error",
154
+ "@regru/prefer-early-return/prefer-early-return": ["error", { maximumStatements: 1 }],
155
+ "tsdoc/syntax": "error",
156
+ "@eslint-community/eslint-comments/no-aggregating-enable": "error",
157
+ "@eslint-community/eslint-comments/no-duplicate-disable": "error",
158
+ "@eslint-community/eslint-comments/no-unused-enable": "error"
159
+ };
160
+ var presetFormattingRules = {
161
+ "eslint-js/padding-line-between-statements": [
162
+ "error",
163
+ { blankLine: "always", prev: ["multiline-const", "multiline-let"], next: "*" },
164
+ { blankLine: "always", prev: "*", next: ["multiline-const", "multiline-let"] }
165
+ ],
166
+ "eslint-js/indent": ["error", 2, { MemberExpression: 1, SwitchCase: 1, flatTernaryExpressions: true }],
167
+ "eslint-js/semi": ["error", "always"],
168
+ "eslint-js/quotes": ["error", "single", { avoidEscape: true }],
169
+ "eslint-js/comma-dangle": ["error", "always-multiline"],
170
+ "eslint-js/no-trailing-spaces": "error",
171
+ "eslint-js/no-multiple-empty-lines": ["error", { max: 1 }],
172
+ "eslint-js/eol-last": ["error", "always"],
173
+ "eslint-js/no-multi-spaces": "error",
174
+ "eslint-js/arrow-spacing": "error",
175
+ "eslint-js/block-spacing": "error",
176
+ "eslint-js/comma-spacing": ["error", { before: false, after: true }],
177
+ "eslint-js/key-spacing": ["error", { beforeColon: false, afterColon: true }],
178
+ "eslint-js/keyword-spacing": ["error", { before: true, after: true }],
179
+ "eslint-js/object-curly-spacing": ["error", "always"],
180
+ "eslint-js/semi-spacing": ["error", { before: false, after: true }],
181
+ "eslint-js/space-before-blocks": "error",
182
+ "eslint-js/space-before-function-paren": ["error", { anonymous: "always", named: "never", asyncArrow: "always" }],
183
+ "eslint-js/space-infix-ops": "error",
184
+ "eslint-js/space-in-parens": ["error", "never"],
185
+ "eslint-js/template-curly-spacing": ["error", "never"],
186
+ "eslint-js/rest-spread-spacing": ["error", "never"],
187
+ "eslint-js/computed-property-spacing": ["error", "never"],
188
+ "eslint-js/brace-style": ["error", "1tbs", { allowSingleLine: false }],
189
+ "eslint-js/arrow-parens": ["error", "always"],
190
+ "eslint-js/no-extra-semi": "error",
191
+ "eslint-js/no-floating-decimal": "error",
192
+ "eslint-js/dot-location": ["error", "property"],
193
+ "eslint-js/no-whitespace-before-property": "error",
194
+ "eslint-js/object-curly-newline": ["error", { consistent: true }],
195
+ "eslint-js/object-property-newline": ["error", { allowAllPropertiesOnSameLine: true }],
196
+ "eslint-js/max-len": [
197
+ "error",
198
+ {
199
+ code: 160,
200
+ ignoreUrls: true,
201
+ ignoreStrings: true,
202
+ ignoreTemplateLiterals: true,
203
+ ignoreRegExpLiterals: true,
204
+ ignoreComments: true
205
+ }
206
+ ]
207
+ };
208
+ var presetReactRules = {
209
+ "react/display-name": "error",
210
+ "react/jsx-no-comment-textnodes": "error",
211
+ "react/jsx-no-target-blank": "error",
212
+ "react/no-unknown-property": "error",
213
+ "react/button-has-type": "error",
214
+ "react/jsx-boolean-value": "error",
215
+ "react/jsx-curly-brace-presence": "error",
216
+ "react/jsx-fragments": "error",
217
+ "react/jsx-no-useless-fragment": "error",
218
+ "react/jsx-pascal-case": "error",
219
+ "react/self-closing-comp": "error",
220
+ "react-perf/jsx-no-new-object-as-prop": "off",
221
+ "react-perf/jsx-no-new-array-as-prop": "off",
222
+ "react-perf/jsx-no-new-function-as-prop": "off",
223
+ "react-perf/jsx-no-jsx-as-prop": "off",
224
+ "react-extra/jsx-sort-props": [
225
+ "error",
226
+ { ignoreCase: true, callbacksLast: true, shorthandFirst: true, multiline: "ignore", locale: "auto" }
227
+ ],
228
+ "react-extra/jsx-first-prop-new-line": ["error", "multiline"],
229
+ "react-extra/jsx-max-props-per-line": ["error", { when: "multiline", maximum: 1 }],
230
+ "react-extra/jsx-indent-props": ["error", 2],
231
+ "react-extra/jsx-closing-bracket-location": ["error", "line-aligned"],
232
+ "react-extra/jsx-wrap-multilines": [
233
+ "error",
234
+ {
235
+ declaration: "parens-new-line",
236
+ assignment: "parens-new-line",
237
+ return: "parens-new-line",
238
+ arrow: "parens-new-line",
239
+ condition: "parens-new-line",
240
+ logical: "parens-new-line",
241
+ prop: "parens-new-line"
242
+ }
243
+ ],
244
+ "react-extra/jsx-curly-newline": ["error", "consistent"],
245
+ "react-extra/jsx-no-leaked-render": "error",
246
+ "react-extra/no-unstable-nested-components": "error",
247
+ "react-extra/function-component-definition": ["error", { namedComponents: "function-declaration" }],
248
+ "react-extra/hook-use-state": "error",
249
+ "react-extra/no-deprecated": "error",
250
+ "react-extra/jsx-uses-vars": "off",
251
+ "react-extra/require-render-return": "error",
252
+ "svg-jsx/camel-case-colon": "error",
253
+ "svg-jsx/no-style-string": "error"
254
+ };
255
+ var presetEffectsRules = {
256
+ "react-you-might-not-need-an-effect/no-adjust-state-on-prop-change": "error",
257
+ "react-you-might-not-need-an-effect/no-reset-all-state-on-prop-change": "error",
258
+ "react-you-might-not-need-an-effect/no-event-handler": "error",
259
+ "react-you-might-not-need-an-effect/no-pass-live-state-to-parent": "error",
260
+ "react-you-might-not-need-an-effect/no-pass-data-to-parent": "error",
261
+ "react-you-might-not-need-an-effect/no-initialize-state": "error",
262
+ "react-you-might-not-need-an-effect/no-chain-state-updates": "error",
263
+ "react-you-might-not-need-an-effect/no-derived-state": "error"
264
+ };
265
+ var presetTailwindRules = {
266
+ "tailwindcss/enforce-canonical": "error",
267
+ "tailwindcss/enforce-sort-order": "error",
268
+ "tailwindcss/enforce-shorthand": "error",
269
+ "tailwindcss/no-conflicting-classes": "error",
270
+ "tailwindcss/no-duplicate-classes": "error",
271
+ "tailwindcss/no-unknown-classes": "off",
272
+ "tailwindcss/no-unnecessary-whitespace": "error",
273
+ "tailwindcss/no-deprecated-classes": "error"
274
+ };
275
+ var presetImportRules = {
276
+ "import/first": "error",
277
+ "import/no-absolute-path": "error",
278
+ "import/no-mutable-exports": "error",
279
+ "import/no-self-import": "error",
280
+ "import/no-duplicates": "error",
281
+ "import/no-named-as-default": "error",
282
+ "import/no-named-as-default-member": "error",
283
+ "import/consistent-type-specifier-style": "error",
284
+ "unused-imports/no-unused-imports": "error",
285
+ "import-extra/no-useless-path-segments": "error",
286
+ "import-extra/no-relative-packages": "error",
287
+ "import-extra/newline-after-import": "error",
288
+ "perfectionist/sort-named-imports": ["error", { type: "natural", ignoreCase: true }]
289
+ };
290
+ var presetNamingRules = {
291
+ "unicorn/filename-case": ["error", { case: "kebabCase" }]
292
+ };
293
+ var presetTestRules = {
294
+ "jest/expect-expect": "off",
295
+ "jest/no-conditional-expect": "off",
296
+ "jest/no-disabled-tests": "off",
297
+ "jest/no-export": "off",
298
+ "jest/no-standalone-expect": "off",
299
+ "jest/require-to-throw-message": "off",
300
+ "vitest/no-conditional-expect": "error",
301
+ "vitest/require-mock-type-parameters": "error"
302
+ };
303
+ var presetModuleMockingApis = ["*.mock", "*.doMock", "*.unstable_mockModule"];
304
+ var presetTestMockingApis = {
305
+ modules: presetModuleMockingApis,
306
+ strict: [...presetModuleMockingApis, "*.spyOn", "*.stubGlobal", "*.stubEnv"]
307
+ };
308
+ var presetTestMockingAllow = {
309
+ modules: [],
310
+ strict: [{ files: ["**/*.driver.test.ts"], apis: ["*"] }]
311
+ };
312
+ var presetStrictMockingMessage = "Inject a fake driver through the dependencies of the code under test instead of mocking or stubbing.";
313
+ var presetNextHandlerMethods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
314
+ var presetNextSegmentConfig = [
315
+ "generateStaticParams",
316
+ "dynamic",
317
+ "dynamicParams",
318
+ "revalidate",
319
+ "fetchCache",
320
+ "runtime",
321
+ "preferredRegion",
322
+ "maxDuration"
323
+ ];
324
+ var presetNextSegmentExports = [
325
+ "generateMetadata",
326
+ "metadata",
327
+ "generateViewport",
328
+ "viewport",
329
+ ...presetNextSegmentConfig
330
+ ];
331
+ var presetExportPrefix = {
332
+ stem: "before-first-dot",
333
+ roles: ["constants", "utils", "handler", "service", "types"],
334
+ separators: [".", "-"],
335
+ normalize: "remove-separators",
336
+ allow: "^(make[A-Z]|\\*$)"
337
+ };
338
+ var presetGeneratedFiles = [
339
+ "**/*.generated.*",
340
+ "**/*.gen.*",
341
+ "**/*.d.ts",
342
+ "**/routes.tree.ts",
343
+ "**/routeTree.gen.ts",
344
+ "**/paraglide/**"
345
+ ];
346
+ var presetBuiltinFileRoles = [
347
+ "service",
348
+ "client",
349
+ "server",
350
+ "rsc",
351
+ "utils",
352
+ "action",
353
+ "query",
354
+ "types",
355
+ "constants",
356
+ "handler",
357
+ "test",
358
+ "spec"
359
+ ];
360
+ var presetShorthandOptions = [["tests", "mocking", "preset"], ["backend", "registry", "file"]];
361
+ var presetOutputFolders = ["dist", "build", "coverage", ".output"];
362
+ var presetLiteralSegment = /^(?!\.{1,2}$)[a-zA-Z0-9._][a-zA-Z0-9._-]*$/;
363
+ var presetIllustrationPalette = [
364
+ "text-color",
365
+ "bg-color",
366
+ "gradient-from",
367
+ "gradient-via",
368
+ "gradient-to",
369
+ "border-color",
370
+ "divide-color",
371
+ "outline-color",
372
+ "ring-color",
373
+ "ring-offset-color",
374
+ "shadow",
375
+ "shadow-color",
376
+ "inset-shadow",
377
+ "inset-shadow-color",
378
+ "drop-shadow",
379
+ "drop-shadow-color",
380
+ "fill",
381
+ "stroke",
382
+ "bg-image",
383
+ "bg-size",
384
+ "bg-position",
385
+ "mask-image",
386
+ "blur",
387
+ "saturate"
388
+ ];
389
+ var presetGroupOrder = [
390
+ "style",
391
+ "imports",
392
+ "types",
393
+ "layout",
394
+ "naming",
395
+ "comments",
396
+ "basics",
397
+ "console",
398
+ "tests",
399
+ "schemas",
400
+ "sql",
401
+ "orpc",
402
+ "react",
403
+ "forms",
404
+ "tanstack",
405
+ "next",
406
+ "expo",
407
+ "backend",
408
+ "tailwind",
409
+ "shadcn",
410
+ "kit",
411
+ "limits",
412
+ "deny",
413
+ "externals"
414
+ ];
415
+ var presetDefaultGroups = [
416
+ "layout",
417
+ "limits",
418
+ "style",
419
+ "basics",
420
+ "naming",
421
+ "comments",
422
+ "types",
423
+ "imports",
424
+ "tests",
425
+ "deny",
426
+ "externals"
427
+ ];
428
+ var presetExternals = [
429
+ "@napi-rs/*",
430
+ "@node-rs/*",
431
+ "@takumi-rs/*",
432
+ "sharp",
433
+ "pg",
434
+ "ioredis",
435
+ "bullmq",
436
+ "tiny-secp256k1",
437
+ "ssh2",
438
+ "playwright",
439
+ "playwright-core"
440
+ ];
441
+ var presetGroupOptions = {
442
+ layout: { level: "error", services: undefined, hooks: undefined, components: undefined, suffixes: [], strict: false, operations: "flat", service: ["make{Domain}Service", "{Domain}Service"] },
443
+ limits: { level: "error", lines: 400, types: 1000, complexity: 32 },
444
+ style: { level: "error", quotes: "single", semicolons: true, multiline: true, padding: true, dashes: true },
445
+ basics: { level: "error", trivial: true, state: true, switch: true, negation: true },
446
+ naming: { level: "error", files: true, exports: true, folders: true, unique: undefined },
447
+ comments: { level: "error", directives: true },
448
+ types: { level: "error", placement: true, safety: true, colocated: [] },
449
+ imports: { level: "error", sort: true, boundaries: true, aliases: {}, internal: undefined },
450
+ tests: { level: "error", assertions: [], functions: [], mocking: "modules", titles: true, fixtures: true },
451
+ react: { level: "error", a11y: true, effects: true, memo: undefined, props: true, perf: undefined, compiler: true },
452
+ tanstack: { level: "error", server: [], client: [], dynamic: [] },
453
+ next: { level: "error", server: [], client: [], dynamic: [] },
454
+ expo: { level: "error", server: [] },
455
+ tailwind: { level: "error", css: undefined, rem: 16 },
456
+ shadcn: { level: "error", ui: "@/components/ui", components: ["^@/components/"] },
457
+ kit: { level: "error" },
458
+ backend: { level: "error", exports: undefined, registry: undefined, writes: ["insertInto", "updateTable", "deleteFrom", "mergeInto", "replaceInto", "dispatch", "send"] },
459
+ orpc: { level: "error", public: [], composers: [], client: undefined },
460
+ schemas: { level: "error" },
461
+ forms: { level: "error", resolver: "standardSchemaResolver" },
462
+ sql: { level: "error", sanitizers: ["escapeLikeWildcards"] },
463
+ console: { level: "error" },
464
+ deny: { level: "error", imports: {}, calls: {} },
465
+ externals: { level: "error", packages: presetExternals }
466
+ };
467
+ var presetFolderKeys = ["severity", "source", "rules"];
468
+ var presetA11yRules = [
469
+ "alt-text",
470
+ "anchor-has-content",
471
+ "anchor-is-valid",
472
+ "aria-activedescendant-has-tabindex",
473
+ "aria-props",
474
+ "aria-proptypes",
475
+ "aria-role",
476
+ "aria-unsupported-elements",
477
+ "autocomplete-valid",
478
+ "click-events-have-key-events",
479
+ "control-has-associated-label",
480
+ "heading-has-content",
481
+ "html-has-lang",
482
+ "iframe-has-title",
483
+ "img-redundant-alt",
484
+ "interactive-supports-focus",
485
+ "label-has-associated-control",
486
+ "lang",
487
+ "media-has-caption",
488
+ "mouse-events-have-key-events",
489
+ "no-access-key",
490
+ "no-aria-hidden-on-focusable",
491
+ "no-autofocus",
492
+ "no-distracting-elements",
493
+ "no-interactive-element-to-noninteractive-role",
494
+ "no-noninteractive-element-interactions",
495
+ "no-noninteractive-element-to-interactive-role",
496
+ "no-noninteractive-tabindex",
497
+ "no-redundant-roles",
498
+ "no-static-element-interactions",
499
+ "prefer-tag-over-role",
500
+ "role-has-required-aria-props",
501
+ "role-supports-aria-props",
502
+ "scope",
503
+ "tabindex-no-positive"
504
+ ].map((rule) => `jsx-a11y/${rule}`);
505
+ var presetExpoRouteExports = ["ErrorBoundary", "unstable_settings", "generateStaticParams"];
506
+ var presetTestFiles = ["**/{test,tests,e2e}/**/*.{ts,tsx,mts,cts}", "**/*.{test,spec}.{ts,tsx,mts,cts}"];
507
+ var presetBaseImports = [{ group: ["zod/v4", "zod/v4/**"], message: "Import from zod, not its transition entry point." }];
508
+ var presetDetectFrameworks = [
509
+ ["next", ["next", "vinext"]],
510
+ ["tanstack", ["@tanstack/react-start"]],
511
+ ["expo", ["expo"]]
512
+ ];
513
+ var presetDetectDesktop = ["electrobun", "electron", "@tauri-apps/api", "@tauri-apps/cli"];
514
+ var presetDetectBackend = ["@orpc/server", "hono", "kysely", "drizzle-orm", "express", "fastify", "elysia"];
515
+ var presetDetectSkip = ["node_modules", "dist", "build", "coverage", "public", ".output", ".next", ".vinext", ".turbo", ".wrangler", "target"];
516
+ var presetStylesheetNames = ["styles", "globals", "global", "app", "index", "main", "style", "tailwind"];
517
+ var presetRegistryFiles = ["services/service.ts", "core/core.services.ts", "service.ts"];
518
+ var presetRegistrySkip = ["brand", "database", "db", "math", "security", "tools"];
519
+ var presetServerEntries = ["rpc", "server", "security", "services"];
520
+ var presetOperationImports = ["*/*-action.*", "*/*-query.*", "*/actions/*", "*/queries/*"];
521
+ var presetKitImports = ["**/kits", "**/kits/index", "**/kits/index.ts"];
522
+ var presetKitFiles = "^(?:kit|[a-z0-9]+(?:-[a-z0-9]+)*\\.kit)\\.ts$";
523
+ var presetCliFiles = "^cli(?:-[a-z0-9]+)*(?:\\.[a-z0-9-]+)*\\.tsx?$";
524
+ var presetMigrationFiles = "^(?:\\d{3,14}[-_][a-z0-9]+(?:[-_][a-z0-9]+)*|index)\\.ts$";
525
+ var presetMigrationNumber = "^(\\d{3,14})[-_]";
526
+ var presetRootImports = { "framer-motion": "Use motion/react." };
527
+ var presetPackageImports = { "../../apps/**": "Packages stay app-agnostic; do not import app internals." };
528
+
529
+ // src/preset/preset.utils.ts
530
+ import { readFileSync, statSync } from "node:fs";
531
+ import { resolve } from "node:path";
532
+ class PresetError extends Error {
533
+ }
534
+ function presetScopes(roots, suffix = `**/${presetExtensions}`) {
535
+ return roots.map((root) => root === "." ? suffix : `${root}/${suffix}`);
536
+ }
537
+ function presetOverride(files, rules, excludeFiles = []) {
538
+ return {
539
+ files,
540
+ excludeFiles: [...presetGeneratedFiles, ...excludeFiles],
541
+ rules
542
+ };
543
+ }
544
+ function presetSeverity(rules, level) {
545
+ return Object.fromEntries(Object.entries(rules).map(([name, value]) => {
546
+ const current = Array.isArray(value) ? value[0] : value;
547
+ if (current === "off" || current === "allow" || current === 0) {
548
+ return [name, value];
549
+ }
550
+ return [name, Array.isArray(value) ? [level, ...value.slice(1)] : level];
551
+ }));
552
+ }
553
+ function presetPackageName(root, directory) {
554
+ try {
555
+ const manifest = JSON.parse(readFileSync(resolve(root, directory, "package.json"), "utf8"));
556
+ return typeof manifest.name === "string" ? manifest.name : undefined;
557
+ } catch {
558
+ return;
559
+ }
560
+ }
561
+ function presetSegment(pattern, segment) {
562
+ if (pattern === segment || pattern === "*" || segment === "*") {
563
+ return true;
564
+ }
565
+ const source = pattern.replaceAll(/[.+^$()|[\]\\]/g, "\\$&").replaceAll("?", ".").replaceAll("*", "[^/]*").replaceAll(/\{([^}]*)\}/g, (_, options) => `(?:${options.split(",").join("|")})`);
566
+ return new RegExp(`^${source}$`).test(segment);
567
+ }
568
+ function presetIntersectSegments(glob, scope) {
569
+ if (scope.length === 0) {
570
+ return glob.length > 0 ? [glob] : [];
571
+ }
572
+ if (glob.length === 0) {
573
+ return [];
574
+ }
575
+ const [head, ...rest] = glob;
576
+ if (head === "**") {
577
+ const deeper = scope.map((_, index) => presetIntersectSegments(rest, scope.slice(index)).map((tail) => [...scope.slice(0, index), ...tail]));
578
+ return [[...scope, ...glob], ...deeper.flat()];
579
+ }
580
+ if (!presetSegment(head, scope[0])) {
581
+ return [];
582
+ }
583
+ const segment = scope[0] === "*" ? head : scope[0];
584
+ return presetIntersectSegments(rest, scope.slice(1)).map((tail) => [segment, ...tail]);
585
+ }
586
+ function presetIntersect(glob, scope) {
587
+ if (scope === ".") {
588
+ return [glob];
589
+ }
590
+ const matches = presetIntersectSegments(glob.split("/"), scope.split("/")).map((segments) => segments.join("/"));
591
+ return [...new Set(matches)];
592
+ }
593
+ function presetAliases(root, aliases) {
594
+ return Object.fromEntries(Object.entries(aliases ?? {}).map(([alias, path]) => [alias.replace(/\/$/, ""), resolve(root, path)]));
595
+ }
596
+ function presetRuntime(project, settings, framework, rule) {
597
+ return project.units.flatMap((unit) => {
598
+ if (unit.framework !== framework) {
599
+ return [];
600
+ }
601
+ const options = {
602
+ web: project.web.map((path) => resolve(project.root, path)),
603
+ backend: project.backend.map((path) => resolve(project.root, path)),
604
+ aliases: presetAliases(project.root, unit.scope.effective.imports?.aliases),
605
+ backendPackages: project.units.flatMap((item) => item.kind === "backend" ? presetPackageName(project.root, item.key) ?? [] : []),
606
+ server: settings.server,
607
+ client: settings.client,
608
+ dynamic: settings.dynamic.map((path) => resolve(project.root, path))
609
+ };
610
+ if (rule === "arch/boundaries-tanstack") {
611
+ options.rpc = project.rpcClients.map((path) => resolve(project.root, path));
612
+ }
613
+ return [
614
+ presetOverride([...presetScopes([unit.source], "services/**/*.{ts,tsx}"), ...presetScopes([unit.source], "lib/**/*.{ts,tsx}")], {
615
+ [rule]: ["error", options]
616
+ })
617
+ ];
618
+ });
619
+ }
620
+ function presetManifest(root, directory) {
621
+ try {
622
+ return JSON.parse(readFileSync(resolve(root, directory, "package.json"), "utf8"));
623
+ } catch {
624
+ return {};
625
+ }
626
+ }
627
+ function presetIsDirectory(path) {
628
+ try {
629
+ return statSync(path, { throwIfNoEntry: false })?.isDirectory() ?? false;
630
+ } catch {
631
+ return false;
632
+ }
633
+ }
634
+ function presetIsFile(path) {
635
+ try {
636
+ return statSync(path, { throwIfNoEntry: false })?.isFile() ?? false;
637
+ } catch {
638
+ return false;
639
+ }
640
+ }
641
+
642
+ // src/preset/preset-detect.ts
643
+ function presetDirectories(root, base, skip = presetDetectSkip) {
644
+ if (!presetIsDirectory(resolve2(root, base))) {
645
+ return [];
646
+ }
647
+ return readdirSync(resolve2(root, base)).filter((name) => !name.startsWith(".") && !skip.includes(name) && presetIsDirectory(resolve2(root, base, name))).map((name) => posix.join(base, name));
648
+ }
649
+ var presetNamedSkip = ["node_modules"];
650
+ function presetSource(root, directory, configured) {
651
+ if (configured !== undefined) {
652
+ return posix.join(directory, configured);
653
+ }
654
+ return existsSync(resolve2(root, directory, "src")) ? posix.join(directory, "src") : directory;
655
+ }
656
+ function presetRegistryFile(root, source, configured) {
657
+ if (configured === false) {
658
+ return;
659
+ }
660
+ const file = typeof configured === "string" ? configured : configured?.file;
661
+ if (file !== undefined) {
662
+ return posix.normalize(file);
663
+ }
664
+ return presetRegistryFiles.map((path) => posix.join(source, path)).find((path) => existsSync(resolve2(root, path)));
665
+ }
666
+ function presetDomains(root, source) {
667
+ return presetDirectories(root, posix.join(source, "services"), presetNamedSkip).map((path) => posix.basename(path)).filter((name) => existsSync(resolve2(root, source, "services", name, `${name}.service.ts`)));
668
+ }
669
+ function presetRecipe(root, directory, app, configured) {
670
+ const manifest = presetManifest(root, directory);
671
+ const dependencies = new Set([manifest.dependencies, manifest.devDependencies, manifest.peerDependencies].flatMap((listed) => Object.keys(listed ?? {})));
672
+ const has = (names) => names.some((name) => dependencies.has(name));
673
+ const source = presetSource(root, directory, configured);
674
+ const framework = presetDetectFrameworks.find(([, names]) => has(names))?.[0];
675
+ const services = resolve2(root, source, "services");
676
+ const entries = presetIsDirectory(services) ? readdirSync(services, { withFileTypes: true }) : [];
677
+ const folders = entries.filter((entry) => presetIsDirectory(resolve2(services, entry.name))).length;
678
+ const flat = entries.filter((entry) => entry.isFile() && /\.tsx?$/.test(entry.name)).length > folders;
679
+ if (framework) {
680
+ return flat ? { ...presetRecipes[framework], layout: { services: "flat" } } : presetRecipes[framework];
681
+ }
682
+ if (has(presetDetectDesktop)) {
683
+ return presetRecipes.desktop;
684
+ }
685
+ if (presetDomains(root, source).length > 0 || has(presetDetectBackend) && (app || presetIsDirectory(services))) {
686
+ return presetRecipes.backend;
687
+ }
688
+ if (has(["react"]) && presetIsDirectory(resolve2(root, source, "components"))) {
689
+ return app ? presetRecipes.react : presetRecipes.ui;
690
+ }
691
+ return presetRecipes.library;
692
+ }
693
+ function presetMergeObject(base, top, depth) {
694
+ const merged = { ...base };
695
+ for (const [key, value] of Object.entries(top)) {
696
+ const previous = Reflect.get(merged, key);
697
+ const nested = previous instanceof Object && value instanceof Object && !Array.isArray(previous) && !Array.isArray(value);
698
+ if (nested && depth > 0) {
699
+ Reflect.set(merged, key, presetMergeObject(previous, value, depth - 1));
700
+ } else if (value !== true || !(previous instanceof Object)) {
701
+ Reflect.set(merged, key, value);
702
+ }
703
+ }
704
+ return merged;
705
+ }
706
+ function presetMerge(base, top) {
707
+ if (!(base instanceof Object) || !(top instanceof Object)) {
708
+ return top === true && base instanceof Object ? base : top;
709
+ }
710
+ return presetMergeObject(base, top, 2);
711
+ }
712
+ function presetImportTargets(value) {
713
+ if (typeof value === "string") {
714
+ return [value];
715
+ }
716
+ if (Array.isArray(value)) {
717
+ return value.flatMap(presetImportTargets);
718
+ }
719
+ return value instanceof Object ? Object.values(value).flatMap(presetImportTargets) : [];
720
+ }
721
+ function presetSpecifiers(manifest, target, file = false) {
722
+ const specifiers = Object.entries(manifest.imports ?? {}).flatMap(([key, value]) => presetImportTargets(value).flatMap((mapped) => {
723
+ const path = posix.normalize(mapped);
724
+ const parts = path.split("*");
725
+ const keyParts = key.split("*");
726
+ if (parts.length === 1 && keyParts.length === 1) {
727
+ return path.replace(/\.[cm]?[jt]sx?$/, "") === target ? [key] : [];
728
+ }
729
+ if (parts.length !== 2 || keyParts.length !== 2) {
730
+ return [];
731
+ }
732
+ const [before, after] = parts;
733
+ const [head, suffix] = keyParts;
734
+ const prefix = before.replace(/\/$/, "");
735
+ if (target !== prefix && !target.startsWith(`${prefix}/`)) {
736
+ return [];
737
+ }
738
+ const rest = target.slice(prefix.length).replace(/^\//, "");
739
+ const tail = after && rest.endsWith(after) ? rest.slice(0, -after.length) : rest;
740
+ return [`${head}${tail}${file ? suffix : ""}`.replace(/\/$/, "")];
741
+ }));
742
+ return [...new Set(specifiers)];
743
+ }
744
+ function presetOverlaps(left, right) {
745
+ const [leftBase] = left.split("*");
746
+ const [rightBase] = right.split("*");
747
+ return leftBase.startsWith(rightBase) || rightBase.startsWith(leftBase);
748
+ }
749
+ function presetDeny(imports) {
750
+ return { deny: { imports } };
751
+ }
752
+ function presetFilled(conventions, exists) {
753
+ return Object.fromEntries(Object.entries(conventions).filter(([path, folder]) => {
754
+ const empty = folder instanceof Object && folder.deny instanceof Object && Object.keys(folder.deny.imports ?? {}).length === 0;
755
+ return exists(path) && !empty;
756
+ }));
757
+ }
758
+ function presetBackendConventions(root, key, value, denied) {
759
+ const manifest = presetManifest(root, key);
760
+ const source = presetSource(root, key, value.source);
761
+ const local = posix.relative(key, source) || ".";
762
+ const at = (path) => local === "." ? path : posix.join(local, path);
763
+ const exists = (path) => existsSync(resolve2(root, key, path));
764
+ const configured = value.backend instanceof Object ? value.backend.registry : undefined;
765
+ const registryFile = presetRegistryFile(root, source, configured);
766
+ const registry = registryFile === undefined ? undefined : posix.relative(key, registryFile);
767
+ const services = presetSpecifiers(manifest, at("services"));
768
+ const message = "Use the service API, not another service’s operations.";
769
+ const operations = Object.fromEntries(services.flatMap((prefix) => presetOperationImports.map((pattern) => [`${prefix}/${pattern}`, message])));
770
+ const registryImports = registry ? Object.fromEntries(presetSpecifiers(manifest, registry.replace(/\.[cm]?[jt]sx?$/, ""), true).map((specifier) => [specifier, "Only the composition root imports the service registry."])) : {};
771
+ const entries = Object.fromEntries(presetServerEntries.map((entry) => [`${manifest.name}/${entry}`, "Server code imports its own modules, not the package entries."]));
772
+ const serviceImports = { ...registryImports, ...operations };
773
+ if (manifest.name) {
774
+ Object.assign(serviceImports, entries);
775
+ }
776
+ const conventions = {
777
+ [at("services")]: presetDeny(serviceImports),
778
+ [at("core")]: presetDeny(operations),
779
+ [at("rpc")]: presetDeny(registryImports),
780
+ [at("routes")]: presetDeny(registryImports),
781
+ [at("cli")]: {
782
+ naming: { files: presetCliFiles },
783
+ ...presetDeny({ ...operations, ...Object.fromEntries(services.map((prefix) => [`${prefix}/*/*.service`, "Reach services through the registry."])) })
784
+ }
785
+ };
786
+ for (const domain of presetDirectories(root, posix.join(source, "services"), presetNamedSkip).map((path) => posix.basename(path))) {
787
+ const imports = services.flatMap((prefix) => presetOperationImports.filter((pattern) => !denied.has(`${prefix}/${pattern}`)).flatMap((pattern) => [
788
+ [`${prefix}/${pattern}`, false],
789
+ [`${prefix}/${pattern.replace(/^\*/, `!(${domain})`)}`, message]
790
+ ]));
791
+ conventions[at(`services/${domain}`)] = presetDeny(Object.fromEntries(imports));
792
+ }
793
+ for (const tests of ["tests", "test"]) {
794
+ if (exists(posix.join(tests, "kits"))) {
795
+ conventions[tests] = presetDeny(Object.fromEntries(presetKitImports.map((pattern) => [pattern, "Import the kit file you need."])));
796
+ conventions[posix.join(tests, "kits")] = { naming: { files: presetKitFiles } };
797
+ }
798
+ }
799
+ const queue = ["."];
800
+ while (queue.length > 0) {
801
+ const folder = queue.shift() ?? ".";
802
+ for (const child of presetDirectories(root, posix.join(key, folder)).map((path) => posix.relative(key, path))) {
803
+ if (posix.basename(child) === "migrations") {
804
+ conventions[child] = { layout: false, naming: { files: presetMigrationFiles, unique: presetMigrationNumber } };
805
+ } else if (child.split("/").length < 8) {
806
+ queue.push(child);
807
+ }
808
+ }
809
+ }
810
+ return presetFilled(conventions, exists);
811
+ }
812
+ function presetWebConventions(root, key, value, servers) {
813
+ const source = posix.relative(key, presetSource(root, key, value.source)) || ".";
814
+ const at = (path) => source === "." ? path : posix.join(source, path);
815
+ const exists = (path) => existsSync(resolve2(root, key, path));
816
+ const own = Object.keys(presetManifest(root, key).imports ?? {});
817
+ const internals = servers.flatMap((manifest) => Object.keys(manifest.imports ?? {}).filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => specifier.replace(/\*$/, "**")));
818
+ const named = servers.flatMap((manifest) => manifest.name ? [manifest.name] : []);
819
+ const entries = Object.fromEntries(named.flatMap((name) => presetServerEntries.map((entry) => [`${name}/${entry}`, "UI code imports the client entry."])));
820
+ const clients = Object.fromEntries(named.map((name) => [`${name}/client`, "Route handlers run on the server; import the server entry."]));
821
+ const appImports = Object.fromEntries(internals.map((specifier) => [specifier, "Import the backend through its client entry."]));
822
+ if (servers.length > 0) {
823
+ appImports["**/services/*.service"] = "Import the client or rsc service file, not a server service.";
824
+ }
825
+ const conventions = {
826
+ ".": presetDeny(appImports),
827
+ [at("components")]: presetDeny(entries),
828
+ [at("routes")]: presetDeny(entries),
829
+ [at("routes/handlers")]: { deny: { imports: { ...clients, ...Object.fromEntries(Object.keys(entries).map((entry) => [entry, false])) } } }
830
+ };
831
+ return presetFilled(conventions, exists);
832
+ }
833
+ function presetWorkspacePatterns(root, manifest) {
834
+ const listed = Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? [];
835
+ const file = resolve2(root, "pnpm-workspace.yaml");
836
+ if (!existsSync(file)) {
837
+ return listed;
838
+ }
839
+ const patterns = [...listed];
840
+ let inside = false;
841
+ for (const line of readFileSync2(file, "utf8").split(`
842
+ `)) {
843
+ const flow = /^packages\s*:\s*\[(.*)\]/.exec(line);
844
+ const item = /^\s*-\s*(["']?)([^"'#\s]+)\1\s*(?:#.*)?$/.exec(line);
845
+ if (flow) {
846
+ patterns.push(...flow[1].split(",").map((entry) => entry.trim().replaceAll(/^["']|["']$/g, "")).filter(Boolean));
847
+ inside = false;
848
+ } else if (/^packages\s*:/.test(line)) {
849
+ inside = true;
850
+ } else if (inside && item) {
851
+ patterns.push(item[2]);
852
+ } else if (/^\S/.test(line)) {
853
+ inside = false;
854
+ }
855
+ }
856
+ return patterns;
857
+ }
858
+ function presetWorkspaces(root, patterns) {
859
+ const clean = (pattern) => posix.normalize(pattern.replace(/^\.\//, "")).replace(/\/$/, "");
860
+ const include = patterns.filter((pattern) => !pattern.startsWith("!")).map(clean);
861
+ const exclude = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => clean(pattern.slice(1)));
862
+ const matches = (path, globs) => globs.some((glob) => path === glob || matchesGlob(path, glob));
863
+ const candidates = new Set;
864
+ for (const pattern of include) {
865
+ const segments = pattern.split("/");
866
+ const literal = segments.findIndex((segment) => /[*?{[]/.test(segment));
867
+ if (literal === -1) {
868
+ candidates.add(pattern);
869
+ continue;
870
+ }
871
+ const depth = segments.includes("**") ? 8 : segments.length - literal;
872
+ let level = [segments.slice(0, literal).join("/") || "."];
873
+ for (let step = 0;step < depth; step++) {
874
+ level = level.flatMap((folder) => presetDirectories(root, folder, presetNamedSkip));
875
+ level.forEach((folder) => candidates.add(folder));
876
+ }
877
+ }
878
+ return [...candidates].filter((path) => matches(path, include) && !matches(path, exclude) && existsSync(resolve2(root, path, "package.json"))).toSorted();
879
+ }
880
+ function presetContains(key, path) {
881
+ if (key === "." || key === path) {
882
+ return true;
883
+ }
884
+ const pattern = key.split("/");
885
+ const segments = path.split("/");
886
+ return pattern.length <= segments.length && pattern.every((segment, index) => segment === "*" || segment === segments[index]);
887
+ }
888
+ function presetOwner(folders, path, owns) {
889
+ const depth = (key) => key === "." ? 0 : key.split("/").length;
890
+ return Object.entries(folders).filter(([key, value]) => presetContains(key, path) && owns(value)).toSorted(([left], [right]) => depth(left) - depth(right) || Number(right.includes("*")) - Number(left.includes("*"))).at(-1)?.[1];
891
+ }
892
+ function presetGroupOff(folders, path, group) {
893
+ const owner = presetOwner(folders, path, (value) => value instanceof Object && (group in value));
894
+ return owner instanceof Object && Object(owner)[group] === false;
895
+ }
896
+ function presetDetect(root, folders) {
897
+ const manifest = presetManifest(root, ".");
898
+ const configuredSource = (key) => {
899
+ const value = folders[key];
900
+ return value instanceof Object ? value.source : undefined;
901
+ };
902
+ const patterns = presetWorkspacePatterns(root, manifest);
903
+ const directories = presetWorkspaces(root, patterns);
904
+ const detected = Object.fromEntries(directories.map((directory) => [directory, presetRecipe(root, directory, !directory.startsWith("packages/"), configuredSource(directory))]));
905
+ const rootRecipe = presetRecipe(root, ".", true, configuredSource("."));
906
+ const single = Object.keys(manifest).length > 0 || rootRecipe !== presetRecipes.library;
907
+ const rootApp = rootRecipe !== presetRecipes.library && presetSource(root, ".", configuredSource(".")) !== ".";
908
+ if (patterns.length === 0 ? single : rootApp) {
909
+ detected["."] = rootRecipe;
910
+ }
911
+ if (existsSync(resolve2(root, "scripts")) && !directories.includes("scripts")) {
912
+ detected.scripts = presetRecipes.cli;
913
+ }
914
+ const keys = Object.keys(folders);
915
+ const covered = (key) => keys.some((user) => user !== key && user !== "." && (key.startsWith(`${user}/`) || user.endsWith("/*") && posix.dirname(key) === user.slice(0, -2)));
916
+ const merged = { ".": presetDeny(presetRootImports) };
917
+ for (const [key, value] of Object.entries(detected)) {
918
+ if (!covered(key)) {
919
+ merged[key] = presetMerge(merged[key], value);
920
+ }
921
+ }
922
+ for (const [key, value] of Object.entries(folders)) {
923
+ merged[key] = key in merged ? presetMerge(merged[key], value) : value;
924
+ }
925
+ const length = (key) => key.split("/").length;
926
+ const nested = (key) => Object.keys(merged).some((other) => other !== "." && length(other) < length(key) && presetContains(other, key));
927
+ const units = Object.entries(merged).flatMap(([key, value]) => {
928
+ if (key.endsWith("/*")) {
929
+ return presetDirectories(root, key.slice(0, -2), presetNamedSkip).filter((directory) => !(directory in merged)).map((directory) => [directory, value]);
930
+ }
931
+ if (key.includes("*") || nested(key)) {
932
+ return [];
933
+ }
934
+ const siblings = Object.entries(merged).filter(([other]) => other !== key && other.endsWith("/*") && length(other) === length(key) && presetContains(other, key));
935
+ return [[key, siblings.reduce((inherited, [, sibling]) => presetMerge(sibling, inherited), value)]];
936
+ });
937
+ const denied = new Set(Object.values(folders).flatMap((folder) => folder instanceof Object && folder.deny instanceof Object ? Object.entries(folder.deny.imports ?? {}).flatMap(([pattern, message]) => typeof message === "string" ? [pattern] : []) : []));
938
+ const servers = units.filter(([, value]) => value instanceof Object && Boolean(value.backend)).map(([key]) => presetManifest(root, key));
939
+ const internals = servers.flatMap((server) => Object.keys(server.imports ?? {}).map((specifier) => specifier.replace(/\*$/, "**")));
940
+ const add = (key, folder) => {
941
+ if (presetOwner(folders, key, () => true) === false) {
942
+ return;
943
+ }
944
+ const kept = folder instanceof Object ? Object.fromEntries(Object.entries(folder).filter(([group]) => !presetGroupOff(folders, key, group))) : folder;
945
+ if (kept instanceof Object && Object.keys(kept).length === 0) {
946
+ return;
947
+ }
948
+ merged[key] = key in merged ? presetMerge(kept, merged[key]) : kept;
949
+ };
950
+ for (const [key, value] of units) {
951
+ if (!(value instanceof Object)) {
952
+ continue;
953
+ }
954
+ if (value.backend) {
955
+ for (const [path, folder] of Object.entries(presetBackendConventions(root, key, value, denied))) {
956
+ add(path === "." ? key : posix.join(key, path), folder);
957
+ }
958
+ } else if (value.tanstack || value.next) {
959
+ for (const [path, folder] of Object.entries(presetWebConventions(root, key, value, servers))) {
960
+ add(path === "." ? key : posix.join(key, path), folder);
961
+ }
962
+ } else if ((value.kit || key.startsWith("packages/")) && !value.expo) {
963
+ const own = Object.keys(presetManifest(root, key).imports ?? {});
964
+ const message = presetPackageImports["../../apps/**"];
965
+ const appImports = internals.filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => [specifier, message]);
966
+ add(key, presetDeny({ ...presetPackageImports, ...Object.fromEntries(appImports) }));
967
+ }
968
+ }
969
+ return merged;
970
+ }
971
+
972
+ // src/preset/groups/backend.ts
973
+ var backendGroup = (project, settings) => {
974
+ const overrides = [
975
+ presetOverride(presetScopes(project.backend, "{rpc,routes}/**/*.ts"), {
976
+ "arch/boundaries-rpc-database": "error",
977
+ "no-restricted-imports": ["error", { paths: ["kysely", "drizzle-orm"], patterns: project.baseImports }]
978
+ }),
979
+ presetOverride(presetScopes(project.backend), {
980
+ "eslint-js/no-restricted-syntax": ["error", { selector: ':function > Identifier.params[name="trx"]', message: "Name the transaction db so helpers take either." }]
981
+ })
982
+ ];
983
+ if (settings.registry === false) {
984
+ return overrides;
985
+ }
986
+ const configured = typeof settings.registry === "string" ? { file: settings.registry } : settings.registry ?? {};
987
+ const backendUnits = project.units.filter((unit) => unit.kind === "backend");
988
+ const file = configured.file === undefined ? undefined : posix2.normalize(configured.file);
989
+ const registries = file === undefined ? backendUnits.flatMap((unit) => {
990
+ const found = presetRegistryFile(project.root, unit.source, undefined);
991
+ return found ? [{ file: found, unit }] : [];
992
+ }) : [{ file, unit: backendUnits.find((unit) => unit.key === "." || file.startsWith(`${unit.key}/`)) }];
993
+ for (const registry of registries) {
994
+ if (!registry.unit) {
995
+ throw new PresetError(`backend.registry \`${registry.file}\` must be a project-relative path inside a backend folder.`);
996
+ }
997
+ const options = {
998
+ registry: resolve3(project.root, registry.file),
999
+ services: resolve3(project.root, registry.unit.source, "services"),
1000
+ skip: configured.skip ?? (file === undefined ? presetRegistrySkip : [])
1001
+ };
1002
+ if (configured.object !== undefined) {
1003
+ options.object = configured.object;
1004
+ }
1005
+ overrides.push(presetOverride([registry.file], { "arch/backend-registry": ["error", options] }));
1006
+ }
1007
+ return overrides;
1008
+ };
1009
+
1010
+ // src/preset/groups/basics.ts
1011
+ var basicsGroup = (project, settings) => {
1012
+ const trivial = { mode: "precise", async: false, callees: [] };
1013
+ const { appFiles, safetyFiles, ui, web } = project;
1014
+ return [
1015
+ ...settings.trivial ? [
1016
+ presetOverride(appFiles, { "arch/code-trivial-functions": ["error", trivial] }, ui),
1017
+ presetOverride([...presetScopes(web, "services/**/*.{client,server,rsc}.ts"), ...presetScopes(web, "services/**/*.rsc.tsx")], { "arch/code-trivial-functions": ["error", { ...trivial, services: true }] }, ui)
1018
+ ] : [],
1019
+ ...settings.state ? [presetOverride(safetyFiles, { "arch/code-mutable-state": ["error", { kinds: ["let", "var"] }] }, ui)] : [],
1020
+ ...settings.switch ? [presetOverride(project.backendFiles, { "arch/code-switch": ["error", { message: "Use ts-pattern in backend application code." }] })] : [],
1021
+ ...settings.negation ? [presetOverride(safetyFiles, { "arch/code-double-negation": "error" }, ui)] : []
1022
+ ];
1023
+ };
1024
+
1025
+ // src/preset/groups/comments.ts
1026
+ var commentsGroup = (project, settings) => [
1027
+ presetOverride(project.appFiles, { "arch/comments-code": ["error", { why: false, allow: ["^\\s*SAFETY: "] }] }, project.ui),
1028
+ presetOverride(presetScopes(project.app, "**/*.{types,constants}.ts"), { "arch/comments-code": ["error", { why: false }] }),
1029
+ ...settings.directives ? [presetOverride([`**/${presetExtensions}`], { "arch/comments-directives": "error" })] : []
1030
+ ];
1031
+
1032
+ // src/preset/groups/console.ts
1033
+ var consoleGroup = () => [presetOverride([`**/${presetExtensions}`], { "no-console": "error" })];
1034
+
1035
+ // src/preset/groups/deny.ts
1036
+ var denyGroup = (project, settings) => {
1037
+ const { imports, calls } = settings;
1038
+ const overrides = [];
1039
+ if (Object.keys(imports).length > 0 || Object.keys(calls).length > 0) {
1040
+ overrides.push(presetOverride([`**/${presetExtensions}`], { "arch/boundaries-deny": ["error", { imports, calls }] }));
1041
+ }
1042
+ for (const unit of project.units) {
1043
+ const writes = unit.kind === "backend" ? unit.scope.effective.backend?.writes ?? [] : [];
1044
+ if (writes.length === 0) {
1045
+ continue;
1046
+ }
1047
+ const files = [
1048
+ ...presetScopes([unit.source], "services/**/*-query.*.ts"),
1049
+ ...presetScopes(project.operations.filter((path) => path === unit.source), "services/*/queries/*.ts")
1050
+ ];
1051
+ const reads = Object.fromEntries(writes.map((name) => [name, "Query files only read; move writes to an action."]));
1052
+ overrides.push(presetOverride(files, { "arch/boundaries-deny": ["error", { imports, calls: { ...reads, ...calls } }] }));
1053
+ }
1054
+ return overrides;
1055
+ };
1056
+
1057
+ // src/preset/groups/expo.ts
1058
+ var expoGroup = (project, settings) => {
1059
+ const { expoWeb, testFiles } = project;
1060
+ const server = settings.server;
1061
+ const overrides = [
1062
+ presetOverride(presetScopes(expoWeb, "app/**/*.tsx"), {
1063
+ "arch/files-route": [
1064
+ "error",
1065
+ { export: "default", exports: presetExpoRouteExports, hooks: ["useState", "useEffect", "useMutation"], intrinsic: false }
1066
+ ]
1067
+ }, testFiles),
1068
+ presetOverride(presetScopes(expoWeb, "app/**/*.tsx"), {
1069
+ "arch/naming-file": [
1070
+ "error",
1071
+ {
1072
+ pattern: "^(?:_layout|index|\\[(?:[.]{3})?[\\w.]+\\]|[+][a-z][\\w-]*|[a-z][a-z0-9-]*)(?:[.](?:web|native|ios|android))?[.]tsx$",
1073
+ message: "Expo Router files are kebab-case, _layout, index, [param], [...rest] or +special."
1074
+ }
1075
+ ],
1076
+ "arch/files-helpers": [
1077
+ "error",
1078
+ {
1079
+ allow: "^makeStyles$",
1080
+ message: "Route files hold screens and their components; move logic to a service or hook."
1081
+ }
1082
+ ]
1083
+ }, testFiles),
1084
+ presetOverride(presetScopes(expoWeb, "app/**/*+api.ts"), {
1085
+ "arch/files-route": ["error", { export: "GET", exports: ["HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], hooks: [], intrinsic: false }]
1086
+ })
1087
+ ];
1088
+ if (server.length > 0) {
1089
+ overrides.push(presetOverride(presetScopes(expoWeb), {
1090
+ "no-restricted-imports": ["error", { paths: server, patterns: project.baseImports }]
1091
+ }));
1092
+ }
1093
+ return overrides;
1094
+ };
1095
+
1096
+ // src/preset/groups/externals.ts
1097
+ var externalsGroup = (_project, settings) => [
1098
+ presetOverride(["**/{vite,next}.config.{ts,mts,cts,js,mjs,cjs}"], { "arch/files-externals": ["error", { packages: settings.packages }] })
1099
+ ];
1100
+
1101
+ // src/preset/groups/forms.ts
1102
+ var formsGroup = (project, settings) => [
1103
+ presetOverride(project.reactFiles, { "arch/code-paired-call": ["error", { when: "useForm", require: settings.resolver }] }, project.ui)
1104
+ ];
1105
+
1106
+ // src/preset/groups/imports.ts
1107
+ import { resolve as resolve4 } from "node:path";
1108
+ function importsPackageNames(project, kinds) {
1109
+ return project.units.flatMap((unit) => {
1110
+ const name = kinds.includes(unit.kind) ? presetPackageName(project.root, unit.key) : undefined;
1111
+ return name ? [name] : [];
1112
+ });
1113
+ }
1114
+ var importsGroup = (project, settings) => {
1115
+ const rules = {
1116
+ ...presetImportRules,
1117
+ "arch/types-namespace-import": ["error", { max: 3 }],
1118
+ "no-restricted-imports": ["error", { patterns: project.baseImports }],
1119
+ "import-extra/no-relative-packages": "error",
1120
+ "perfectionist/sort-imports": [
1121
+ "error",
1122
+ {
1123
+ type: "natural",
1124
+ ignoreCase: true,
1125
+ newlinesBetween: 0,
1126
+ environment: "bun",
1127
+ internalPattern: settings.internal ?? [
1128
+ "^@/.*",
1129
+ "^#.*",
1130
+ "^~icons/.*",
1131
+ ...project.publicEntrypoints.map((path) => `^${path.split("/").slice(0, 2).join("/")}/`)
1132
+ ],
1133
+ groups: [["builtin", "external"], "internal", ["parent", "sibling", "index"], "type"]
1134
+ }
1135
+ ]
1136
+ };
1137
+ if (!settings.sort) {
1138
+ delete rules["perfectionist/sort-imports"];
1139
+ delete rules["perfectionist/sort-named-imports"];
1140
+ }
1141
+ const overrides = [presetOverride([`**/${presetExtensions}`], rules)];
1142
+ if (!settings.boundaries) {
1143
+ return overrides;
1144
+ }
1145
+ const absolute = (paths) => paths.map((path) => resolve4(project.root, path));
1146
+ overrides.push(presetOverride([...project.appFiles, ...presetScopes(project.packages, `**/${presetExtensions}`)], {
1147
+ "arch/boundaries-imports": [
1148
+ "error",
1149
+ {
1150
+ web: absolute(project.web),
1151
+ backend: absolute(project.backend),
1152
+ packages: absolute(project.packages),
1153
+ roles: project.suffixes,
1154
+ portable: true,
1155
+ aliases: presetAliases(project.root, settings.aliases),
1156
+ appPackages: importsPackageNames(project, ["web", "backend"]),
1157
+ backendPackages: importsPackageNames(project, ["backend"]),
1158
+ entrypoints: project.publicEntrypoints
1159
+ }
1160
+ ]
1161
+ }));
1162
+ return overrides;
1163
+ };
1164
+
1165
+ // src/preset/groups/layout.ts
1166
+ import { posix as posix3 } from "node:path";
1167
+ var layoutGroup = (project, settings) => {
1168
+ const { web, nextWeb, serviceRoots, app, services, utilities, ui, testFiles, layouts, operations } = project;
1169
+ const folderOperations = presetScopes(operations, "services/*/{actions,queries}/*.ts");
1170
+ const operationMessage = "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.";
1171
+ const roles = [...presetBuiltinFileRoles.filter((role) => role !== "action" && role !== "query"), ...project.suffixes];
1172
+ const conceptFiles = project.suffixes.map((suffix) => `**/*.${suffix}.ts`);
1173
+ const frontendEntries = [...presetScopes(web, "services/**/*.{client,server,rsc}.ts"), ...presetScopes(web, "services/**/*.rsc.tsx")];
1174
+ const ungroupedWeb = web.filter((path) => !layouts.some((scope) => scope.path === posix3.join(path, "components")));
1175
+ return [
1176
+ ...layouts.flatMap(({ path, folder, mode }) => {
1177
+ const folders = folder === "services" && operations.some((source) => posix3.join(source, "services") === path);
1178
+ const registries = folder === "services" ? project.registries : [];
1179
+ const restricted = presetOverride(mode === "flat" ? [`${path}/*/**/${presetExtensions}`] : [`${path}/${presetExtensions}`, `${path}/*/*/**/${presetExtensions}`], {
1180
+ "arch/files-restricted": [
1181
+ "error",
1182
+ {
1183
+ 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."
1184
+ }
1185
+ ]
1186
+ }, [...folders ? [`${path}/*/{actions,queries}/*.ts`] : [], ...registries]);
1187
+ if (mode !== "domain") {
1188
+ return [restricted];
1189
+ }
1190
+ const prefix = { singularize: folder === "components" ? "trailing-s" : "none", separators: [".", "-"] };
1191
+ const naming = folder === "hooks" ? { ...prefix, prefixes: ["use-"] } : prefix;
1192
+ const domain = [restricted, presetOverride([`${path}/*/${presetExtensions}`], { "arch/naming-folder": ["error", naming] }, registries)];
1193
+ if (!settings.strict || folder !== "services") {
1194
+ return domain;
1195
+ }
1196
+ const strict = presetOverride([`${path}/*/**/${presetExtensions}`], {
1197
+ "arch/naming-file": [
1198
+ "error",
1199
+ {
1200
+ pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*(?:[.](?:${roles.join("|")})[.]ts|[.]rsc[.]tsx)$`,
1201
+ message: `Domain files use domain[-part].<role>.ts with a role from: ${roles.join(", ")}. Add new roles to layout.suffixes.`
1202
+ }
1203
+ ]
1204
+ }, ["**/*-{action,query}.*.ts", ...testFiles, ...registries]);
1205
+ return [...domain, strict];
1206
+ }),
1207
+ presetOverride(presetScopes(ungroupedWeb, `components/*/*/**/${presetExtensions}`), { "arch/files-restricted": ["error", { message: "Components allow one group folder." }] }, ui),
1208
+ presetOverride([...presetScopes(web, "components/**/*.ts"), ...presetScopes(web, "hooks/**/*.tsx")], { "arch/files-restricted": ["error", { message: "Components use TSX; hooks use .ts files." }] }, ui),
1209
+ presetOverride(presetScopes(web, "**/use-*.{ts,tsx}"), { "arch/files-restricted": ["error", { message: "Hooks belong in src/hooks." }] }, [...presetScopes(web, "hooks/**"), ...ui]),
1210
+ presetOverride(presetScopes(web, "services/**/*.{ts,tsx}"), {
1211
+ "arch/files-restricted": [
1212
+ "error",
1213
+ {
1214
+ message: "Frontend services use .client.ts, .server.ts or .rsc.{ts,tsx}; shared domain helpers use .utils.ts and values use .constants.ts."
1215
+ }
1216
+ ]
1217
+ }, [
1218
+ "**/*.{client,server,rsc,utils,constants}.ts",
1219
+ "**/*.rsc.tsx",
1220
+ ...presetScopes(nextWeb, "services/**/*.action.ts"),
1221
+ ...conceptFiles,
1222
+ ...project.colocated,
1223
+ ...testFiles
1224
+ ]),
1225
+ presetOverride(services, {
1226
+ "arch/backend-service-functions": "error",
1227
+ "arch/types-inline": ["error", { parameters: true, returns: true, functions: true, min: 1 }]
1228
+ }, [...utilities, ...testFiles, ...conceptFiles]),
1229
+ presetOverride(frontendEntries, { "arch/backend-service-functions": ["error", { frontend: true }] }, testFiles),
1230
+ ...conceptFiles.map((pattern) => presetOverride(presetScopes(app, `services/${pattern}`), {
1231
+ "arch/backend-service-functions": "off",
1232
+ "arch/naming-exports": ["error", { stem: "full-basename", scope: "functions" }]
1233
+ }, testFiles)),
1234
+ presetOverride([...presetScopes(app, "services/**/*-{action,query}.*.ts"), ...presetScopes(web, "services/**/*-{action,query}.*.rsc.tsx")], { "arch/backend-service-functions": ["error", { helpers: true, single: true, message: operationMessage }] }, testFiles),
1235
+ presetOverride(folderOperations, { "arch/backend-service-functions": ["error", { helpers: true, single: true, message: operationMessage }] }, testFiles),
1236
+ presetOverride(presetScopes(serviceRoots, "services/**/*.ts"), { "arch/files-domain-constants": "error" }, [...utilities, ...testFiles]),
1237
+ presetOverride(presetScopes(web, "services/**/*.{ts,tsx}"), { "arch/files-domain-constants": ["error", { data: true }] }, [
1238
+ "**/*.constants.ts",
1239
+ ...testFiles
1240
+ ]),
1241
+ presetOverride(frontendEntries, { "arch/files-domain-constants": ["error", { data: true, services: true }] }, testFiles),
1242
+ presetOverride([...presetScopes(serviceRoots, "services/**/*-{action,query}.*.ts"), ...folderOperations, ...frontendEntries], { "arch/code-object-params": ["error", { max: 2 }] }, testFiles),
1243
+ presetOverride(frontendEntries, { "arch/code-object-params": ["error", { max: 2, services: true }] }, testFiles),
1244
+ presetOverride(presetScopes(serviceRoots, "services/**/*.service.ts"), { "arch/files-exports": ["error", { names: settings.service, stem: "before-first-dot", types: false }] }, testFiles),
1245
+ presetOverride(presetScopes(app.filter((path) => !operations.includes(path)), "services/**/{actions,queries}/**/*.ts"), {
1246
+ "arch/files-restricted": ["error", { message: "Use domain-action.name.ts or domain-query.name.ts." }]
1247
+ })
1248
+ ];
1249
+ };
1250
+
1251
+ // src/preset/groups/limits.ts
1252
+ var limitsGroup = (project, settings) => [
1253
+ presetOverride(project.safetyFiles, {
1254
+ "max-lines": ["error", { max: settings.lines, skipBlankLines: true, skipComments: true }],
1255
+ complexity: ["error", { max: settings.complexity }]
1256
+ }, project.ui),
1257
+ presetOverride(presetScopes(project.app, "**/*.types.ts"), { "max-lines": ["error", { max: settings.types, skipBlankLines: true, skipComments: true }] }, project.ui)
1258
+ ];
1259
+
1260
+ // src/preset/groups/naming.ts
1261
+ var namingOperationMessage = "Use domain-action.name.ts or domain-query.name.ts.";
1262
+ var namingFrontendPrefix = { ...presetExportPrefix, scope: "functions", services: true, allow: "^\\*$" };
1263
+ function namingOperationPrefix(role) {
1264
+ return { ...presetExportPrefix, roles: [role], scope: "functions", allow: "^\\*$" };
1265
+ }
1266
+ function namingOperation(role, file) {
1267
+ return { mode: "template", file, export: `{domain}${role === "action" ? "Action" : "Query"}{Name}`, placeholder: "[a-z0-9-]+", camelCase: true };
1268
+ }
1269
+ var namingGroup = (project, settings, owner) => {
1270
+ const { web, serviceRoots, app, ui, testFiles, components, hooks, operations } = project;
1271
+ const flatApp = app.filter((path) => !operations.includes(path));
1272
+ const folderApp = app.filter((path) => operations.includes(path));
1273
+ const frontendEntries = [...presetScopes(web, "services/**/*.{client,server,rsc}.ts"), ...presetScopes(web, "services/**/*.rsc.tsx")];
1274
+ const ungroupedWeb = web.filter((path) => !project.layouts.some((scope) => scope.path === `${path}/components`));
1275
+ const overrides = [];
1276
+ const { files: filesPattern, exports: exportsPattern, unique } = settings;
1277
+ for (const [key, source] of [["files", filesPattern], ["exports", exportsPattern], ["unique", unique]]) {
1278
+ try {
1279
+ RegExp(typeof source === "string" ? source : "");
1280
+ } catch {
1281
+ throw new PresetError(`naming.${String(key)} must be a valid regular expression.`);
1282
+ }
1283
+ }
1284
+ const folderChecks = [];
1285
+ if (typeof filesPattern === "string" || unique !== undefined) {
1286
+ const check = { pattern: typeof filesPattern === "string" ? filesPattern : "", message: `File names here must match ${String(filesPattern)}.` };
1287
+ if (unique !== undefined) {
1288
+ check.unique = unique;
1289
+ }
1290
+ folderChecks.push(check);
1291
+ overrides.push(presetOverride([`**/${presetExtensions}`], { "arch/naming-file": ["error", check] }));
1292
+ for (const override of owner.effective.layout ? layoutGroup(project, owner.effective.layout, owner) : []) {
1293
+ const layoutRule = override.rules?.["arch/naming-file"];
1294
+ if (Array.isArray(layoutRule)) {
1295
+ overrides.push({ ...override, rules: { "arch/naming-file": [...layoutRule, check] } });
1296
+ }
1297
+ }
1298
+ }
1299
+ const add = (kind, files, rules, excludeFiles) => {
1300
+ const fileRule = rules["arch/naming-file"];
1301
+ if (Array.isArray(fileRule) && folderChecks.length > 0) {
1302
+ rules["arch/naming-file"] = [...fileRule, ...folderChecks];
1303
+ }
1304
+ if (settings[kind]) {
1305
+ overrides.push(presetOverride(files, rules, excludeFiles));
1306
+ }
1307
+ };
1308
+ add("files", [`**/${presetExtensions}`], presetNamingRules);
1309
+ add("files", presetScopes(app, "types/**/*.ts"), { "arch/naming-file": ["error", { pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*[.]types[.]ts$", message: "Use domain.types.ts filenames." }] }, ["**/*.constants.ts"]);
1310
+ add("exports", presetScopes(app, "**/*.{constants,utils,handler,service,types,client,server,rsc}.ts"), { "arch/naming-exports": ["error", presetExportPrefix] }, [...ui, ...presetScopes(serviceRoots, "entry.*.ts")]);
1311
+ add("exports", frontendEntries, { "arch/naming-exports": ["error", namingFrontendPrefix] }, testFiles);
1312
+ add("exports", presetScopes(web, "services/**/*.constants.ts"), { "arch/naming-exports": ["error", { ...presetExportPrefix, scope: "declarations", allow: "^\\*$" }] }, testFiles);
1313
+ add("exports", presetScopes(app, "**/*.utils.ts"), { "arch/naming-exports": ["error", { ...presetExportPrefix, scope: "functions", allow: "^\\*$" }] }, ui);
1314
+ add("files", hooks, {
1315
+ "arch/naming-file": [
1316
+ "error",
1317
+ {
1318
+ pattern: "^use-[a-z0-9]+(?:-[a-z0-9]+)*(?:[.](?:web|native|ios|android))?[.]ts$",
1319
+ message: "Hooks use use-domain.ts filenames, with an optional .web, .native, .ios or .android platform suffix."
1320
+ }
1321
+ ]
1322
+ }, ui);
1323
+ add("exports", hooks, { "arch/naming-exports": ["error", {}, { mode: "pattern", pattern: "^use[A-Z]", types: true }] }, ui);
1324
+ add("exports", components, { "arch/files-components-only": ["error", { filename: true, types: true, roles: ["illustration"] }] }, [...ui, ...testFiles]);
1325
+ add("folders", presetScopes(ungroupedWeb, "components/**/*.tsx"), { "arch/naming-folder": ["error", { singularize: "trailing-s", separators: ["-"] }] }, [...ui, ...testFiles]);
1326
+ for (const role of ["action", "query"]) {
1327
+ const files = [...presetScopes(flatApp, `services/**/*-${role}.*.ts`), ...presetScopes(web, `services/**/*-${role}.*.rsc.tsx`)];
1328
+ add("files", files, {
1329
+ "arch/naming-file": ["error", { pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*-${role}\\.[a-z0-9]+(?:-[a-z0-9]+)*\\.ts$`, message: namingOperationMessage }]
1330
+ }, testFiles);
1331
+ add("exports", files, {
1332
+ "arch/naming-exports": [
1333
+ "error",
1334
+ namingOperationPrefix(role),
1335
+ namingOperation(role, `{domain}-${role}.{name}.ts`)
1336
+ ]
1337
+ }, testFiles);
1338
+ }
1339
+ for (const [role, folder, separator] of [["action", "actions", "."], ["query", "queries", "-"]]) {
1340
+ const files = presetScopes(folderApp, `services/*/${folder}/*.ts`);
1341
+ add("files", files, {
1342
+ "arch/naming-file": [
1343
+ "error",
1344
+ {
1345
+ pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*-${role}[${separator}][a-z0-9]+(?:-[a-z0-9]+)*[.]ts$`,
1346
+ message: "Use domain-action.name.ts in actions/ and domain-query-name.ts in queries/."
1347
+ }
1348
+ ]
1349
+ }, testFiles);
1350
+ add("exports", files, {
1351
+ "arch/naming-exports": [
1352
+ "error",
1353
+ { mode: "template", file: `{domain}-${role}${separator}{name}.ts`, export: `make{Domain}${role === "action" ? "Action" : "Query"}{Name}`, placeholder: "[a-z0-9-]+" }
1354
+ ]
1355
+ }, testFiles);
1356
+ }
1357
+ for (const role of ["action", "query"]) {
1358
+ for (const suffix of ["client.ts", "server.ts", "rsc.ts", "rsc.tsx"]) {
1359
+ const files = presetScopes(web, `services/**/*-${role}.*.${suffix}`);
1360
+ add("files", files, {
1361
+ "arch/naming-file": [
1362
+ "error",
1363
+ {
1364
+ pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*-${role}\\.[a-z0-9]+(?:-[a-z0-9]+)*\\.${suffix.replace(".", "\\.")}$`,
1365
+ message: "Frontend operations retain a client, server or rsc runtime suffix."
1366
+ }
1367
+ ]
1368
+ }, testFiles);
1369
+ add("exports", files, { "arch/naming-exports": ["error", namingOperationPrefix(role), namingOperation(role, `{domain}-${role}.{name}.${suffix}`)] }, testFiles);
1370
+ }
1371
+ }
1372
+ add("exports", presetScopes(web, "services/**/*.rsc.{ts,tsx}"), { "arch/naming-exports": ["error", namingFrontendPrefix, { mode: "pattern", pattern: "^[a-z][a-z0-9]*Rsc[A-Z][a-zA-Z0-9]*$" }] }, ["**/*-{action,query}.*.rsc.{ts,tsx}", ...testFiles]);
1373
+ if (typeof exportsPattern === "string") {
1374
+ overrides.push(presetOverride([`**/${presetExtensions}`], { "arch/naming-exports": ["error", { mode: "pattern", pattern: exportsPattern, types: true }] }));
1375
+ }
1376
+ return overrides;
1377
+ };
1378
+
1379
+ // src/preset/groups/next.ts
1380
+ var nextGroup = (project, settings) => {
1381
+ const { nextWeb, testFiles } = project;
1382
+ const hooks = ["useState", "useEffect", "useMutation"];
1383
+ const page = { export: "default", exports: presetNextSegmentExports, hooks, inspect: true };
1384
+ return [
1385
+ presetOverride(presetScopes(nextWeb, "app/**/{page,template,default,not-found,forbidden,unauthorized,loading}.tsx"), { "arch/files-route": ["error", { ...page, intrinsic: true }] }, [...testFiles, ...presetScopes(nextWeb, "app/not-found.tsx")]),
1386
+ presetOverride([...presetScopes(nextWeb, "app/**/{layout,global-not-found}.tsx"), ...presetScopes(nextWeb, "app/not-found.tsx")], { "arch/files-route": ["error", { ...page, intrinsic: false }] }, testFiles),
1387
+ presetOverride(presetScopes(nextWeb, "app/**/route.ts"), {
1388
+ "arch/files-route": [
1389
+ "error",
1390
+ { export: "GET", exports: [...presetNextHandlerMethods.slice(1), ...presetNextSegmentConfig], hooks: [], intrinsic: false }
1391
+ ],
1392
+ "arch/files-helpers": [
1393
+ "error",
1394
+ {
1395
+ components: false,
1396
+ allow: `^(${[...presetNextHandlerMethods, ...presetNextSegmentConfig].join("|")})$`,
1397
+ 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."
1398
+ }
1399
+ ]
1400
+ }),
1401
+ ...presetRuntime(project, settings, "next", "arch/boundaries-next")
1402
+ ];
1403
+ };
1404
+
1405
+ // src/preset/groups/orpc.ts
1406
+ var orpcGroup = (project) => project.orpc.public.length > 0 ? [
1407
+ presetOverride(project.orpc.public, {
1408
+ "arch/backend-orpc-output": ["error", { handler: "handler", output: "output", composers: project.orpc.composers }]
1409
+ })
1410
+ ] : [];
1411
+
1412
+ // src/preset/groups/react.ts
1413
+ var reactGroup = (project, settings) => {
1414
+ const { reactFiles, web, ui, testFiles, components } = project;
1415
+ const memo = settings.memo ?? settings.compiler;
1416
+ const perf = settings.perf ?? !settings.compiler;
1417
+ const rules = { ...presetReactRules, "arch/layout-jsx-attributes": ["error", { min: 3 }] };
1418
+ if (perf) {
1419
+ for (const rule of ["new-object", "new-array", "new-function", "jsx"]) {
1420
+ rules[`react-perf/jsx-no-${rule}-as-prop`] = "error";
1421
+ }
1422
+ }
1423
+ const overrides = [
1424
+ presetOverride(reactFiles, rules, ui),
1425
+ presetOverride(components, { "arch/files-helpers": ["error", { components: true, hooks: "^use[A-Z]" }] }, [...ui, ...testFiles])
1426
+ ];
1427
+ if (settings.effects) {
1428
+ overrides.push(presetOverride(reactFiles, { ...presetEffectsRules, "arch/react-empty-effect": "error" }, ui));
1429
+ }
1430
+ if (memo) {
1431
+ overrides.push(presetOverride(reactFiles, { "arch/react-memo": "error" }, ui));
1432
+ }
1433
+ if (web.length > 0) {
1434
+ overrides.push(presetOverride(project.webFiles, {
1435
+ "arch/boundaries-token-owner": [
1436
+ "error",
1437
+ {
1438
+ restrictions: [
1439
+ { token: "RouterClient", allow: [...project.rpcClients] },
1440
+ { token: "createContext", patterns: [...presetScopes(web, "context/"), ...project.webUiRoots.map((path) => `${path}/`)] }
1441
+ ]
1442
+ }
1443
+ ]
1444
+ }));
1445
+ }
1446
+ if (settings.props) {
1447
+ overrides.push(presetOverride(components, { "arch/react-props": ["error", { prefix: true }] }, [...ui, ...testFiles]));
1448
+ }
1449
+ if (!settings.a11y) {
1450
+ overrides.push(presetOverride(["**/*.{tsx,jsx}"], Object.fromEntries(presetA11yRules.map((rule) => [rule, "off"]))));
1451
+ }
1452
+ return overrides;
1453
+ };
1454
+
1455
+ // src/preset/groups/schemas.ts
1456
+ var schemasGroup = (project) => {
1457
+ const { backend, nextWeb, packageSources, ui } = project;
1458
+ const owners = backend.length > 0 ? backend : nextWeb;
1459
+ return [
1460
+ presetOverride(project.appFiles, { "arch/schemas-local": ["error", { packages: ["zod"], namespaces: ["z"] }] }, [...presetScopes([...owners, ...packageSources], "types/**"), ...project.colocated, ...ui]),
1461
+ presetOverride([...project.webFiles, ...presetScopes(packageSources)], {
1462
+ "arch/schemas-rederive": ["error", { from: project.publicEntrypoints, namespaces: ["z"], operators: ["infer", "input"] }]
1463
+ })
1464
+ ];
1465
+ };
1466
+
1467
+ // src/preset/groups/shadcn.ts
1468
+ var shadcnGroup = (project) => {
1469
+ const { web, webFiles, ui, webUiRoots } = project;
1470
+ const allow = ["layout", "opacity", "truncate"];
1471
+ const padding = ["p", "px", "py", "ps", "pe", "pt", "pr", "pb", "pl"];
1472
+ const gap = ["gap", "gap-x", "gap-y"];
1473
+ const safeArea = padding.flatMap((group) => ["top", "right", "bottom", "left"].flatMap((side) => [
1474
+ `${group}-[env(safe-area-inset-${side})]`,
1475
+ ...group === "p" ? [] : [`${group}-[max(--spacing(*),env(safe-area-inset-${side}))]`]
1476
+ ]));
1477
+ const restyle = (base) => ({
1478
+ allow: base,
1479
+ contracts: [
1480
+ { pattern: "^(CardContent|PopoverContent)$", allow: [...base, ...padding] },
1481
+ { pattern: "^TabsContent$", allow: [...base, ...gap, ...padding] },
1482
+ { pattern: "^(Tabs|HoverCardContent|SidebarHeader|BreadcrumbList)$", allow: [...base, ...gap] }
1483
+ ]
1484
+ });
1485
+ if (Object.keys(project.css).length === 0) {
1486
+ return [];
1487
+ }
1488
+ return [
1489
+ presetOverride(webFiles, {
1490
+ "shadcn/no-restyle": ["error", restyle(allow)],
1491
+ "shadcn/no-arbitrary-values": ["error", { allow: ["layout", ...safeArea] }],
1492
+ "shadcn/require-static-classes": "error",
1493
+ "arch/react-dynamic-classes": "error"
1494
+ }, ui),
1495
+ presetOverride(presetScopes(web, "components/**/*.illustration.tsx"), {
1496
+ "shadcn/no-restyle": ["error", restyle([...allow, ...presetIllustrationPalette])],
1497
+ "shadcn/no-arbitrary-values": ["error", { allow: ["layout", ...safeArea, ...presetIllustrationPalette] }]
1498
+ }, ui),
1499
+ presetOverride(webFiles, { "shadcn/no-unknown-classes": ["error", { allow: [] }] }),
1500
+ presetOverride(presetScopes(webUiRoots, "sonner.tsx"), { "shadcn/no-unknown-classes": ["error", { allow: ["toaster", "toast"] }] })
1501
+ ];
1502
+ };
1503
+
1504
+ // src/preset/groups/sql.ts
1505
+ var sqlGroup = (project, settings) => [
1506
+ presetOverride(presetScopes(project.serviceRoots, "services/**/*.ts"), {
1507
+ "arch/backend-sql-like": [
1508
+ "error",
1509
+ { methods: ["like", "ilike"], operators: ["where"], sanitizers: settings.sanitizers, bindings: true }
1510
+ ]
1511
+ })
1512
+ ];
1513
+
1514
+ // src/preset/groups/style.ts
1515
+ var styleGroup = (_project, settings) => {
1516
+ const files = [`**/${presetExtensions}`];
1517
+ const layout = {};
1518
+ if (settings.padding) {
1519
+ layout["arch/layout-padding"] = ["error", { multiline: true }];
1520
+ }
1521
+ if (settings.multiline) {
1522
+ layout["arch/layout-object"] = ["error", { min: 3, scope: "call-args" }];
1523
+ layout["arch/layout-call-array"] = ["error", { callees: ["Promise.all"], min: 2 }];
1524
+ layout["arch/layout-key-value"] = "error";
1525
+ }
1526
+ if (settings.dashes) {
1527
+ layout["arch/copy-dashes"] = "error";
1528
+ }
1529
+ return [
1530
+ presetOverride(files, {
1531
+ ...presetFormattingRules,
1532
+ "eslint-js/semi": ["error", settings.semicolons ? "always" : "never"],
1533
+ "eslint-js/quotes": ["error", settings.quotes, { avoidEscape: true }]
1534
+ }),
1535
+ presetOverride(files, layout)
1536
+ ];
1537
+ };
1538
+
1539
+ // src/preset/groups/tailwind.ts
1540
+ var tailwindGroup = (project) => Object.entries(project.css).map(([path, entryPoint]) => presetOverride(presetScopes([path]), Object.fromEntries(Object.entries(presetTailwindRules).map(([rule, severity]) => [
1541
+ rule,
1542
+ rule === "tailwindcss/no-duplicate-classes" || rule === "tailwindcss/no-unnecessary-whitespace" ? severity : [severity, { entryPoint }]
1543
+ ]))));
1544
+
1545
+ // src/preset/groups/tanstack.ts
1546
+ var tanstackGroup = (project, settings) => {
1547
+ const { tanstackWeb, testFiles } = project;
1548
+ return [
1549
+ presetOverride(presetScopes(tanstackWeb, "routes/**/*.{ts,tsx}"), { "arch/files-route": ["error", { export: "Route", hooks: ["useState", "useEffect", "useMutation"], intrinsic: true }] }, testFiles),
1550
+ presetOverride(presetScopes(tanstackWeb, "routes/handlers/**/*.ts"), {
1551
+ "arch/files-helpers": [
1552
+ "error",
1553
+ {
1554
+ components: false,
1555
+ 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."
1556
+ }
1557
+ ]
1558
+ }),
1559
+ presetOverride(presetScopes(tanstackWeb), {
1560
+ "no-restricted-imports": [
1561
+ "error",
1562
+ {
1563
+ paths: [{ name: "@tanstack/react-start/rsc", message: "Render server components only in services/*.rsc.tsx files." }],
1564
+ patterns: project.baseImports
1565
+ }
1566
+ ]
1567
+ }, presetScopes(tanstackWeb, "services/**/*.rsc.{ts,tsx}")),
1568
+ ...presetRuntime(project, settings, "tanstack", "arch/boundaries-tanstack")
1569
+ ];
1570
+ };
1571
+
1572
+ // src/utils/options.utils.ts
1573
+ class OptionsError extends Error {
1574
+ }
1575
+ var optionsPatternSchema = {
1576
+ anyOf: [{ type: "string" }, { type: "array", items: { type: "string" }, minItems: 1 }]
1577
+ };
1578
+ function optionsFirst(context, fallback) {
1579
+ return context.options[0] ?? fallback;
1580
+ }
1581
+ function optionsAll(context) {
1582
+ return context.options;
1583
+ }
1584
+ function optionsPatterns(pattern, flags) {
1585
+ return optionsPatternsWithEmpty(pattern === undefined ? undefined : [pattern].flat().filter((source) => source !== ""), flags);
1586
+ }
1587
+ function optionsPatternsWithEmpty(pattern, flags) {
1588
+ return pattern === undefined ? [] : [pattern].flat().map((source) => new RegExp(source, flags));
1589
+ }
1590
+ function optionsPatternsTest(patterns, value) {
1591
+ return patterns.some((pattern) => {
1592
+ pattern.lastIndex = 0;
1593
+ return pattern.test(value);
1594
+ });
1595
+ }
1596
+ function optionsGlob(pattern) {
1597
+ const negated = [];
1598
+ const source = pattern.replaceAll(/!\(([^()]*)\)/g, (_, options) => {
1599
+ negated.push(options.split("|").map((option) => option.replaceAll(/[.+^$()|[\]\\*?{}]/g, "\\$&")).join("|"));
1600
+ return `\x02${negated.length - 1}\x03`;
1601
+ }).replaceAll(/[.+^$()|[\]\\]/g, "\\$&").replaceAll("?", "[^/]").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*").replaceAll(/\{([^}]*)\}/g, (_, options) => `(?:${options.split(",").join("|")})`);
1602
+ return new RegExp(`^${negated.reduce((text, names, index) => text.replace(`\x02${index}\x03`, `(?!(?:${names})(?:/|$))[^/]*`), source)}$`);
1603
+ }
1604
+ function optionsPatternLabel(pattern) {
1605
+ return pattern === undefined ? "" : [pattern].flat().filter((source) => source !== "").join(" | ");
1606
+ }
1607
+ function optionsAssertMockingApis(patterns) {
1608
+ if (patterns.some((pattern) => !/^(?:\*|(?:vi|jest|\*)\.(?:[A-Za-z_$][\w$]*|\*))$/.test(pattern))) {
1609
+ throw new OptionsError("Test mocking APIs must be `*` or `<vi|jest|*>.<method|*>`.");
1610
+ }
1611
+ }
1612
+
1613
+ // src/preset/groups/tests.ts
1614
+ var testsGroup = (project, settings) => {
1615
+ const { functions, assertions } = settings;
1616
+ const testFunctions = ["test", "it", ...functions];
1617
+ if (testFunctions.some((name) => !/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(name))) {
1618
+ throw new PresetError("Test functions must be literal identifiers or dotted names.");
1619
+ }
1620
+ if (assertions.some((name) => !/^[A-Za-z_$][\w$]*(?:\.[\w$*]+)*$/.test(name))) {
1621
+ throw new PresetError("Assertion functions must be rooted dotted names or patterns.");
1622
+ }
1623
+ const configured = settings.mocking;
1624
+ const mocking = typeof configured === "string" ? { preset: configured } : configured;
1625
+ const preset = mocking.preset ?? "modules";
1626
+ if (!["modules", "strict"].includes(preset)) {
1627
+ throw new PresetError("Unknown test mocking preset.");
1628
+ }
1629
+ const deny = [...mocking.deny ?? [], ...presetTestMockingApis[preset]].map((entry) => typeof entry === "string" && preset === "strict" ? { api: entry, message: presetStrictMockingMessage } : entry);
1630
+ const allow = [...presetTestMockingAllow[preset], ...mocking.allow ?? []].toSorted((left, right) => Number(left.apis.includes("*")) - Number(right.apis.includes("*")));
1631
+ optionsAssertMockingApis([...deny.map((entry) => typeof entry === "string" ? entry : entry.api), ...allow.flatMap((entry) => entry.apis)]);
1632
+ const customized = preset !== "modules" || (mocking.deny ?? []).length > 0;
1633
+ const mockingRule = (apis) => ["error", { deny, allow: ["*.fn", ...apis], checkDestructured: customized }];
1634
+ const backendRules = { "arch/tests-concurrent-db": "error" };
1635
+ if (settings.fixtures) {
1636
+ backendRules["no-restricted-imports"] = [
1637
+ "error",
1638
+ { patterns: [...project.baseImports, { group: ["**/helpers/**"], message: "Use named fixtures or tests/support." }] }
1639
+ ];
1640
+ }
1641
+ const rules = {
1642
+ ...presetTestRules,
1643
+ "dillon-anti-slop/no-module-mocking": customized ? mockingRule([]) : "error",
1644
+ "arch/tests-modifiers": ["error", { functions }],
1645
+ "arch/tests-assertions": ["error", { functions, assertions }]
1646
+ };
1647
+ if (settings.titles) {
1648
+ rules["arch/tests-titles"] = ["error", { callees: ["describe", ...testFunctions], deny: "^should\\b", flags: "i" }];
1649
+ }
1650
+ return [
1651
+ presetOverride(project.testFiles, rules),
1652
+ presetOverride(presetScopes(project.units.map((unit) => unit.source), "**/*.{test,spec}.{ts,tsx,mts,cts}"), { "arch/files-restricted": ["error", { message: "Tests live in a tests/ folder, not next to the code." }] }, ["**/tests/**", "**/test/**"]),
1653
+ presetOverride(presetScopes(project.backendRoots, "tests/**/*.ts"), backendRules),
1654
+ ...allow.map((entry) => presetOverride(entry.files, { "dillon-anti-slop/no-module-mocking": entry.apis.includes("*") ? "off" : mockingRule(entry.apis) }))
1655
+ ];
1656
+ };
1657
+
1658
+ // src/preset/groups/types.ts
1659
+ var typesGroup = (project, settings) => {
1660
+ const { web, backend, packageSources, app, appFiles, services, utilities, types, ui, testFiles, components, hooks } = project;
1661
+ const schemaOwners = backend.length > 0 ? backend : project.nextWeb;
1662
+ const runtimeFreeTypes = backend.length > 0 ? web : [...project.tanstackWeb, ...project.expoWeb];
1663
+ const colocated = project.colocated;
1664
+ const overrides = [];
1665
+ if (settings.placement) {
1666
+ overrides.push(presetOverride(appFiles, { "arch/types-declarations": "error" }, [
1667
+ ...types,
1668
+ ...colocated,
1669
+ ...components,
1670
+ ...hooks,
1671
+ ...services,
1672
+ ...ui,
1673
+ ...testFiles
1674
+ ]), presetOverride(services, { "arch/types-service": "error" }, [...utilities, ...testFiles]), presetOverride([...components, ...hooks], {
1675
+ "arch/types-local-alias": "error",
1676
+ "arch/naming-types": "error",
1677
+ "arch/naming-declarations": [
1678
+ "error",
1679
+ { kinds: ["interface"], stem: "before-first-dot", normalize: "remove-separators" }
1680
+ ]
1681
+ }, [...ui, ...testFiles]), presetOverride(presetScopes(app, "**/*.types.ts"), { "arch/files-restricted": ["error", { message: "Domain types belong in src/types." }] }, [...types, ...colocated, ...ui]), presetOverride(presetScopes(app, "**/*.{utils,constants}.ts"), { "arch/types-declarations": "error" }, [...ui, ...testFiles]), presetOverride([...presetScopes([...schemaOwners, ...packageSources], "types/**/*.types.ts"), ...colocated, ...presetScopes(app, "**/*.constants.ts")], { "arch/files-no-functions": ["error", { reexports: false }] }), ...runtimeFreeTypes.length > 0 ? [
1682
+ presetOverride(presetScopes(runtimeFreeTypes, "types/**/*.types.ts"), {
1683
+ "arch/files-types-only": ["error", { imports: "ban", sources: [], reexports: true }]
1684
+ })
1685
+ ] : []);
1686
+ }
1687
+ if (settings.safety) {
1688
+ overrides.push(presetOverride(project.safetyFiles, {
1689
+ "typescript/no-explicit-any": "error",
1690
+ "typescript/ban-ts-comment": ["error", { "ts-ignore": true, "ts-nocheck": true, "ts-expect-error": true, "ts-check": false }],
1691
+ "arch/types-unknown": "error",
1692
+ "arch/types-imported-alias": "error",
1693
+ "arch/code-literal-in": "error",
1694
+ "arch/code-promise-mutation": "error",
1695
+ "arch/code-constructor": ["error", { constructors: ["Error"], calls: true }],
1696
+ "dillon-anti-slop/no-chained-type-assertions": "error",
1697
+ "dillon-anti-slop/no-conditional-empty-object-spread": "error",
1698
+ "dillon-anti-slop/no-known-value-widening": "error",
1699
+ "dillon-anti-slop/no-reflect-apply": "error",
1700
+ "dillon-anti-slop/no-reflect-get": "error",
1701
+ "dillon-anti-slop/no-runtime-typeof": "error",
1702
+ "dillon-anti-slop/no-shape-in-symbol-names": "error",
1703
+ "dillon-anti-slop/no-unknown-parameters": "error",
1704
+ "dillon-anti-slop/no-unknown-returns": "error",
1705
+ "dillon-anti-slop/no-unknown-type-aliases": "error",
1706
+ "dillon-anti-slop/no-unsafe-dictionary-type": "error",
1707
+ "dillon-anti-slop/no-widen-then-assert": "error",
1708
+ "dillon-anti-slop/require-safety-comment-for-type-assertion": "error"
1709
+ }));
1710
+ }
1711
+ return overrides;
1712
+ };
1713
+
1714
+ // src/preset/preset-folders.ts
1715
+ import { existsSync as existsSync3, readdirSync as readdirSync4, realpathSync as realpathSync2 } from "node:fs";
1716
+ import { posix as posix6, resolve as resolve7 } from "node:path";
1717
+
1718
+ // src/preset/preset-stylesheet.ts
1719
+ import { readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync } from "node:fs";
1720
+ import { posix as posix4, resolve as resolve5 } from "node:path";
1721
+ function presetTailwindEntry(css) {
1722
+ const code = css.replaceAll(/\/\*[\s\S]*?\*\//g, "");
1723
+ return /(?:^|[;{}])\s*@(?:import\s+(?:url\(\s*(["']?)tailwindcss(?:\/[^"')\s]*)?\1\s*\)|(["'])tailwindcss(?:\/[^"']*)?\2)|tailwind\s)/.test(code);
1724
+ }
1725
+ function presetStylesheet(root, directory) {
1726
+ const found = [];
1727
+ const queue = [directory];
1728
+ const visited = new Set;
1729
+ while (queue.length > 0) {
1730
+ const folder = queue.shift() ?? directory;
1731
+ const real = realpathSync(resolve5(root, folder));
1732
+ if (visited.has(real)) {
1733
+ continue;
1734
+ }
1735
+ visited.add(real);
1736
+ for (const name of readdirSync2(resolve5(root, folder))) {
1737
+ const path = posix4.join(folder, name);
1738
+ const full = resolve5(root, path);
1739
+ const depth = posix4.relative(directory, path).split("/").length;
1740
+ if (presetIsDirectory(full)) {
1741
+ if (!name.startsWith(".") && !presetDetectSkip.includes(name) && depth < 8) {
1742
+ queue.push(path);
1743
+ }
1744
+ } else if (name.endsWith(".css") && presetIsFile(full) && presetTailwindEntry(readFileSync3(full, "utf8"))) {
1745
+ found.push(path);
1746
+ }
1747
+ }
1748
+ }
1749
+ const rank = (path) => {
1750
+ const index = presetStylesheetNames.indexOf(posix4.basename(path, ".css"));
1751
+ return index === -1 ? presetStylesheetNames.length : index;
1752
+ };
1753
+ return found.toSorted((left, right) => rank(left) - rank(right) || left.length - right.length).at(0);
1754
+ }
1755
+
1756
+ // src/preset/preset-units.ts
1757
+ import { existsSync as existsSync2, readdirSync as readdirSync3 } from "node:fs";
1758
+ import { posix as posix5, resolve as resolve6 } from "node:path";
1759
+ var presetFrameworks = ["tanstack", "next", "expo"];
1760
+ var presetKindGroups = [...presetFrameworks, "backend", "console"];
1761
+ function presetUnitSource(root, key, kind, source) {
1762
+ if (source !== undefined) {
1763
+ if (source !== "." && !source.split("/").every((segment) => presetLiteralSegment.test(segment))) {
1764
+ throw new PresetError(`Folder \`${key}\` source must be \`.\` or a literal relative path.`);
1765
+ }
1766
+ return posix5.join(key, source);
1767
+ }
1768
+ return kind === "console" || !existsSync2(resolve6(root, key, "src")) ? key : posix5.join(key, "src");
1769
+ }
1770
+ function presetSets(folder, name) {
1771
+ return folder[name] !== undefined && folder[name] !== false;
1772
+ }
1773
+ function presetKind(key, enabled) {
1774
+ const frameworks = presetFrameworks.filter(enabled);
1775
+ if (frameworks.length > 1) {
1776
+ throw new PresetError(`Folder \`${key}\` enables more than one of tanstack, next and expo.`);
1777
+ }
1778
+ const [framework] = frameworks;
1779
+ if (frameworks.length > 0 && enabled("backend")) {
1780
+ throw new PresetError(`Folder \`${key}\` is either an app (tanstack, next, expo) or a backend, not both.`);
1781
+ }
1782
+ let kind = "library";
1783
+ if (frameworks.length > 0) {
1784
+ kind = "web";
1785
+ } else if (enabled("backend")) {
1786
+ kind = "backend";
1787
+ } else if (enabled("console")) {
1788
+ kind = "console";
1789
+ }
1790
+ return { kind, framework };
1791
+ }
1792
+ function presetHasManifest(root, key) {
1793
+ if (key === ".") {
1794
+ return false;
1795
+ }
1796
+ if (!key.endsWith("/*")) {
1797
+ return existsSync2(resolve6(root, key, "package.json"));
1798
+ }
1799
+ const base = resolve6(root, key.slice(0, -2));
1800
+ return presetIsDirectory(base) && readdirSync3(base).some((name) => existsSync2(resolve6(base, name, "package.json")));
1801
+ }
1802
+ function presetUnit(root, key, scope) {
1803
+ const { effective } = scope;
1804
+ const wildcard = scope.parent && scope.parent.depth === scope.depth && scope.parent.key.includes("*") ? scope.parent.folder : {};
1805
+ const folder = { ...wildcard, ...scope.folder };
1806
+ const own = presetKindGroups.some((name) => presetSets(folder, name)) || presetHasManifest(root, key);
1807
+ const { kind, framework } = presetKind(key, (name) => own ? presetSets(folder, name) : Boolean(effective[name]));
1808
+ const layout = {};
1809
+ const settings = effective.layout;
1810
+ if (settings) {
1811
+ const services = settings.services ?? (kind === "console" ? false : "domain");
1812
+ const hooks = settings.hooks ?? (kind === "web" ? "flat" : false);
1813
+ const modes = { services, hooks, components: settings.components ?? false };
1814
+ for (const [name, mode] of Object.entries(modes)) {
1815
+ if (mode === "flat" || mode === "domain") {
1816
+ layout[name] = mode;
1817
+ }
1818
+ }
1819
+ }
1820
+ return { key, scope, kind, framework, source: presetUnitSource(root, key, kind, folder.source), layout };
1821
+ }
1822
+ function presetUnits(root, scopes, ignored) {
1823
+ const [rootScope] = scopes;
1824
+ const rootKind = presetKind(".", (name) => Boolean(rootScope.effective[name])).kind;
1825
+ const candidates = [];
1826
+ for (const scope of scopes) {
1827
+ const manifest = presetHasManifest(root, scope.key);
1828
+ const own = presetKindGroups.some((name) => presetSets(scope.folder, name)) || manifest;
1829
+ const top = scope.parent?.key === "." && (rootKind === "library" || own);
1830
+ const sibling = scope.parent !== null && candidates.includes(scope.parent) && scope.parent.depth === scope.depth && !scope.key.includes("*");
1831
+ const workspace = scope.parent !== null && scope.parent.key !== "." && manifest;
1832
+ if (top || sibling || workspace) {
1833
+ candidates.push(scope);
1834
+ }
1835
+ }
1836
+ const unitScopes = candidates.filter((scope) => !ignored.has(scope.key));
1837
+ const explicit = new Set(unitScopes.map((scope) => scope.key));
1838
+ const rootSource = presetUnitSource(root, ".", rootKind, rootScope.folder.source);
1839
+ const overlaps = (left, right) => left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
1840
+ const rootOwnsSource = rootSource !== "." && !unitScopes.some((scope) => overlaps(scope.key.replace(/\/\*$/, ""), rootSource));
1841
+ const rootUnit = rootKind !== "library" || rootOwnsSource || unitScopes.every((scope) => presetUnit(root, scope.key, scope).kind === "console");
1842
+ return [...rootUnit ? [rootScope] : [], ...unitScopes].flatMap((scope) => {
1843
+ if (!scope.key.endsWith("/*")) {
1844
+ return [presetUnit(root, scope.key, scope)];
1845
+ }
1846
+ const base = scope.key.slice(0, -2);
1847
+ return readdirSync3(resolve6(root, base)).filter((name) => !name.startsWith(".") && presetIsDirectory(resolve6(root, base, name))).map((name) => posix5.join(base, name)).filter((key) => !explicit.has(key) && !ignored.has(key)).map((key) => presetUnit(root, key, scope));
1848
+ });
1849
+ }
1850
+
1851
+ // src/preset/preset-folders.ts
1852
+ function presetNormalizeKey(key) {
1853
+ const portable = key.replaceAll("\\", "/");
1854
+ if (!portable.trim() || posix6.isAbsolute(portable) || /^[a-z]:/i.test(portable)) {
1855
+ throw new PresetError(`Folder keys must be project-relative paths: ${key}`);
1856
+ }
1857
+ const normalized = posix6.normalize(portable).replace(/\/$/, "");
1858
+ if (normalized === ".." || normalized.startsWith("../")) {
1859
+ throw new PresetError(`Folder keys must stay inside the project: ${key}`);
1860
+ }
1861
+ const segments = normalized.split("/");
1862
+ if (normalized !== "." && segments.some((segment) => segment !== "*" && !presetLiteralSegment.test(segment))) {
1863
+ throw new PresetError(`Folder keys must be literal paths; only a whole \`*\` segment may be a wildcard: ${key}`);
1864
+ }
1865
+ if (segments.slice(0, -1).includes("*")) {
1866
+ throw new PresetError(`Only the last segment of a folder key may be \`*\`: ${key}`);
1867
+ }
1868
+ return normalized;
1869
+ }
1870
+ function presetMatches(pattern, path) {
1871
+ return pattern.every((segment, index) => segment === "*" || segment === path[index]);
1872
+ }
1873
+ function presetGroupValue(name, value, inherited, severity) {
1874
+ if (value === undefined) {
1875
+ return inherited && severity ? { ...inherited, level: severity } : inherited ?? null;
1876
+ }
1877
+ if (value === false) {
1878
+ return null;
1879
+ }
1880
+ const base = inherited ?? presetGroupOptions[name];
1881
+ if (value === true) {
1882
+ return { ...base, level: severity ?? base.level };
1883
+ }
1884
+ if (value === "warn") {
1885
+ return { ...base, level: "warn" };
1886
+ }
1887
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1888
+ throw new PresetError(`Group \`${name}\` takes true, false, 'warn' or an options object.`);
1889
+ }
1890
+ const changes = Object(value);
1891
+ const unknown = Object.keys(changes).find((key) => !(key in base) || key === "level");
1892
+ if (unknown) {
1893
+ throw new PresetError(`Unknown option \`${name}.${unknown}\`.`);
1894
+ }
1895
+ const plain = (option) => option instanceof Object && !Array.isArray(option);
1896
+ const merged = Object.fromEntries(Object.entries(changes).map(([key, option]) => {
1897
+ const stored = Object(base)[key];
1898
+ const field = presetShorthandOptions.find(([group, setting]) => group === name && setting === key)?.[2];
1899
+ const previous = typeof stored === "string" && field !== undefined && plain(option) ? { [field]: stored } : stored;
1900
+ return [key, plain(previous) && plain(option) ? { ...Object(previous), ...Object(option) } : option];
1901
+ }));
1902
+ return { ...base, ...merged, level: severity ?? base.level };
1903
+ }
1904
+ function presetDenyEntries(inherited, changes) {
1905
+ return Object.fromEntries(Object.entries({ ...inherited, ...changes }).flatMap(([key, message]) => {
1906
+ if (message !== false && typeof message !== "string") {
1907
+ throw new PresetError(`Deny entry \`${key}\` takes a message string or false.`);
1908
+ }
1909
+ return message === false ? [] : [[key, message]];
1910
+ }));
1911
+ }
1912
+ function presetEffective(folder, parent) {
1913
+ const resolveGroup = (name) => {
1914
+ const defaults = presetDefaultGroups.includes(name) ? presetGroupOptions[name] : null;
1915
+ return [name, presetGroupValue(name, folder[name], parent ? parent[name] : defaults, folder.severity)];
1916
+ };
1917
+ const effective = Object.fromEntries(presetGroupOrder.map(resolveGroup));
1918
+ const deny = folder.deny;
1919
+ if (effective.deny && deny instanceof Object) {
1920
+ const inherited = (parent ? parent.deny : null) ?? presetGroupOptions.deny;
1921
+ effective.deny = {
1922
+ ...effective.deny,
1923
+ imports: presetDenyEntries(inherited.imports, deny.imports),
1924
+ calls: presetDenyEntries(inherited.calls, deny.calls)
1925
+ };
1926
+ }
1927
+ return effective;
1928
+ }
1929
+ function presetFolder(key, value) {
1930
+ if (value === true || value === false) {
1931
+ return {};
1932
+ }
1933
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1934
+ throw new PresetError(`Folder \`${key}\` takes true, false or an object.`);
1935
+ }
1936
+ const options = Object.keys(value);
1937
+ const groups = presetGroupOrder;
1938
+ const unknown = options.find((option) => !presetFolderKeys.includes(option) && !groups.includes(option));
1939
+ if (unknown) {
1940
+ throw new PresetError(`Unknown folder option \`${unknown}\` in \`${key}\`.`);
1941
+ }
1942
+ const folder = value;
1943
+ if (folder.severity !== undefined && !["error", "warn"].includes(folder.severity)) {
1944
+ throw new PresetError(`Folder \`${key}\` severity must be 'error' or 'warn'.`);
1945
+ }
1946
+ return folder;
1947
+ }
1948
+ function presetScopesTree(root, folders) {
1949
+ const entries = Object.entries(folders).map(([key, value]) => [presetNormalizeKey(key), value]);
1950
+ const keys = entries.map(([key]) => key);
1951
+ const duplicate = keys.find((key, index) => keys.indexOf(key) !== index);
1952
+ if (duplicate) {
1953
+ throw new PresetError(`Folder \`${duplicate}\` is listed twice.`);
1954
+ }
1955
+ for (const key of keys) {
1956
+ const base = key.endsWith("/*") ? key.slice(0, -2) : key;
1957
+ if (!existsSync3(resolve7(root, base))) {
1958
+ throw new PresetError(`Folder \`${key}\` does not exist.`);
1959
+ }
1960
+ }
1961
+ const rootFolder = entries.find(([key]) => key === ".")?.[1];
1962
+ const rootScope = { key: ".", parent: null, depth: 0, folder: presetFolder(".", rootFolder ?? true), effective: {} };
1963
+ rootScope.effective = presetEffective(rootScope.folder, null);
1964
+ const scopes = [rootScope];
1965
+ const sorted = entries.filter(([key]) => key !== ".").toSorted(([left], [right]) => left.split("/").length - right.split("/").length || Number(right.includes("*")) - Number(left.includes("*")));
1966
+ for (const [key, value] of sorted) {
1967
+ const segments = key.split("/");
1968
+ const parents = scopes.filter((candidate) => {
1969
+ if (candidate.key === ".") {
1970
+ return false;
1971
+ }
1972
+ const pattern = candidate.key.split("/");
1973
+ const sibling = pattern.length === segments.length && candidate.key.includes("*") && !key.includes("*");
1974
+ return (pattern.length < segments.length || sibling) && presetMatches(pattern, segments);
1975
+ });
1976
+ const parent = parents.at(-1) ?? rootScope;
1977
+ const folder = presetFolder(key, value);
1978
+ scopes.push({
1979
+ key,
1980
+ parent,
1981
+ depth: segments.length,
1982
+ folder,
1983
+ effective: presetEffective(folder, parent.effective)
1984
+ });
1985
+ }
1986
+ return scopes;
1987
+ }
1988
+ function presetProject(config) {
1989
+ const given = resolve7(config.root ?? process.cwd());
1990
+ const root = existsSync3(given) ? realpathSync2(given) : given;
1991
+ const listed = Object.entries(config.folders ?? {}).map(([key, value]) => [presetNormalizeKey(key), value]);
1992
+ const twice = listed.find(([key], index) => listed.findIndex(([other]) => other === key) !== index);
1993
+ if (twice) {
1994
+ throw new PresetError(`Folder \`${twice[0]}\` is listed twice.`);
1995
+ }
1996
+ const folders = config.detect === false ? Object.fromEntries(listed) : presetDetect(root, Object.fromEntries(listed));
1997
+ const scopes = presetScopesTree(root, folders);
1998
+ const ignored = new Set(Object.entries(folders).flatMap(([key, value]) => value === false ? [presetNormalizeKey(key)] : []));
1999
+ const live = scopes.filter((scope) => !ignored.has(scope.key));
2000
+ const dead = live.find((scope) => scope.parent && ignored.has(scope.parent.key) && scope.parent.depth < scope.depth);
2001
+ if (dead) {
2002
+ throw new PresetError(`Folder \`${dead.key}\` sits inside the skipped folder \`${dead.parent?.key}\`.`);
2003
+ }
2004
+ const units = presetUnits(root, scopes, ignored);
2005
+ const unitKeys = new Set(units.map((unit) => unit.scope.key));
2006
+ const misplaced = live.find((scope) => !unitKeys.has(scope.key) && Object(scope.folder.layout).operations !== undefined);
2007
+ if (misplaced) {
2008
+ throw new PresetError(`Set layout.operations on an app or package folder, not on \`${misplaced.key}\`.`);
2009
+ }
2010
+ const writes = live.find((scope) => !unitKeys.has(scope.key) && Object(scope.folder.backend).writes !== undefined);
2011
+ if (writes) {
2012
+ throw new PresetError(`Set backend.writes on a backend folder, not on \`${writes.key}\`.`);
2013
+ }
2014
+ const of = (kind) => units.filter((unit) => unit.kind === kind);
2015
+ const webUnits = of("web");
2016
+ const web = webUnits.map((unit) => unit.source);
2017
+ const backendUnits = of("backend");
2018
+ const backend = backendUnits.map((unit) => unit.source);
2019
+ const backendRoots = backendUnits.map((unit) => unit.key);
2020
+ const libraryUnits = of("library");
2021
+ const packageSources = libraryUnits.map((unit) => unit.source);
2022
+ const app = [...web, ...backend, ...packageSources];
2023
+ const appFiles = presetScopes(app);
2024
+ const webFiles = presetScopes(web);
2025
+ const effective = live.map((scope) => scope.effective);
2026
+ const suffixes = [...new Set(effective.flatMap((groups) => groups.layout?.suffixes ?? []))];
2027
+ if (suffixes.some((suffix) => !/^[a-z][a-z0-9-]*$/.test(suffix))) {
2028
+ throw new PresetError("Layout suffixes must be lowercase kebab-case names.");
2029
+ }
2030
+ if (suffixes.some((suffix) => presetBuiltinFileRoles.includes(suffix))) {
2031
+ throw new PresetError("Layout suffixes must not replace built-in file roles.");
2032
+ }
2033
+ const orpcScopes = effective.flatMap((groups) => groups.orpc ? [groups.orpc] : []);
2034
+ const listOf = (values) => [...new Set(values.flatMap((value) => value ?? []))];
2035
+ const client = orpcScopes.find((settings) => settings.client !== undefined)?.client;
2036
+ const rpcClients = client === undefined ? webUnits.map((unit) => posix6.join(unit.source, "services", unit.layout.services === "domain" ? "rpc/rpc.client.ts" : "rpc.client.ts")) : [client];
2037
+ const kitUnits = units.filter((unit) => unit.scope.effective.kit);
2038
+ const uiRoots = [...new Set([...web.map((path) => posix6.join(path, "components/ui")), ...kitUnits.map((unit) => unit.source)])];
2039
+ const webUiRoots = uiRoots.filter((path) => web.some((webRoot) => webRoot === "." || path === webRoot || path.startsWith(`${webRoot}/`)));
2040
+ const stylesheet = (unit) => {
2041
+ const tailwind = unit.scope.effective.tailwind;
2042
+ if (!tailwind) {
2043
+ return;
2044
+ }
2045
+ const css = tailwind.css ?? presetStylesheet(root, unit.key);
2046
+ return css === undefined ? undefined : resolve7(root, css);
2047
+ };
2048
+ const webCss = Object.fromEntries(webUnits.flatMap((unit) => {
2049
+ const css = stylesheet(unit);
2050
+ return css ? [[unit.source, css]] : [];
2051
+ }));
2052
+ const webStylesheets = [...new Set(Object.values(webCss))];
2053
+ const kitCss = Object.fromEntries(units.filter((unit) => unit.kind !== "web" && unit.scope.effective.tailwind && !webUiRoots.includes(unit.source)).flatMap((unit) => {
2054
+ const name = presetManifest(root, unit.key).name ?? "";
2055
+ const consumer = webUnits.find((candidate) => {
2056
+ const manifest = presetManifest(root, candidate.key);
2057
+ return name in { ...manifest.dependencies, ...manifest.devDependencies };
2058
+ });
2059
+ const css = stylesheet(unit) ?? (unit.scope.effective.kit ? (consumer && webCss[consumer.source]) ?? webStylesheets.at(0) : undefined);
2060
+ if (css === undefined && unit.scope.effective.kit) {
2061
+ throw new PresetError(`The UI kit ${unit.key} needs a Tailwind stylesheet: set tailwind.css on it.`);
2062
+ }
2063
+ return css ? [[unit.source, css]] : [];
2064
+ }));
2065
+ const backendExports = listOf(effective.map((groups) => groups.backend?.exports));
2066
+ const publicEntrypoints = backendExports.length > 0 ? backendExports : backendRoots.flatMap((directory) => {
2067
+ const manifest = presetManifest(root, directory);
2068
+ const entries = manifest.exports instanceof Object ? Object.keys(manifest.exports).filter((key) => key.startsWith("./")) : [];
2069
+ if (!manifest.name) {
2070
+ return [];
2071
+ }
2072
+ return entries.length > 0 ? entries.map((key) => `${manifest.name}/${key.slice(2)}`) : [`${manifest.name}/client`];
2073
+ });
2074
+ const publicProcedures = listOf(orpcScopes.map((settings) => settings.public));
2075
+ const skipped = [...ignored].flatMap((key) => {
2076
+ if (!key.endsWith("/*")) {
2077
+ return [key];
2078
+ }
2079
+ const base = key.slice(0, -2);
2080
+ return readdirSync4(resolve7(root, base)).filter((name) => presetIsDirectory(resolve7(root, base, name))).map((name) => posix6.join(base, name)).filter((path) => !live.some((scope) => scope.key === path));
2081
+ });
2082
+ return {
2083
+ root,
2084
+ scopes: live,
2085
+ units,
2086
+ suffixes,
2087
+ colocated: listOf(effective.map((groups) => groups.types?.colocated)),
2088
+ orpc: {
2089
+ public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) => posix6.join(unit.source, "rpc/api")).filter((path) => existsSync3(resolve7(root, path))).map((path) => `${path}/**`),
2090
+ composers: listOf(orpcScopes.map((settings) => settings.composers)),
2091
+ client: client ?? ""
2092
+ },
2093
+ layouts: units.flatMap((unit) => Object.entries(unit.layout).map(([folder, mode]) => ({ path: posix6.join(unit.source, folder), folder, mode }))),
2094
+ rpcClients,
2095
+ operations: units.flatMap((unit) => unit.scope.effective.layout?.operations === "folders" ? [unit.source] : []),
2096
+ packages: libraryUnits.map((unit) => unit.key),
2097
+ scripts: of("console").map((unit) => unit.key),
2098
+ web,
2099
+ tanstackWeb: webUnits.filter((unit) => unit.framework === "tanstack").map((unit) => unit.source),
2100
+ nextWeb: webUnits.filter((unit) => unit.framework === "next").map((unit) => unit.source),
2101
+ expoWeb: webUnits.filter((unit) => unit.framework === "expo").map((unit) => unit.source),
2102
+ backend,
2103
+ backendRoots,
2104
+ packageSources,
2105
+ serviceRoots: [...backend, ...packageSources],
2106
+ app,
2107
+ appFiles,
2108
+ webFiles,
2109
+ reactFiles: [
2110
+ ...webFiles,
2111
+ ...presetScopes(packageSources, "**/*.tsx"),
2112
+ ...presetScopes(packageSources, `components/**/${presetExtensions}`),
2113
+ ...presetScopes(packageSources, `hooks/**/${presetExtensions}`)
2114
+ ],
2115
+ backendFiles: presetScopes(backend),
2116
+ services: presetScopes(app, `services/**/${presetExtensions}`),
2117
+ utilities: ["**/*.utils.ts", "**/*.constants.ts", "**/*.types.ts"],
2118
+ types: presetScopes(app, "types/**"),
2119
+ uiRoots,
2120
+ webUiRoots,
2121
+ ui: presetScopes(uiRoots, "**"),
2122
+ generated: presetGeneratedFiles,
2123
+ testFiles: presetTestFiles,
2124
+ safetyFiles: [...appFiles, ...presetTestFiles],
2125
+ components: presetScopes([...web, ...packageSources], "components/**/*.tsx"),
2126
+ hooks: presetScopes([...web, ...packageSources], "hooks/**/*.ts"),
2127
+ publicEntrypoints,
2128
+ baseImports: presetBaseImports,
2129
+ css: { ...webCss, ...kitCss },
2130
+ registries: backendUnits.flatMap((unit) => presetRegistryFile(root, unit.source, unit.scope.effective.backend?.registry) ?? []),
2131
+ ignores: skipped.map((key) => `${key}/**`),
2132
+ outputs: [".", ...units.map((unit) => unit.key)].flatMap((key) => presetOutputFolders.map((folder) => key === "." ? `${folder}/**` : `${key}/${folder}/**`))
2133
+ };
2134
+ }
2135
+
2136
+ // src/preset/preset.ts
2137
+ var recipes = presetRecipes;
2138
+ var templates = presetTemplates;
2139
+ var externals = presetExternals;
2140
+ var require2 = createRequire(import.meta.url);
2141
+ function presetOptional(name, group) {
2142
+ try {
2143
+ return require2.resolve(name);
2144
+ } catch {
2145
+ throw new PresetError(`The ${group} group needs ${name}: bun add -d ${name}`);
2146
+ }
2147
+ }
2148
+ var presetBuilders = {
2149
+ style: styleGroup,
2150
+ imports: importsGroup,
2151
+ types: typesGroup,
2152
+ layout: layoutGroup,
2153
+ naming: namingGroup,
2154
+ comments: commentsGroup,
2155
+ basics: basicsGroup,
2156
+ console: consoleGroup,
2157
+ tests: testsGroup,
2158
+ schemas: schemasGroup,
2159
+ sql: sqlGroup,
2160
+ orpc: orpcGroup,
2161
+ react: reactGroup,
2162
+ forms: formsGroup,
2163
+ tanstack: tanstackGroup,
2164
+ next: nextGroup,
2165
+ expo: expoGroup,
2166
+ backend: backendGroup,
2167
+ tailwind: tailwindGroup,
2168
+ shadcn: shadcnGroup,
2169
+ limits: limitsGroup,
2170
+ deny: denyGroup,
2171
+ externals: externalsGroup
2172
+ };
2173
+ function presetLevel(scope) {
2174
+ for (let current = scope;current; current = current.parent) {
2175
+ if (current.folder.severity) {
2176
+ return current.folder.severity;
2177
+ }
2178
+ }
2179
+ return "error";
2180
+ }
2181
+ function presetSettings(scope, name) {
2182
+ const settings = scope.effective[name];
2183
+ if (name !== "console") {
2184
+ return settings ?? null;
2185
+ }
2186
+ return settings ? null : { level: presetLevel(scope) };
2187
+ }
2188
+ function presetEmit(project, name, builder) {
2189
+ const key = (scope) => JSON.stringify(presetSettings(scope, name));
2190
+ const breaks = project.scopes.filter((scope) => scope.parent === null || key(scope) !== key(scope.parent));
2191
+ const nearest = (owner) => breaks.filter((scope) => {
2192
+ let current = scope.parent;
2193
+ while (current && current !== owner && !breaks.includes(current)) {
2194
+ current = current.parent;
2195
+ }
2196
+ return scope !== owner && current === owner;
2197
+ });
2198
+ return breaks.flatMap((owner) => {
2199
+ const settings = presetSettings(owner, name);
2200
+ if (!settings) {
2201
+ return [];
2202
+ }
2203
+ const excluded = nearest(owner).map((scope) => `${scope.key}/**`);
2204
+ return builder(project, settings, owner).flatMap((override) => {
2205
+ const files = override.files.flatMap((glob) => presetIntersect(glob, owner.key));
2206
+ if (files.length === 0 || !override.rules) {
2207
+ return [];
2208
+ }
2209
+ return [
2210
+ {
2211
+ ...override,
2212
+ files,
2213
+ excludeFiles: [...override.excludeFiles ?? [], ...excluded],
2214
+ rules: presetSeverity(override.rules, settings.level)
2215
+ }
2216
+ ];
2217
+ });
2218
+ });
2219
+ }
2220
+ function presetPlugins(project) {
2221
+ const react = project.scopes.some((scope) => scope.effective.react);
2222
+ const tailwinds = project.scopes.flatMap((scope) => scope.effective.tailwind ? [scope.effective.tailwind] : []);
2223
+ const shadcns = project.scopes.flatMap((scope) => scope.effective.shadcn ? [scope.effective.shadcn] : []);
2224
+ const tailwind = tailwinds.at(0);
2225
+ const shadcn = shadcns.at(0);
2226
+ if (new Set(tailwinds.map((settings2) => settings2.rem)).size > 1) {
2227
+ throw new PresetError("tailwind.rem is project-wide: set the same value in every folder, or set it once on `.`.");
2228
+ }
2229
+ if (new Set(shadcns.map((settings2) => JSON.stringify([settings2.ui, settings2.components]))).size > 1) {
2230
+ throw new PresetError("shadcn.ui and shadcn.components are project-wide: set the same values in every folder, or set them once on `.`.");
2231
+ }
2232
+ const jsPlugins = [
2233
+ { name: "eslint-js", specifier: require2.resolve("oxlint-plugin-eslint") },
2234
+ { name: "import-extra", specifier: require2.resolve("eslint-plugin-import") },
2235
+ require2.resolve("@regru/eslint-plugin-prefer-early-return"),
2236
+ require2.resolve("eslint-plugin-unused-imports"),
2237
+ require2.resolve("eslint-plugin-perfectionist"),
2238
+ require2.resolve("eslint-plugin-tsdoc"),
2239
+ require2.resolve("@eslint-community/eslint-plugin-eslint-comments"),
2240
+ { name: "arch", specifier: require2.resolve("oxlint-plugin-arch") },
2241
+ { name: "dillon-anti-slop", specifier: require2.resolve("oxlint-plugin-arch/dillon-anti-slop") }
2242
+ ];
2243
+ const settings = {
2244
+ react: { version: "19" },
2245
+ "import/resolver": { node: { extensions: [".js", ".jsx", ".ts", ".tsx", ".mts", ".cts"] } }
2246
+ };
2247
+ if (react) {
2248
+ jsPlugins.push({ name: "react-extra", specifier: presetOptional("eslint-plugin-react", "react") }, presetOptional("eslint-plugin-react-you-might-not-need-an-effect", "react"), presetOptional("eslint-plugin-svg-jsx", "react"));
2249
+ }
2250
+ if (!tailwind || Object.keys(project.css).length === 0) {
2251
+ return { jsPlugins, settings };
2252
+ }
2253
+ jsPlugins.push(presetOptional("oxlint-tailwindcss", "tailwind"));
2254
+ settings.tailwindcss = { rootFontSize: tailwind.rem };
2255
+ if (shadcn) {
2256
+ jsPlugins.push(presetOptional("@shadcn/lint", "shadcn"));
2257
+ settings.shadcn = { ui: shadcn.ui, componentImports: shadcn.components };
2258
+ }
2259
+ return { jsPlugins, settings };
2260
+ }
2261
+ function preset(config) {
2262
+ const project = presetProject(config);
2263
+ const [root] = project.scopes;
2264
+ const level = presetLevel(root);
2265
+ const emitGroup = (name) => {
2266
+ const builder = presetBuilders[name];
2267
+ return builder ? presetEmit(project, name, builder) : [];
2268
+ };
2269
+ const severities = project.scopes.filter((scope) => scope.parent && presetLevel(scope) !== presetLevel(scope.parent)).toSorted((left, right) => left.depth - right.depth).map((scope) => ({ files: [`${scope.key}/**`], rules: presetSeverity(presetCommonRules, presetLevel(scope)) }));
2270
+ const overrides = [...severities, ...presetGroupOrder.flatMap(emitGroup)];
2271
+ for (const scope of project.scopes.toSorted((left, right) => left.depth - right.depth)) {
2272
+ if (scope.folder.rules) {
2273
+ overrides.push({ files: [scope.key === "." ? "**" : `${scope.key}/**`], rules: scope.folder.rules });
2274
+ }
2275
+ }
2276
+ return {
2277
+ options: { typeAware: true },
2278
+ categories: { correctness: level },
2279
+ plugins: ["typescript", "react", "react-perf", "jsx-a11y", "import", "unicorn", "promise", "vitest", "node", "oxc"],
2280
+ ignorePatterns: [
2281
+ "**/node_modules/**",
2282
+ ...project.outputs,
2283
+ ...project.nextWeb.length > 0 ? ["**/.next/**", "**/next-env.d.ts"] : [],
2284
+ ...project.expoWeb.length > 0 ? ["**/.expo/**", "**/expo-env.d.ts"] : [],
2285
+ ...project.generated,
2286
+ ...project.ignores
2287
+ ],
2288
+ ...presetPlugins(project),
2289
+ rules: presetSeverity(presetCommonRules, level),
2290
+ overrides
2291
+ };
2292
+ }
2293
+ export {
2294
+ templates,
2295
+ recipes,
2296
+ preset,
2297
+ externals,
2298
+ PresetError
2299
+ };