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