oxlint-plugin-arch 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,38 +6,26 @@ Keep a codebase shaped the way you designed it: files in the right folders, expo
6
6
 
7
7
  - 🗂️ **Filename-aware.** Rules derive expected names from the file they run on (`user-action.create.ts` → `makeUserActionCreate`).
8
8
  - 🎛️ **Configurable.** No app names, folders, or libraries are baked in. Globs and policy live in your config.
9
- - 🛟 **Safe autofix.** Layout and comment rules only touch whitespace or comments. Type-import fixes qualify scope-resolved references and skip unsafe rewrites.
9
+ - 🛟 **Safe autofix.** Layout and comment rules touch whitespace or comments; `object-multiline` and `call-arguments-multiline` can also add trailing commas. Type-import fixes qualify scope-resolved references and skip unsafe rewrites.
10
10
  - ⚡ **Fast.** Built on Oxlint's JS plugin API and ESTree, with no extra parser.
11
11
 
12
- ---
12
+ ## Two ways to use it
13
13
 
14
- ## TanStack Start React modules preset
14
+ | | You get | Start here |
15
+ | --- | --- | --- |
16
+ | **Preset** | A ready policy for React apps with a typed backend (TanStack Start, Next.js, or both). Folders map to roles; every rule is already wired. | [Preset docs](docs/preset/README.md) |
17
+ | **Rules only** | 46 generic `arch/*` rules. You pick rules, globs and options in your own config. | [Install](#-install) |
15
18
 
16
- The optional **`tanstack-start-react-modules-preset`** supplies an opinionated application policy. The individual `arch` rules remain generic; importing the plugin alone does not enable the preset.
19
+ ## 🚀 Preset in 30 seconds
17
20
 
18
21
  ```sh
19
22
  bun add -d oxlint oxlint-tsgolint oxlint-plugin-arch
20
23
  ```
21
24
 
22
- In your root `oxlint.config.ts`:
23
-
24
25
  ```ts
26
+ // oxlint.config.ts
25
27
  import preset from 'oxlint-plugin-arch/presets/tanstack-start-react-modules-preset';
26
28
 
27
- export default preset({ root: import.meta.dirname });
28
- ```
29
-
30
- The equivalent named import is `tanstackStartReactModulesPreset` from `oxlint-plugin-arch/presets`. Both return a normal `OxlintConfig` object. The preset ships its JS-plugin dependencies and resolves them from its own package, including React, effects, Tailwind, shadcn, import ordering, formatting and type-safety rules. Type-aware linting is enabled; install `oxlint-tsgolint` and provide a tsconfig for the files you lint. Oxlint 1.85 or newer is required.
31
-
32
- Tailwind checks use `oxlint-tailwindcss`: canonical names, ordering, shorthand, conflicts, duplicates, deprecated classes and whitespace. Shadcn retains unknown-class validation and component styling contracts. The Tailwind plugin caches compiled theme data on disk; cold runs can cost more than subsequent runs. Set `OXLINT_TAILWINDCSS_CACHE_DIR` to control its cache location. Existing `better-tailwindcss/*` consumer overrides must migrate to `tailwindcss/*`: `enforce-canonical-classes` becomes `enforce-canonical`, `enforce-consistent-class-order` becomes `enforce-sort-order`, and `enforce-shorthand-classes` becomes `enforce-shorthand`; the other enabled rule suffixes remain the same.
33
-
34
- Custom utilities must be declared in the configured CSS entry point or its imports. Conflicts with properties defined by those utilities are still reported. Review intentional property overrides instead of globally allowing custom class names. In Oxlint 1.85, overlapping autofixes to the same class string can require another `--fix` run before it is clean.
35
-
36
- ### Architecture
37
-
38
- Paths are relative **app roots**, not `src` directories. These are the defaults:
39
-
40
- ```ts
41
29
  export default preset({
42
30
  root: import.meta.dirname,
43
31
  architecture: {
@@ -49,199 +37,7 @@ export default preset({
49
37
  });
50
38
  ```
51
39
 
52
- `architecture` maps paths to roles and replaces the defaults when supplied. Multiple apps may share a role: `{ 'apps/dashboard': 'web', 'apps/storefront': 'web', 'apps/api': 'server', packages: 'packages', scripts: 'scripts' }`. Use `runner` for another strict backend, or `{ '.': 'web' }` for a standalone frontend. Omit absent roots. Paths are normalized; duplicates, overlapping roots and paths outside the project are rejected. Web and backend roles derive `src/services`, `src/types` and other application scopes; packages have no assumed app layout, and scripts permit console output. Shared baseline policies still apply everywhere. Each web root defaults to its own `src/application/styles.css`. Set `tailwind.cssEntryPointsByRoot: { 'apps/storefront': 'themes/store.css' }` for per-root paths, or `tailwind.cssEntryPoint` for an explicitly shared stylesheet. Entry paths are relative to `root`; `tailwind: false` disables Tailwind. Per-root CSS is supplied through Tailwind rule options because Oxlint 1.85 has no override-level `settings`. Shadcn discovers each app's theme through its own `components.json` (or package-local discovery). `root` makes asset and package resolution independent of the lint command's working directory; invoke the root config from its real filesystem location.
53
-
54
- Folder layout is independent of role/content rules. Shorthand roles use defaults: web has domain services and flat hooks; server/runner have domain services. Other folders keep their existing role conventions unless explicitly configured. Object entries merge folder choices into those defaults:
55
-
56
- ```ts
57
- architecture: {
58
- 'apps/dashboard': { role: 'web', layout: { services: 'flat', hooks: 'domain', features: 'domain' } },
59
- 'apps/storefront': { role: 'web', layout: { services: 'domain', features: 'flat' } },
60
- 'apps/api': { role: 'server', layout: { services: 'domain' } },
61
- }
62
- ```
63
-
64
- Folder keys are literal paths relative to an app's `src` (relative to the root for packages/scripts), not new architecture slots. Segments use letters, numbers, underscores and hyphens; absolute paths, escapes, globs, empty segments and overlapping configured/default folders are rejected. `flat` requires source files directly in that folder. `domain` requires exactly one directory and filename ownership by that directory; loose/deeper files cannot coexist. Checks cover TS/TSX/MTS/CTS source files, not asset directory trees. Generic domain filenames start with their owning directory plus a dot/hyphen; hooks retain `use-` and components retain their existing singular-name convention. Choosing layout never disables service exports, types, constants, runtime suffixes or anti-slop. Existing policy callbacks and native overrides remain available.
65
-
66
- RPC ownership follows each web root's service layout: `services/rpc.client.ts` for flat, `services/rpc/rpc.client.ts` for domain. `orpc.clientOwnerFile` remains the explicit consumer override. There is no mixed-layout mode.
67
-
68
- ### Configure settings and policies
69
-
70
- Settings are grouped by responsibility. The unreleased preset uses only this grouped API; old flat preset keys are not aliases. Standalone rule options are unchanged.
71
-
72
- ```ts
73
- export default preset({
74
- root: import.meta.dirname,
75
- severity: 'warn',
76
- limits: { maxFileLines: 400, maxFunctionComplexity: 32 },
77
- modules: { customFileRoles: ['prompts'] },
78
- imports: {
79
- backendEntryPoints: ['@app/server/client'],
80
- aliases: { '@backend': 'apps/server/src' },
81
- internalSortPatterns: ['^@app/'],
82
- },
83
- orpc: {
84
- publicProcedureFiles: ['apps/server/src/rpc/public/**/*.ts'],
85
- outputSchemaComposers: ['paginatedOutput'],
86
- clientOwnerFile: 'apps/web/src/services/rpc/rpc.client.ts',
87
- },
88
- react: { compiler: true },
89
- forms: { schemaResolver: 'standardSchemaResolver' },
90
- sql: { likeSanitizers: ['escapeLikeWildcards'] },
91
- policies: {
92
- typePlacement: (current) => current.map((scope) => ({
93
- ...scope,
94
- excludeFiles: [...(scope.excludeFiles ?? []), '**/portable/**'],
95
- })),
96
- },
97
- });
98
- ```
99
-
100
- Each entry under `policies` accepts `true` (default configuration), `false` (disabled), or a callback receiving fresh native Oxlint override objects. Callbacks run in the existing policy order before overall severity is applied; deliberately disabled rules stay disabled. They never mutate another preset invocation. Append an override to the callback's returned array to extend a policy.
101
-
102
- Available policies: `typePlacement`, `typeSafety`, `serviceStructure`, `fileLayout`, `naming`, `comments`, `formatting`, `statementLayout`, `imports`, `react`, `effects`, `memoization`, `routes`, `forms`, `schemas`, `boundaries`, `tests`, `trivialFunctions`, `mutableState`, `backend`, `rpcClientOwnership`, `tailwind`, and `shadcn`.
103
-
104
- `policies.typePlacement: false` disables type location restrictions; `policies.typeSafety` still rejects explicit any/unknown. `imports` contains import settings, while `policies.imports` customizes rule overrides. Backend entry points govern permitted imports and schema ownership; internal sort patterns only classify import ordering. `orpc.publicProcedureFiles` identifies endpoint files requiring output schemas. `cliFiles` lists CLI files receiving console/switch exceptions.
105
-
106
- `severity` defaults to `error` and accepts `warn`. File and function limits default to 400 and 32. React Compiler is assumed by default: manual memoization is banned and the four React performance rules against render-time function/object/array/JSX props are off. `react: { compiler: false }` allows manual memoization and enables those four rules. `react: { componentProps: false }` turns off only `arch/component-props` (props typed with `interface XProps` directly above each component); it is on by default. `shadcn: false` disables shadcn integration while retaining Tailwind checks. `tailwind` accepts `cssEntryPoint`, `cssEntryPointsByRoot` and `rootFontSize`; `shadcn` accepts `uiImportPath` and `componentImportSources`. UI-kit files remain linted, with architecture/appearance exceptions rather than a global ignore.
107
-
108
- Shadcn allows caller-owned layout, standard opacity utilities and exact `truncate` (including variant forms). Stock composition slots also allow these class groups:
109
-
110
- | Components | Additional groups |
111
- | --- | --- |
112
- | `CardContent`, `PopoverContent` | `p`, `px`, `py`, `ps`, `pe`, `pt`, `pr`, `pb`, `pl` |
113
- | `TabsContent` | The padding groups above plus `gap`, `gap-x`, `gap-y` |
114
- | `Tabs`, `HoverCardContent`, `SidebarHeader`, `BreadcrumbList` | `gap`, `gap-x`, `gap-y` |
115
-
116
- These are exact component-name contracts, not blanket content/spacing exceptions. Each keeps `layout`, `opacity` and `truncate`; controls such as `Button` still reject padding and gap overrides. A contract's `allow` replaces the baseline, and the last matching contract wins. Native file-specific rule options replace contract arrays rather than merging them.
117
-
118
- Arbitrary padding values may use `env(safe-area-inset-top)`, `env(safe-area-inset-right)`, `env(safe-area-inset-bottom)` or `env(safe-area-inset-left)` with any padding group above. Padding sides/axes (not `p`) also allow `max(--spacing(*),env(safe-area-inset-…))`, for example `pb-[max(--spacing(4),env(safe-area-inset-bottom))]`. Literal floors such as `max(1rem,env(…))` remain rejected. These are narrow class globs, **not CSS-expression validation**: the wildcard inside `--spacing(...)` can still match malformed arguments or extra arithmetic. The allowance does not bypass component restyling checks.
119
-
120
- Only `<web root>/src/components/ui/sonner.tsx` allows the stock Sonner selector hooks `toaster` and `toast`; all other unknown classes remain checked, including in UI files. These defaults need no extra preset options or consumer overrides.
121
-
122
- Module configuration, lookup tables, limits and defaults belong in domain `.constants.ts` files, including frontend services. All frontend constant declarations carry the domain prefix (for example `BOT_MODEL_PRIORITY`). Frontend service scopes enable `modules/domain-constants` with `includeData: true`: it detects module-level literal/object/array data, literal calculations and seeded Map/Set tables, plus the existing uppercase constants. Function-local calculations and call-created service instances remain in their owner; empty Map/Set state is not classified as configuration. This is syntactic enforcement, not semantic data-flow analysis: imported aliases and arbitrary call-created configuration still need review. Backend rule defaults are unchanged. Every scope follows the configured architecture roots; a web `.server.ts` file remains frontend-owned.
123
-
124
- ### Tests
125
-
126
- Both test profiles use the same assertion, typed-mock, type-safety and no-module-mocking rules. `fixtures` is the default and preserves the backend convention of importing named fixtures or `tests/support`, rather than `tests/helpers`. `standard` leaves helper-folder organization open. Neither profile chooses a database, constructs a test kit, changes fixture lifetimes, or requires a particular service assembly.
127
-
128
- ```ts
129
- export default preset({
130
- tests: {
131
- profile: 'fixtures', // Or 'standard'.
132
- additionalTestFunctions: ['integrationTest'],
133
- additionalAssertionFunctions: ['expectRejectsWithInternal'],
134
- },
135
- });
136
- ```
137
-
138
- Regular tests, `test.for`/`test.each` tables and their `it` equivalents are recognized in both profiles, including concurrent/sequential forms. Additional test names also register those forms. Test-function names must be literal identifiers or dotted names. Assertion helpers also accept rooted dotted patterns, for example `arch.**.to*` for an explicitly reviewed fluent assertion API. Register only assertion helpers that actually fail on incorrect results; registering a name does not inspect its implementation. Standard Vitest `expect`, `expectTypeOf`, `assert`, `assert.*` and `assertType` assertions remain enabled.
139
-
140
- `modules/test-assertions` checks assertion presence and placement for native Vitest tests, locally extended fixture tests and configured custom functions. It replaces the preset’s `vitest/expect-expect` and `vitest/no-standalone-expect` checks: those cannot consistently follow fixture and table-row callbacks. It follows called local helpers and statically declared object table rows, including destructured/renamed callbacks, and requires an assertion for every row. Unused assertion callbacks do not satisfy it. Assertions outside tests or assertion helpers remain errors.
141
-
142
- Move custom native assertion-name overrides to `tests.additionalAssertionFunctions` (or scoped `modules/test-assertions` options through `policies.tests`). This is a static check, not proof that every execution reaches an assertion. Imported callbacks, dynamically constructed tables and cross-file helper bodies are not inferred; keep a direct assertion or register a reviewed imported assertion helper. Conditional assertions remain forbidden by the separate Vitest rule.
143
-
144
- Prefer a throwing assertion to conditional expectations when narrowing a discriminated result:
145
-
146
- ```ts
147
- import { assert, expect } from 'vitest';
148
-
149
- const result = await service.run(params);
150
-
151
- assert(result.ok, 'Expected the operation to succeed');
152
-
153
- expect(result.data.ordersCreated).toBe(1);
154
- ```
155
-
156
- `assert` fails the test and narrows the TypeScript type. No cast, custom unwrap helper or conditional expectation is needed. A prior success expectation followed by an `if` can be logically sound, but this form is simpler and keeps the conditional-expect rule enabled. Use `policies.tests` for native policy customization and `ignorePatterns` only when intentionally excluding tests from the entire lint run.
157
-
158
- ### Framework migrations
159
-
160
- Migration files can have framework-required exports such as Kysely's `up`/`down`. Declare their exact scope instead of restructuring them into service factories:
161
-
162
- ```ts
163
- export default preset({
164
- modules: {
165
- migrationFiles: ['apps/server/src/services/database/migrations/**/*.ts'],
166
- },
167
- });
168
- ```
169
-
170
- No migration paths are assumed by default. The supplied files are excluded from flat/domain folder-depth enforcement and the general service-functions/inline-signature scope. Formatting, naming, imports, type safety, constants and other rules still apply; this is not a whole-file ignore. Keep scopes narrow. In particular, `unknown` or framework-specific type exceptions require their own explicit `ruleExclusions`.
171
-
172
- ### Exceptions and native customization
173
-
174
- Action/query files (`services/**/*-{action,query}.*.ts` under configured app roots) export exactly one named operation. Helpers used only by that operation may be private nested functions or arrows inside it. Extra exports, including types, constants and re-exports, fail. Module-level private helpers still fail. Backend service files retain the stricter helper policy; `.utils.ts` files are for genuinely shared domain helpers, not a destination for every extraction. The trivial-function rule still applies.
175
-
176
- Frontend `.client.ts`, `.server.ts` and `.rsc.{ts,tsx}` service entries are cohesive modules: multiple public domain-prefixed operations are allowed. No private module-level function satellites are allowed. Operation-only helpers stay nested inside their public operation and retain the domain prefix; genuinely shared helpers belong in domain `.utils.ts`. Factories are optional for real state, injection or lifetimes, and existing singleton exports remain valid. Do not export helpers just to evade this rule. Runtime suffix checks, React-free client checks and anti-slop rules remain enabled. A direct `export const themeService = { themeApply(...) { ... } }` is also allowed: a nonempty method-only object, static identifier keys, both binding and methods domain-prefixed. Data fields, getters/setters, strings/computed keys, spreads, arrow properties and callback-reference tables do not qualify. Returned factory API keys retain their existing contract. `allowServiceMethods` on domain-constants, `serviceMethods` on naming/object-parameter rules and `checkServiceMethods` on no-trivial-functions are enabled only for frontend runtime scopes; backend defaults remain unchanged. Object methods receive wrapper/generic-guard checks and named-parameter-contract rules. This is a syntax form, not proof that an object owns meaningful state.
177
-
178
- Frontend actions/queries retain their runtime suffix: `theme-action.apply.client.ts` exports exactly `themeActionApply`, not `themeActionApplyClient`. Related helpers stay inside that operation. Backend operations remain `domain-action.name.ts` / `domain-query.name.ts`, without runtime suffixes.
179
-
180
- Shared frontend domain helpers use `<domain>.utils.ts` in the configured services layout. Domain mode groups all runtime, utility, constants and action/query files under `services/<domain>/`; flat mode keeps all directly under `services/`. Utility functions, including private helpers, keep their domain prefix. Pure implementation does not make an application-specific helper portable: the preset enables `portableLib` in `modules/import-boundaries` for every configured web root. Files under `src/lib/` cannot import or re-export any configured app's services or app package APIs, including otherwise public backend entrypoints and type-only references. Backend library scopes retain their existing rules.
181
-
182
- A portable module may consume its own contract using a type-only import from exactly the same web root's `types/<module>.types.ts`. The module stem removes the source extension and optional `.utils` suffix: both `lib/fade.ts` and `lib/fade.utils.ts` may consume `types/fade.types.ts`. Value imports, other-domain types, same-stem types from another app, and app-type re-exports (including imported bindings subsequently exported) fail. Relative paths, the built-in `@/` alias and configured aliases use the same ownership check. This is a filename ownership convention, not transitive semantic portability proof: the rule does not inspect imported contracts or follow module graphs. Static imports/re-exports, literal dynamic imports and TypeScript import types are checked; computed imports and arbitrary alias/data flow are not resolved. Custom aliases must be configured explicitly. An own-type match never overrides a services/backend/API ban.
183
-
184
- The supporting rule's `portableLib` option defaults to `false` outside the preset for compatibility. Use the existing `policies.boundaries` callback, native overrides or file-specific `ruleExclusions['modules/import-boundaries']` for explicit consumer exceptions; an exclusion skips that rule's entire boundary check for the named file, so keep it narrow and documented.
185
-
186
- The preset's imports policy enables `arch/prefer-namespace-type-import` with `{ max: 3 }` for checked TypeScript files. Up to three named type imports stay named; four or more become a type namespace (for example, `BotTypes` from `bot.types`) with qualified references. The scope-aware autofix preserves aliases and rewrites only supported type references. Imports with namespace collisions, inline comments, local re-exports or unsupported reference syntax remain reported for manual review. Domain exports keep their names. Customize `max` or the rule's source-to-namespace `names` map through the existing `policies.imports` callback or a native override; file-specific exclusions remain available.
187
-
188
- Declare cohesive private concept roles with `modules: { customFileRoles: ['prompts'] }`. Built-in roles (`service`, `client`, `server`, `rsc`, `utils`, `action`, `query`, `types`, `constants`, `handler`, `test`, `spec`) cannot be redefined. A `route.prompts.ts` file may contain multiple builders, but every named function, including private functions, starts with the full `routePrompts` prefix. Arbitrary suffixes grant no exemption. Anti-slop checks remain enabled and comment exceptions remain explicit.
189
-
190
- With declared file roles, private service-domain files can be imported or re-exported only within their owning domain in the same app root. Cross-domain callers use service APIs (`.service`, `.client`, `.server`, `.rsc`) or genuinely shared `.utils`, types or constants. Public service/utility surfaces cannot re-export private internals, including private barrels; this checks export-from, export-star and directly imported bindings exported by name or namespace. Operations may use private concepts internally. This is a per-file surface contract, not a transitive module graph or arbitrary data-flow/alias analysis; do not move concept logic into utilities to evade ownership.
191
-
192
- `modules/service-functions` accepts `frontend`, `allowLocalHelpers`, `allowReturnedMethods`, `singleExport`, and `message` through the `policies.serviceStructure` callback or native overrides. The booleans default to `false` outside the preset's scoped overrides. `frontend: true` permits multiple public operations and their nested helpers; `singleExport: true` still requires exactly one operation. The preset enables frontend mode only for web runtime entries and applies the stricter single-export override to explicit actions/queries. The preset's diagnostic explains where helpers belong; consumers can replace `message` without disabling the rule.
193
-
194
- `ruleExclusions` disables only the named rule in the selected files, including rules enabled through several scopes. `ignorePatterns` adds whole-file ignores to the generated/build/dependency defaults.
195
-
196
- ```ts
197
- export default preset({
198
- ruleExclusions: {
199
- 'arch/no-trivial-functions': ['apps/web/src/services/adapter.client.ts'],
200
- 'modules/service-types': ['apps/server/src/services/portable-parser.ts'],
201
- },
202
- ignorePatterns: ['vendor/**'],
203
- });
204
- ```
205
-
206
- Native composition remains available; later matching overrides can change an individual rule without a policy callback:
207
-
208
- ```ts
209
- import { defineConfig } from 'oxlint';
210
- import preset from 'oxlint-plugin-arch/presets/tanstack-start-react-modules-preset';
211
-
212
- export default defineConfig({
213
- options: { typeAware: true },
214
- extends: [preset({ root: import.meta.dirname })],
215
- overrides: [{
216
- files: ['scripts/**/*.ts'],
217
- rules: { 'unicorn/filename-case': 'off' },
218
- }],
219
- });
220
- ```
221
-
222
- Rule-specific inline disables are allowed with a reason after `--`. The preset does not force warnings to fail or define a CI/Fallow/compiler workflow.
223
-
224
- Oxlint reads `typeAware` from the root config only. The direct `export default preset(...)` form already sets it; when using `extends`, keep the explicit root option shown above.
225
-
226
- ### Application conventions
227
-
228
- - Single quotes, semicolons, two spaces, braced multiline control flow, early returns, 160-character lines and 400-line app and test files. Promise arrays and call objects expand; method chains are not forced multiline.
229
- - Components live in one group folder with matching prefixed names. Hooks default to flat `use-*.ts` files; an explicit domain layout retains the hook filename convention. Local React types are prefixed interfaces; exported Props interfaces are allowed. Component props are never inline: every component `X` takes `interface XProps` declared directly above it, and local components carry the file prefix. Other types belong in domain `types/*.types.ts` files.
230
- - Backend operations use `domain-action.name.ts` / `domain-query.name.ts`. Services allow one domain folder, one exported operation without private helpers, object parameters and named signature types. Explicit `.utils.ts` files can hold generic helpers, but their types still belong in domain type files. Domain type and constant files cannot contain top-level functions. Frontend services use `.client.ts`, `.server.ts` or `.rsc.{ts,tsx}`. RSC JSX entries retain `domainRscName` exports and the same helper/constants policies.
231
- - Routes wire components, forms use a schema resolver, context creation has an owner directory, and effects cannot replace derived values or event handlers. React Compiler projects reject manual memoization. Test IDs are allowed. JSX rejects new object/function props inside render, and noninteractive elements cannot receive a tab index.
232
- - Safety rules reject unknown/any, chained assertions, unjustified assertions, loose dictionaries, module mocks, shared mutable bindings and focused/skipped/placeholder tests. Comments are limited to supported directives, SAFETY notes and explicit exceptions. Type/constant files do not allow SAFETY comments; prompt files require explicit comment exceptions. UI-kit code retains type-safety, formatting and layout checks.
233
- - Unbound methods and untyped mocks are rejected, including in tests. Console calls require an explicit script/CLI scope or adapter exclusion. Promise rejection callbacks must not receive implicit `any`; omitting the error parameter is allowed. App code needing an `unknown` boundary must use an explicit rule-specific exception.
234
- - Backend owns shared schemas and exposes a public client entry point; frontend type files are runtime-free. Packages cannot import app internals, RPC handlers delegate database work, and service dependencies are injected.
235
-
236
- Provide `orpc: { publicProcedureFiles: ['apps/server/src/rpc/api/**/*.ts'] }` for externally exposed oRPC endpoints requiring output schemas; exposure cannot be inferred from filenames or authentication. `imports.backendEntryPoints` defaults to `<backend package name>/client` read from each configured backend manifest. Configure it for another public import surface.
237
-
238
- For custom import aliases, supply `imports: { aliases: { '@backend': 'apps/server/src' } }`. Keys are import prefixes without wildcards, and values are directories relative to `root`. Boundary checks then treat `@backend/services/chat.service` like its relative path. `imports.internalSortPatterns` controls import sorting only.
239
-
240
- Native correctness defaults are promoted through `categories.correctness`; redundant rule entries are omitted after checking Oxlint's resolved configuration. Framework policies add the opinionated options on top. Other future presets can have their own named subpath under `/presets`; this factory does not enable any other preset.
241
-
242
- The preset has [paired CLI fixtures for every enabled rule](tests/fixtures/tanstack-start-react-modules/README.md), including Oxlint defaults, plus architecture and consumer-override scenarios. The coverage guard detects newly enabled rules without fixtures.
243
-
244
- The preset's supporting `modules/*` rules keep independent policies from overwriting one another: service type/factory matching, helper restrictions, import boundaries, reasoned lint directives, Shape-suffix names, empty effects, memoization and test modifiers. Dillon Mulroy's vendored [anti-slop rules](https://github.com/dmmulroy/anti-slop) live in `src/rules/dillon-anti-slop` and use the `dillon-anti-slop/*` namespace. They ship in this package; consumers do not need local plugin files. These supporting plugins are loaded by the preset, not registered by the generic plugin entry point.
40
+ For a Next.js app use `oxlint-plugin-arch/presets/next-react-modules-preset`. See [architecture](docs/preset/architecture.md), [frameworks](docs/preset/frameworks.md) and [options](docs/preset/options.md).
245
41
 
246
42
  ## 📦 Install
247
43
 
@@ -264,43 +60,7 @@ export default defineConfig({
264
60
  })
265
61
  ```
266
62
 
267
- ### Use the supporting plugins without the preset
268
-
269
- All three first-party rule sets have public package entry points. Register only the plugins and rules you need; this does not enable the preset or its policies.
270
-
271
- ```ts
272
- import { defineConfig } from 'oxlint'
273
-
274
- export default defineConfig({
275
- jsPlugins: [
276
- { name: 'arch', specifier: 'oxlint-plugin-arch' },
277
- { name: 'dillon-anti-slop', specifier: 'oxlint-plugin-arch/rules/dillon-anti-slop' },
278
- { name: 'modules', specifier: 'oxlint-plugin-arch/rules/module-policy' },
279
- ],
280
- rules: {
281
- 'arch/no-literal-in': 'error',
282
- 'dillon-anti-slop/no-chained-type-assertions': 'error',
283
- 'modules/no-unknown': 'error',
284
- },
285
- })
286
- ```
287
-
288
- Both `/rules/` entry points also provide a default plugin export and TypeScript declarations. Do not import `dist/` paths. The preset resolves these same public entry points, including the existing package root for `arch`; it needs no other first-party plugin entry points. Individual rule implementations and helpers are bundled into those plugins.
289
-
290
- If you copy the preset's third-party plugin registrations into your own config, install those packages directly with `bun add -d` rather than relying on transitive dependency hoisting:
291
-
292
- | Scope | Packages |
293
- | --- | --- |
294
- | Always | `oxlint-plugin-eslint`, `eslint-plugin-import`, `@regru/eslint-plugin-prefer-early-return`, `eslint-plugin-unused-imports`, `eslint-plugin-perfectionist`, `eslint-plugin-tsdoc`, `@eslint-community/eslint-plugin-eslint-comments` |
295
- | Web roots | `eslint-plugin-react`, `eslint-plugin-react-you-might-not-need-an-effect`, `eslint-plugin-svg-jsx` |
296
- | CSS entry points with Tailwind enabled | `oxlint-tailwindcss` |
297
- | Same CSS scope with shadcn enabled | `@shadcn/lint` |
298
-
299
- These plugins remain **dependencies** of `oxlint-plugin-arch`, not peer dependencies: the preset loads its own installed copies. In an application-owned config, list directly used plugins as **devDependencies**. A reusable config package should use dependencies for plugins it resolves itself, or peerDependencies if it intentionally requires consumers to supply them. `@oxlint/plugins` is the bundled plugins' runtime dependency, not another preset rule set. `oxlint` remains a peer dependency; `oxlint-tsgolint` is an optional peer required when using the preset's type-aware checks. Native Oxlint plugin groups need no separate npm plugin packages.
300
-
301
- ## 🚀 Quick start
302
-
303
- Rules are switched on per folder with `overrides`. Here are three small ones:
63
+ Then turn rules on per folder with `overrides`:
304
64
 
305
65
  ```ts
306
66
  export default defineConfig({
@@ -325,324 +85,152 @@ export default defineConfig({
325
85
  })
326
86
  ```
327
87
 
88
+ To also use the `modules/*` and `dillon-anti-slop/*` rule sets without the preset, see [using the plugins without the preset](docs/plugins.md).
89
+
328
90
  ## 📚 Examples
329
91
 
330
92
  | File | Use it when |
331
93
  | --- | --- |
332
94
  | [`minimal.oxlint.config.ts`](examples/minimal.oxlint.config.ts) | You want the smallest possible starting point. |
333
95
  | [`monorepo.oxlint.config.ts`](examples/monorepo.oxlint.config.ts) | ⭐ You are starting an `apps/server` + `apps/web` monorepo and want a strict, copy-ready setup. |
334
- | [`full.oxlint.config.ts`](examples/full.oxlint.config.ts) | You want to see every one of the 44 rules with its options. |
96
+ | [`full.oxlint.config.ts`](examples/full.oxlint.config.ts) | You want to see every `arch/*` rule with its options. |
97
+ | [`preset.oxlint.config.ts`](examples/preset.oxlint.config.ts) | You use the TanStack Start preset with layouts, migrations and a policy callback. |
98
+ | [`mixed-frameworks.oxlint.config.ts`](examples/mixed-frameworks.oxlint.config.ts) | You use the generic preset with Next.js App Router and TanStack Start web roots in one config. |
335
99
 
336
100
  > [!TIP]
337
- > `monorepo.oxlint.config.ts` already loads `oxlint-plugin-arch`, so you can copy it as-is and adjust globs and names. The other two load `../src/index.ts`; change that specifier to `oxlint-plugin-arch` when you copy them.
338
-
339
- ---
101
+ > `monorepo.oxlint.config.ts` and the preset examples already load `oxlint-plugin-arch`, so you can copy them as-is. `minimal` and `full` load `../src/index.ts`; change that specifier to `oxlint-plugin-arch` when you copy them.
340
102
 
341
103
  ## 🧩 Rules
342
104
 
343
- 44 rules in 9 groups. 🔧 means the rule can autofix. Each rule file in [`src/rules/`](src/rules) explains its behavior in plain English, and the tests in [`src/tests/`](src/tests) show every option shape.
105
+ 🔧 means the rule can autofix. Each rule links to its page with examples and options.
344
106
 
345
107
  Options that take one regex string (`pattern`, `allowPattern`, `forbid`, `require`, and the other `*Pattern` options) also take a non-empty list; any pattern in the list may match. Options that were already lists, such as `allowPatterns` and `allowCallees`, are unchanged.
346
108
 
347
- ### 📁 Files & folders
109
+ ### `arch/*`
110
+
111
+ #### 📁 Files & folders
348
112
 
349
113
  | Rule | What it enforces |
350
114
  | --- | --- |
351
- | `filename-match` | Filenames match a pattern you configure. |
352
- | `folder-prefix` | Filenames start with their parent folder name, or the folders after a root you choose. |
353
- | `no-restricted-files` | Files matching a glob you forbid are reported, for example misplaced `*.types.ts`. |
115
+ | [`filename-match`](docs/rules/arch/filename-match.md) | Filenames match a pattern you configure. |
116
+ | [`folder-prefix`](docs/rules/arch/folder-prefix.md) | Filenames start with their parent folder name, or the folders after a root you choose. |
117
+ | [`no-restricted-files`](docs/rules/arch/no-restricted-files.md) | Files matching a glob you forbid are reported, for example misplaced `*.types.ts`. |
354
118
 
355
- ### 🏷️ Naming & exports
119
+ #### 🏷️ Naming & exports
356
120
 
357
121
  | Rule | What it enforces |
358
122
  | --- | --- |
359
- | `declaration-name` | Selected declarations start with a prefix taken from the filename, or match a pattern. |
360
- | `export-file-prefix` | Names start with the filename prefix. `allFunctions` includes private functions; `allDeclarations` includes every declaration. `trailingRoles` handles role suffixes. |
361
- | `export-name-pattern` | Export names (or every function and type) match a regular expression. |
362
- | `filename-export-name` | Function names follow a template built from the filename. Set `camelCase: true` to convert lowercase placeholders such as `{domain}` from `user-profile` to `userProfile`; the default preserves captured text. |
363
- | `no-extra-exports` | A file exports only the names you allow. `patterns` accepts families such as `{Domain}[A-Z]\w*`. |
364
- | `only-export-constants` | Constant files export only local `const` values. [Details ↓](#only-export-constants) |
123
+ | [`declaration-name`](docs/rules/arch/declaration-name.md) | Selected declarations start with a prefix taken from the filename, or match a pattern. |
124
+ | [`export-file-prefix`](docs/rules/arch/export-file-prefix.md) | Names start with the filename prefix. |
125
+ | [`export-name-pattern`](docs/rules/arch/export-name-pattern.md) | Export names (or every function and type) match a regular expression. |
126
+ | [`filename-export-name`](docs/rules/arch/filename-export-name.md) | Function names follow a template built from the filename. |
127
+ | [`no-extra-exports`](docs/rules/arch/no-extra-exports.md) | A file exports only the names you allow. |
128
+ | [`only-export-constants`](docs/rules/arch/only-export-constants.md) | Constant files export only local `const` values. |
365
129
 
366
- ### 🏭 Functions & factories
130
+ #### 🏭 Functions & factories
367
131
 
368
132
  | Rule | What it enforces |
369
133
  | --- | --- |
370
- | `require-file-factory` | The file defines the factory function its filename promises. |
371
- | `no-extra-factory-keys` | A factory returns only allowed keys. `requireKeys` makes keys mandatory; `keySets` sets keys per factory pattern. |
372
- | `require-object-params` | Functions take object parameters instead of long positional lists. `maxParams: 2` allows `(deps, params)`. |
373
- | `no-file-level-helpers` | No stray helper functions at module scope. |
374
- | `no-top-level-functions` | No top-level functions (and optionally no re-exports) in matched files. |
375
- | `no-trivial-functions` | No empty/pass-through functions. `mode: 'precise'` preserves transformations and checks forbidden helper names and renamed generic guards. |
376
- | `no-module-mutable-state` | No module-level `let`/`var`, which is shared across requests and tests. |
134
+ | [`require-file-factory`](docs/rules/arch/require-file-factory.md) | The file defines the factory function its filename promises. |
135
+ | [`no-extra-factory-keys`](docs/rules/arch/no-extra-factory-keys.md) | A factory returns only the keys you allow. |
136
+ | [`require-object-params`](docs/rules/arch/require-object-params.md) | Functions take object parameters instead of long positional lists. |
137
+ | [`no-file-level-helpers`](docs/rules/arch/no-file-level-helpers.md) | No stray helper functions at module scope. |
138
+ | [`no-top-level-functions`](docs/rules/arch/no-top-level-functions.md) | No top-level functions (and optionally no re-exports) in matched files. |
139
+ | [`no-trivial-functions`](docs/rules/arch/no-trivial-functions.md) | No empty or pass-through functions. |
140
+ | [`no-module-mutable-state`](docs/rules/arch/no-module-mutable-state.md) | No module-level `let`/`var`, which is shared across requests and tests. |
377
141
 
378
- ### 🔷 Types
142
+ #### 🔷 Types
379
143
 
380
144
  | Rule | What it enforces |
381
145
  | --- | --- |
382
- | `no-inline-types` | Function signatures use named types instead of inline `{ ... }` objects. [Details ↓](#no-inline-types) |
383
- | `no-type-declarations` | No type aliases or interfaces in matched files, so types live in one place. |
384
- | `no-runtime-in-types` | Type modules stay free of runtime code. Opt in to also ban runtime imports and re-exports. |
385
- | `no-imported-type-alias` | No exported aliases that only rename an imported type; generic instantiations such as `Selectable<Table>` remain allowed. |
386
- | `prefer-namespace-type-import` 🔧 | Long type-only named imports become a namespace import with qualified references. [Details ↓](#prefer-namespace-type-import) |
146
+ | [`no-inline-types`](docs/rules/arch/no-inline-types.md) | Function signatures use named types instead of inline `{ ... }` objects. |
147
+ | [`no-type-declarations`](docs/rules/arch/no-type-declarations.md) | No type aliases or interfaces in matched files, so types live in one place. |
148
+ | [`no-runtime-in-types`](docs/rules/arch/no-runtime-in-types.md) | Type modules stay free of runtime code. |
149
+ | [`no-imported-type-alias`](docs/rules/arch/no-imported-type-alias.md) | No exported aliases that only rename an imported type. |
150
+ | [`prefer-namespace-type-import`](docs/rules/arch/prefer-namespace-type-import.md) 🔧 | Long type-only named imports become a namespace import with qualified references. |
387
151
 
388
- ### 🧪 Schemas & APIs
152
+ #### 🧪 Schemas & APIs
389
153
 
390
154
  | Rule | What it enforces |
391
155
  | --- | --- |
392
- | `no-local-schema-construction` | No schemas built outside the files that own them. |
393
- | `no-inline-schema-elements` | Named schemas inside combinators like `z.array(...)`. |
394
- | `no-rederive-schema` | No re-deriving types from schemas imported from elsewhere. |
395
- | `no-single-use-scalar-schema` | Scalar Zod aliases used once get inlined. |
396
- | `require-orpc-output` | oRPC procedures declare a named `.output()` schema. |
397
- | `no-unescaped-like` | `LIKE` / `ILIKE` values pass through your sanitizer; optional `operatorMethods` covers calls such as `.where(column, 'like', value)`. |
156
+ | [`no-local-schema-construction`](docs/rules/arch/no-local-schema-construction.md) | No schemas built outside the files that own them. |
157
+ | [`no-inline-schema-elements`](docs/rules/arch/no-inline-schema-elements.md) | Named schemas inside combinators like `z.array(...)`. |
158
+ | [`no-rederive-schema`](docs/rules/arch/no-rederive-schema.md) | No re-deriving types from schemas imported from elsewhere. |
159
+ | [`no-single-use-scalar-schema`](docs/rules/arch/no-single-use-scalar-schema.md) | Reports scalar Zod aliases that are used only once. |
160
+ | [`require-orpc-output`](docs/rules/arch/require-orpc-output.md) | oRPC procedures declare a named `.output()` schema. |
161
+ | [`no-unescaped-like`](docs/rules/arch/no-unescaped-like.md) | `LIKE` / `ILIKE` values pass through your sanitizer. |
398
162
 
399
- ### 🚧 Boundaries & safety
163
+ #### 🚧 Boundaries & safety
400
164
 
401
165
  | Rule | What it enforces |
402
166
  | --- | --- |
403
- | `no-restricted-token` | Identifiers or member calls appear only in owner files. `member: '*.insertInto'` keeps writes out of query files. [Details ↓](#no-restricted-token) |
404
- | `no-restricted-constructor` | No `new Error()` (or any constructor you list) where typed errors are required. |
405
- | `require-paired-call` | Calling one function requires calling another, for example `useForm` with a schema resolver. |
406
- | `no-promise-all-mutation` | No `results.push(...)` or `total += x` on outer variables inside `Promise.all`. Return values instead. |
407
- | `no-literal-in` | No `'key' in value`. Use `Object.hasOwn()` or a discriminated union. |
167
+ | [`no-restricted-token`](docs/rules/arch/no-restricted-token.md) | Identifiers or member calls appear only in owner files. |
168
+ | [`no-restricted-constructor`](docs/rules/arch/no-restricted-constructor.md) | No `new Error()` (or any constructor you list) where typed errors are required. |
169
+ | [`require-paired-call`](docs/rules/arch/require-paired-call.md) | Calling one function requires calling another, for example `useForm` with a schema resolver. |
170
+ | [`no-promise-all-mutation`](docs/rules/arch/no-promise-all-mutation.md) | No `results.push(...)` or `total += x` on outer variables inside `Promise.all`. Return values instead. |
171
+ | [`no-literal-in`](docs/rules/arch/no-literal-in.md) | No `'key' in value`. Use `Object.hasOwn()` or a discriminated union. |
408
172
 
409
- ### ⚛️ React
173
+ #### ⚛️ React
410
174
 
411
175
  | Rule | What it enforces |
412
176
  | --- | --- |
413
- | `component-props` | Components type props with `interface XProps` declared directly above them; pass-through components may use `ComponentProps<'tag'>`. [Details ↓](#component-props) |
414
- | `only-export-components` | Component files export only components. `denyTypePattern: 'Props$'` keeps props private. |
415
- | `route-surface` | Route files export the route only, with no banned hooks or raw JSX. |
177
+ | [`component-props`](docs/rules/arch/component-props.md) | Components type props with `interface XProps` declared directly above them. |
178
+ | [`only-export-components`](docs/rules/arch/only-export-components.md) | Component files export only components, named after the file. |
179
+ | [`route-surface`](docs/rules/arch/route-surface.md) | Route files export only the route, with no banned hooks or raw JSX. |
416
180
 
417
- ### 💬 Comments & tests
181
+ #### 💬 Comments & tests
418
182
 
419
183
  | Rule | What it enforces |
420
184
  | --- | --- |
421
- | `no-comments` 🔧 | No comments in matched files. Directives, bundler annotations, and `Why:` notes are kept; JSX comments and comments next to `disable-next-line` are reported without an autofix. |
422
- | `no-member-comments` 🔧 | No comments on interface, type, and object members. [Details ↓](#no-member-comments) |
423
- | `test-title-pattern` | Test titles match your rules, for example no leading "should". |
185
+ | [`no-comments`](docs/rules/arch/no-comments.md) 🔧 | No comments in matched files, except directives and `Why:` notes. |
186
+ | [`no-member-comments`](docs/rules/arch/no-member-comments.md) 🔧 | No comments on interface, type, and object members. |
187
+ | [`test-title-pattern`](docs/rules/arch/test-title-pattern.md) | Test titles match your rules, for example no leading "should". |
424
188
 
425
- ### 🎨 Layout
189
+ #### 🎨 Layout
426
190
 
427
191
  | Rule | What it enforces |
428
192
  | --- | --- |
429
- | `padding-between-statements` 🔧 | Blank lines around functions and classes, around control flow, and before `return` in longer blocks; `multilineVariables: true` also separates multiline declarations, including exports (default `false`). |
430
- | `object-multiline` 🔧 | Objects with 3+ properties passed to a call go one property per line (`scope: 'all'` for every object). |
431
- | `key-value-same-line` 🔧 | An object key and the start of its value stay on the same line. |
432
- | `chain-newline` 🔧 | Long method chains go one call per line. You pick which chains with `groups`. |
433
- | `call-array-multiline` 🔧 | Arrays passed to `Promise.all` (or callees you list) go one element per line. |
434
- | `jsx-attributes-multiline` 🔧 | JSX tags with 3+ attributes go one attribute per line. |
193
+ | [`padding-between-statements`](docs/rules/arch/padding-between-statements.md) 🔧 | Blank lines around functions, classes, control flow and late returns. |
194
+ | [`object-multiline`](docs/rules/arch/object-multiline.md) 🔧 | Objects with 3+ properties passed to a call go one property per line. |
195
+ | [`ternary-layout`](docs/rules/arch/ternary-layout.md) 🔧 | Join short conditionals or split long ones before `?` and `:`. |
196
+ | [`call-arguments-multiline`](docs/rules/arch/call-arguments-multiline.md) 🔧 | Calls and constructors with 3+ arguments go one argument per line. |
197
+ | [`key-value-same-line`](docs/rules/arch/key-value-same-line.md) 🔧 | An object key and the start of its value stay on the same line. |
198
+ | [`chain-newline`](docs/rules/arch/chain-newline.md) 🔧 | Long method chains go one call per line. |
199
+ | [`call-array-multiline`](docs/rules/arch/call-array-multiline.md) 🔧 | Arrays passed to `Promise.all` (or callees you list) go one element per line. |
200
+ | [`jsx-attributes-multiline`](docs/rules/arch/jsx-attributes-multiline.md) 🔧 | JSX tags with 3+ attributes go one attribute per line. |
435
201
 
436
202
  > [!WARNING]
437
- > Oxfmt and Prettier collapse short chains, arrays, and JSX tags back onto one line. Don't run `chain-newline`, `call-array-multiline`, or `jsx-attributes-multiline` on files a formatter also rewrites, or they will undo each other forever.
438
-
439
- ---
440
-
441
- ## 🔍 Rule details
442
-
443
- ### File size
444
-
445
- `limits.maxFileLines?: number` sets the existing ESLint `max-lines` ceiling on checked safety scopes; default `400`. Blank lines and comments remain skipped. Use native Oxlint overrides or the existing `ruleExclusions` configuration for deliberate per-file exceptions. File size is a ceiling, not a cohesion proof.
446
-
447
- ### TanStack runtime boundaries
448
-
449
- `modules/tanstack-runtime` checks every configured web root's services and lib. Server dependencies include Node builtins (bare and `node:`), Bun/Cloudflare modules, `@orpc/server`, configured backend server/RPC APIs, `.server` files, TanStack server APIs/markers and React DOM server rendering. Known RSC renderer imports are checked by symbol; the entire mixed RSC package is not banned. Pure public backend client/schema imports remain governed by existing architecture rules.
450
-
451
- `.client.ts` is the preset's frontend-facing, potentially SSR-used service convention. Known server runtime dependencies require inline callbacks of scope-resolved imports of `createServerFn().handler`, `createServerOnlyFn`, `createMiddleware().server` or `createIsomorphicFn().server`. Alias/namespace imports are recognized; lookalike names and shadowed parameters are not. Static isomorphic server imports are allowed only in the actual configured RPC owner, and every runtime reference must stay in its server branch. Elsewhere that branch can load a literal dynamic import. Module-level execution and re-exports do not inherit the exception.
452
-
453
- `.server` files may statically import server implementation; dynamic loading alone proves nothing. RSC permits JSX, React and server-component definitions, but server renderer/dependency references still require a server callback. Server/RSC modules reject browser globals, local hook imports, client-only markers/APIs and React/TanStack client hooks outside recognized client branches. Global checks respect lexical shadowing and direct `globalThis.name` / literal-key access. Shared utilities/constants/lib cannot launder known server or client-only imports. Type-only imports/re-exports erase at runtime but retain all architecture restrictions.
454
-
455
- `tanstackStart.additionalServerOnlyImports` and `additionalClientOnlyImports` add package/module prefixes. `tanstackStart.computedImportAllowedFiles` permits computed imports only in explicitly named project-relative files; review all targets before using it. Other checks still run. This is direct syntax and lexical-reference enforcement, not an import graph, interprocedural alias analysis, sandbox, or proof of SSR safety. Wrapper factories, indirect callback references, re-exported framework factories and arbitrary computed member aliases are not recognized as boundary proofs. Native rule exclusions remain explicit consumer decisions.
456
-
457
- TanStack's [environment functions](https://tanstack.com/start/latest/docs/framework/react/guide/environment-functions) remove opposite branches, and [import protection](https://tanstack.com/start/latest/docs/framework/react/guide/import-protection) checks the resulting graph. Start's default import protection treats `.client.*` as browser-only, unlike this preset's service convention: SSR consumers must deliberately configure its server file pattern (Orbs uses `server.files: []`). Keep build-side protection enabled for server dependencies; lint does not replace it.
458
-
459
- ### Private domain helpers and precise wrapper detection
460
-
461
- The preset requires all named functions in `room.utils.ts`, exported or private, to start with `room`, such as `roomUserIsOwner`. Private action/query helpers use the same domain prefix; the exported operation must exactly match its filename (`room-action.send-message.ts` → `roomActionSendMessage`). Ordinary local variables are unaffected. Standalone `export-file-prefix` users opt in with `allFunctions: true`.
462
-
463
- The preset enables `no-trivial-functions` with `mode: 'precise'`; standalone defaults retain legacy behavior. Precise mode rejects empty/constant/identity functions and unchanged-argument forwarding. Transformed arguments, query chains and interpolated prompts remain allowed. Nested generic runtime guards are checked too.
464
-
465
- Default exact banned names: `isRecord`, `isPlainObject`, `isObject`, `isString`, `isNumber`, `isBoolean`, `isArray`, `asRecord`, `asArray`. `bannedNames` replaces the list; `[]` disables name checks. `checkGenericGuards: false` disables structural checks. `allowPattern` exempts named helpers; `allowCallees` and `allowAsync` exempt wrappers only. No usage-count threshold, cross-file analysis or automatic rewrite is used.
466
-
467
- ### `component-props`
468
-
469
- Every top-level React component with props, exported or local, must use exactly `interface XProps`, declared directly above the component with only blank lines between. A file with `ProbeCard` and `ProbeCardInline` declares `ProbeCardProps` above `ProbeCard` and `ProbeCardInlineProps` above `ProbeCardInline`. Inline object types always fail. Components without parameters pass. `memo` and `forwardRef` are unwrapped, and `forwardRef<Ref, XProps>` type arguments count as the props type. Hooks and functions that do not return JSX are ignored.
470
-
471
- A component that adds no props of its own may take a plain `ComponentProps<'header'>` or `ComponentProps<typeof Button>` instead of an empty `interface XProps extends ComponentProps<'header'> {}`. Once it adds, omits or intersects anything, it needs `XProps`.
472
-
473
- | Option | Default | What it does |
474
- | --- | --- | --- |
475
- | `filePrefix` | `false` | Local components with props must start with the filename prefix, so `XProps` also matches prefix rules such as `declaration-name`. In `rooms-chat-tools.tsx`, rename `RoomsChatTool` to `RoomsChatToolsItem`. |
476
-
477
- ```ts
478
- // ✅ Pass
479
- interface ProbeCardProps extends ComponentProps<'button'> { tone: string }
480
-
481
- export function ProbeCard({ tone, ...rest }: ProbeCardProps) { return <button {...rest} /> }
482
-
483
- export function ProbeCardHeader(props: ComponentProps<'header'>) { return <header {...props} /> }
484
-
485
- // ❌ Fail: inline, wrong name, wrapped props
486
- export function ProbeCard({ title }: { title: string }) { return <h2>{title}</h2> }
487
- export function ProbeCard(props: ProbeCardOptions) { return <h2>{props.title}</h2> }
488
- export function ProbeCard(props: PropsWithChildren<ProbeCardProps>) { return props.children }
489
- ```
490
-
491
- There is no autofix: moving an interface is a code change, not whitespace. It replaces `no-inline-types` for component files, which would also flag inline types on event handlers and callbacks inside components and cannot require the `XProps` name. The preset enables `filePrefix`.
492
-
493
- ### `no-inline-types`
494
-
495
- Requires named types in function signatures: local functions, callbacks, methods, constructors, and overloads. It checks explicit annotations only; inferred types, variable annotations, and generic constraints are left alone.
496
-
497
- | Option | Default | What it does |
498
- | --- | --- | --- |
499
- | `parameters` | `true` | Check parameter annotations, including destructuring, defaults, rest, and constructor parameter properties. |
500
- | `returns` | `true` | Check explicit return annotations. |
501
- | `functionTypes` | `true` | Also reject inline function and constructor types. |
502
- | `minMembers` | `1` | Allow inline object types with fewer members than this. `1` also rejects `{}`. |
503
-
504
- Nested types count too, so `Promise<{ id: string }>` fails. An outer type is reported once, not again for each member inside it.
505
-
506
- ```ts
507
- // ✅ Pass
508
- function save(input: SaveInput): SaveResult { return persist(input) }
509
-
510
- // ❌ Fail
511
- function save(input: { id: string }): { ok: boolean } { return persist(input) }
512
- ```
513
-
514
- <details>
515
- <summary>More about <code>no-inline-types</code></summary>
516
-
517
- Use `no-type-declarations` separately to control where named types may live. With `functionTypes: false`, object types nested inside function types are still checked.
518
203
 
519
- </details>
204
+ ### `modules/*`
520
205
 
521
- ### `prefer-namespace-type-import`
206
+ Rules the preset uses to keep its policies from overwriting each other. You can also register them on their own.
522
207
 
523
- Prefers a namespace when a type-only import has **more than** `max` named specifiers. Both `import type { A, B }` and `import { type A, type B }` are checked; any runtime or default binding leaves the declaration alone.
524
-
525
- | Option | Default | What it does |
526
- | --- | --- | --- |
527
- | `max` | `3` | Maximum named specifiers allowed (non-negative integer). Three passes; four fails. |
528
- | `names` | `{}` | Exact module-path-to-namespace-name map, such as `{ '#/types/room.types': 'RoomTypes' }`. Values must be ASCII binding identifiers, not reserved words. |
529
-
530
- The default name is the last path segment in PascalCase, stripping JS/TS extensions and adding `Types` unless already present: `#/types/room.types` → `RoomTypes`, `./harness.types.ts` → `HarnessTypes`, `@orbs/server/client` → `ClientTypes`. Other punctuation separates words; names starting with a digit get a `Module` prefix.
531
-
532
- ```ts
533
- // Before
534
- import type { A, B as LocalB, C, D } from './room.types'
535
- export type Result = [A, LocalB, C, D]
536
-
537
- // After
538
- import type * as RoomTypes from './room.types'
539
- export type Result = [RoomTypes.A, RoomTypes.B, RoomTypes.C, RoomTypes.D]
540
- ```
541
-
542
- The autofix updates scope-resolved type references, generics, indexed access, type-position `typeof`, interface heritage, and class implementations, including TSX. Shadowed local names are untouched. Exported type declarations are updated, but local export lists (`export type { A }`, `export { type A }`, or `export { A }`) are **report-only**; direct re-exports from another module are untouched.
543
-
544
- The diagnostic explains why an autofix was skipped: comments inside the import, an existing namespace import from the same module (no merging), a namespace name colliding with any binding/reference in the file or another proposed namespace, an unsupported imported name, or a reference outside the supported type syntax. Multiple qualifying imports from the same module are report-only too. Choose distinct `names` overrides to resolve collisions between different modules. Fixes preserve the module string, import attributes, semicolon, and comments outside the import.
545
-
546
- ### `no-type-declarations`
547
-
548
- Rejects type aliases and interfaces in matched files, so types live in one place.
549
-
550
- | Option | Default | What it does |
551
- | --- | --- | --- |
552
- | `allowPattern` | none | Keep a declaration whose **name** matches. A pattern that starts with `=` or `^=` matches a type alias's **value** instead: `= ` followed by the value's source text, without surrounding parentheses or comments. Interfaces have no value, so only name patterns apply to them. |
553
-
554
- ```ts
555
- // allowPattern: ['(Service|Deps)$', '^= ReturnType<typeof ']
556
- export type AiRetry = ReturnType<typeof makeAiRetry> // ✅ value matches
557
- export interface OrderDeps { db: Database } // ✅ name matches
558
- export type OrderCreateParams = { id: string } // ❌ move it to your types file
559
- ```
560
-
561
- ### `only-export-constants`
562
-
563
- Every runtime export must point to a local `const`. Direct, destructured, renamed, and default exports of local constants all work.
564
-
565
- | Option | Default | What it does |
566
- | --- | --- | --- |
567
- | `allowFunctionValues` | `false` | Allow function-valued `const` bindings (not function declarations). |
568
- | `allowTypeExports` | `false` | Allow type aliases, interfaces, and type-only exports. |
569
- | `allowReExports` | `false` | Allow `export ... from`, `export *`, and exports of imported bindings. |
570
-
571
- ```ts
572
- // ✅ Pass
573
- const retryLimit = 3
574
- export { retryLimit }
575
-
576
- // ❌ Fail
577
- export let retryCount = 0
578
- export const retry = () => run()
579
- ```
580
-
581
- <details>
582
- <summary>More about <code>only-export-constants</code></summary>
583
-
584
- `let`, `var`, function, class, enum, and namespace declarations, class-valued constants, bare default expressions, and TypeScript `export =` / `export as namespace` are rejected. Name a default value with a local `const` first.
585
-
586
- This is a syntax rule, not a deep-immutability check. It recognizes direct function and class values, TypeScript expression wrappers, and local aliases. It does not follow values returned by calls, imports, member access, or destructuring. Objects and arrays do not need `as const` or `Object.freeze()`.
587
-
588
- </details>
589
-
590
- ### `no-restricted-token`
591
-
592
- Keeps identifiers and member calls inside the files that own them.
593
-
594
- ```ts
595
- 'arch/no-restricted-token': ['error', {
596
- restrictions: [
597
- { token: 'RouterClient', allowIn: ['/src/services/rpc/rpc.client.ts'] },
598
- { member: 'process.platform', allowIn: ['/src/runtime.ts'] },
599
- { member: '*.insertInto', message: 'Queries are read-only; move writes to an action.' },
600
- ],
601
- }]
602
- ```
603
-
604
- | Field | What it does |
605
- | --- | --- |
606
- | `token` | An identifier name to restrict. Type-only imports are ignored. |
607
- | `member` | A dotted member path such as `process.platform`. `*.name` matches any receiver, including `this.db` and chained calls. |
608
- | `allowIn` | Filename suffixes where the token is allowed. |
609
- | `allowPathPatterns` | Regular expressions for allowed paths. |
610
- | `message` | Extra guidance added to the report. |
611
-
612
- The older single form `{ token, allowIn }` still works.
613
-
614
- ### `no-member-comments`
615
-
616
- Removes comments on object-type members: interface properties and methods, index, call, and construct signatures, type-literal members, object literal properties, and spreads.
617
-
618
- | Kept ✅ | Removed ❌ |
619
- | --- | --- |
620
- | `Why: ...` notes (with `allowWhy: true`, the default) | Other leading and same-line trailing member comments |
621
- | Directives such as `@ts-expect-error`, `eslint-disable`, `prettier-ignore` | |
622
-
623
- <details>
624
- <summary>Autofix and scope details</summary>
625
-
626
- The autofix removes the comment and any blank line after it, drops the spaces before a trailing comment, and never removes the member itself. A run of stacked comments is removed by one fix, so `--fix` finishes in one pass.
627
-
628
- `Why:` detection checks the prefix (after whitespace or JSDoc decoration) and includes contiguous `//` continuation lines. Set `{ allowWhy: false }` to remove those too. Always-kept directives: `@ts-ignore`, `@ts-expect-error`, `@ts-nocheck`, `eslint-`/`oxlint-disable`/`enable`, `prettier-ignore`, `biome-ignore`, `c8`/`v8`/`istanbul ignore`, and webpack magic comments.
629
-
630
- Out of scope: declaration, function-body, class-field, enum-member, union-member, and plain parameter comments, plus comments inside destructuring patterns and array literals. For whole-file comment policy, use `no-comments`.
631
-
632
- </details>
633
-
634
- ---
635
-
636
- ## 🤝 Works well with anti-slop
637
-
638
- We recommend [Dillon Mulroy's anti-slop](https://github.com/dmmulroy/anti-slop) alongside this plugin:
639
-
640
- | Plugin | Catches |
208
+ | Rule | What it enforces |
641
209
  | --- | --- |
642
- | **anti-slop** | Low-evidence TypeScript and JavaScript patterns inside functions. |
643
- | **oxlint-plugin-arch** | File, export, boundary, API, and layout structure. |
644
-
645
- Register both in the same config; they use separate `anti-slop/*` and `arch/*` namespaces.
210
+ | [`backend-switch`](docs/rules/modules/backend-switch.md) | Do not use `switch` in backend application code. Use ts-pattern instead. |
211
+ | [`concurrent-db`](docs/rules/modules/concurrent-db.md) | Concurrent tests must not destructure a `db` fixture. Use an isolated database fixture instead. |
212
+ | [`domain-constants`](docs/rules/modules/domain-constants.md) | Move module data to a domain `.constants.ts` file. Do not keep SCREAMING_SNAKE constants (or configured data) in the service file. |
213
+ | [`double-negation`](docs/rules/modules/double-negation.md) | Do not coerce with `!!`. Use `Boolean(value)` or an explicit comparison. |
214
+ | [`dynamic-classes`](docs/rules/modules/dynamic-classes.md) | Write complete `className` strings. Do not build them with template expressions or `+`. |
215
+ | [`empty-effect`](docs/rules/modules/empty-effect.md) | Remove `useEffect` / `useLayoutEffect` callbacks whose body is empty. |
216
+ | [`import-boundaries`](docs/rules/modules/import-boundaries.md) | Reject imports that cross configured application boundaries, including type-only imports. |
217
+ | [`local-type-alias`](docs/rules/modules/local-type-alias.md) | Do not declare type aliases in component and hook files. Use an interface named for the owner, or move derived aliases to domain types. |
218
+ | [`memoization`](docs/rules/modules/memoization.md) | Do not import or call React `memo`, `useMemo` or `useCallback`. Use the React Compiler. Unrelated `cache.memo` calls are left alone. |
219
+ | [`next-runtime`](docs/rules/modules/next-runtime.md) | Check Next.js runtime ownership from file names and directives. `.server` / `.rsc` and `'use server'` modules stay off the browser. `.client` and `'use client'` modules stay off server-only APIs. Shared modules use neither. |
220
+ | [`no-unknown`](docs/rules/modules/no-unknown.md) | Do not write the `unknown` type. Use an owned domain type. |
221
+ | [`pascal-interface`](docs/rules/modules/pascal-interface.md) | Interface names must be PascalCase. |
222
+ | [`reasoned-directives`](docs/rules/modules/reasoned-directives.md) | Lint disables must name specific rules and explain why after `--`. |
223
+ | [`rpc-database`](docs/rules/modules/rpc-database.md) | RPC handlers must not call the database. Delegate that work to services. |
224
+ | [`service-functions`](docs/rules/modules/service-functions.md) | Keep backend operations strict. Frontend modules may expose several operations, with helpers nested inside them. |
225
+ | [`service-types`](docs/rules/modules/service-types.md) | Only the matching factory-derived `Service` alias may stay in a service file. Types such as `ChatDeps` belong in domain types. |
226
+ | [`shape-suffix`](docs/rules/modules/shape-suffix.md) | Do not name a declaration with a `Shape` suffix. Name it for its domain role. |
227
+ | [`tanstack-runtime`](docs/rules/modules/tanstack-runtime.md) | Check direct runtime dependencies and imported TanStack execution boundaries. Compilation still owns graph isolation. |
228
+ | [`test-assertions`](docs/rules/modules/test-assertions.md) | Every test case must contain a recognized assertion, and `expect` calls must sit inside a test or assertion helper. |
229
+ | [`test-modifiers`](docs/rules/modules/test-modifiers.md) | Reject focused, skipped and placeholder tests, including renamed imports such as `check.only`. |
230
+
231
+ ### `dillon-anti-slop/*`
232
+
233
+ 15 vendored [anti-slop](https://github.com/dmmulroy/anti-slop) rules by Dillon Mulroy. [List](docs/rules/dillon-anti-slop.md).
646
234
 
647
235
  ## ⚡ Benchmarks
648
236
 
@@ -654,8 +242,6 @@ Wall time for a full `oxlint` run over three real TypeScript monorepos, best of
654
242
  | Medium | 792 | 100.5k | 0.10s | **0.49s** | 0.69s |
655
243
  | Large | 1,880 | 190k | 0.10s | **0.72s** | 1.08s |
656
244
 
657
- <sub>Apple M5 Pro, Oxlint 1.78.0, default thread count. Most of the gap between "no rules" and the other columns is Oxlint's one-time cost of handing the AST to JavaScript plugins, paid once however many rules are on.</sub>
658
-
659
245
  ## 📄 License
660
246
 
661
247
  MIT