oxlint-plugin-arch 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -1
- package/README.md +92 -5
- package/dist/constants/test.constants.d.ts +1 -0
- package/dist/index.js +205 -83
- package/dist/presets/index.d.ts +1 -1
- package/dist/presets/index.js +79 -33
- package/dist/presets/tanstack-start-react-modules-preset.js +79 -33
- package/dist/rules/module-policy/index.js +321 -11
- package/dist/types/preset.types.d.ts +8 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.6.1
|
|
4
|
+
|
|
5
|
+
- Relax the preset's shadcn defaults for stock composition, so strict consumers need no overrides: exact padding groups on `CardContent`/`PopoverContent`, gap and padding on `TabsContent`, and gap on `Tabs`/`HoverCardContent`/`SidebarHeader`/`BreadcrumbList`; exact `truncate` everywhere. Controls such as `Button` stay protected.
|
|
6
|
+
- Allow safe-area padding values (`env(safe-area-inset-*)` and `max(--spacing(*),env(safe-area-inset-*))` on padding sides/axes) in `shadcn/no-arbitrary-values`. Literal floors such as `max(1rem,…)` stay rejected.
|
|
7
|
+
- Allow the stock Sonner hooks `toaster` and `toast` only in each web root's `components/ui/sonner.tsx`. No new preset options.
|
|
8
|
+
|
|
9
|
+
## 0.6.0
|
|
10
|
+
|
|
11
|
+
- Add `tests.profile` (`fixtures` by default, or `standard`), explicit custom test/assertion names, parameterized-test recognition and assertion-based narrowing guidance. Both profiles retain assertion, mock and type-safety checks; fixtures additionally require named fixture/support imports.
|
|
12
|
+
- Add explicit `modules.migrationFiles` scopes to exempt framework migration functions from service function/signature and folder-layout conventions without globally ignoring their safety checks.
|
|
13
|
+
- Recognize `test.for` titles and configured custom test modifiers; cover profiles and migration isolation with real Oxlint CLI regressions.
|
|
14
|
+
- Replace the preset’s native assertion-presence/placement checks with `modules/test-assertions`, covering native and extended tests, called local helpers and static table-row assertion callbacks. Require an assertion in every row; retain errors for standalone assertions, unused helpers and shadowed expectations. Custom assertion DSLs use `tests.additionalAssertionFunctions` or scoped module-rule options.
|
|
15
|
+
- Recognize polling/soft assertions without mistaking table builders or asymmetric matchers for assertions. Verify Tailwind fixes converge by file contents rather than assuming exit zero means an unchanged next pass.
|
|
16
|
+
- Add `arch/component-props`: every React component with props, exported or local, uses `interface XProps` declared directly above it; inline props always fail. Pass-through components may take a plain `ComponentProps<'tag'>` or `ComponentProps<typeof X>`. `filePrefix` requires local components to carry the file prefix. Enabled in the preset (with `filePrefix`) for `components/**/*.tsx`, excluding `components/ui` and tests; hooks, routes and non-component functions are unaffected. Opt out with `react: { componentProps: false }`, which leaves the other type-placement rules on.
|
|
17
|
+
|
|
18
|
+
## 0.5.0
|
|
4
19
|
|
|
5
20
|
- Replace the preset's `eslint-plugin-better-tailwindcss` dependency with `oxlint-tailwindcss` 1.14.0. Preserve seven Tailwind checks, per-root CSS themes and root font size, with shadcn retaining unknown-class and component checks. Consumer rule overrides must use the new `tailwindcss/*` IDs documented in the README. Add real autofix/idempotence coverage; deprecated classes now belong to the dedicated deprecation diagnostic.
|
|
6
21
|
|
package/README.md
CHANGED
|
@@ -103,12 +103,72 @@ Available policies: `typePlacement`, `typeSafety`, `serviceStructure`, `fileLayo
|
|
|
103
103
|
|
|
104
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
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. `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.
|
|
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
107
|
|
|
108
|
-
Shadcn allows caller-owned layout
|
|
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.
|
|
109
121
|
|
|
110
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.
|
|
111
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
|
+
|
|
112
172
|
### Exceptions and native customization
|
|
113
173
|
|
|
114
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.
|
|
@@ -166,7 +226,7 @@ Oxlint reads `typeAware` from the root config only. The direct `export default p
|
|
|
166
226
|
### Application conventions
|
|
167
227
|
|
|
168
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.
|
|
169
|
-
- 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. Other types belong in domain `types/*.types.ts` files.
|
|
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.
|
|
170
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.
|
|
171
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.
|
|
172
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.
|
|
@@ -237,7 +297,7 @@ export default defineConfig({
|
|
|
237
297
|
| --- | --- |
|
|
238
298
|
| [`minimal.oxlint.config.ts`](examples/minimal.oxlint.config.ts) | You want the smallest possible starting point. |
|
|
239
299
|
| [`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. |
|
|
240
|
-
| [`full.oxlint.config.ts`](examples/full.oxlint.config.ts) | You want to see every one of the
|
|
300
|
+
| [`full.oxlint.config.ts`](examples/full.oxlint.config.ts) | You want to see every one of the 44 rules with its options. |
|
|
241
301
|
|
|
242
302
|
> [!TIP]
|
|
243
303
|
> `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.
|
|
@@ -246,7 +306,7 @@ export default defineConfig({
|
|
|
246
306
|
|
|
247
307
|
## 🧩 Rules
|
|
248
308
|
|
|
249
|
-
|
|
309
|
+
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.
|
|
250
310
|
|
|
251
311
|
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.
|
|
252
312
|
|
|
@@ -316,6 +376,7 @@ Options that take one regex string (`pattern`, `allowPattern`, `forbid`, `requir
|
|
|
316
376
|
|
|
317
377
|
| Rule | What it enforces |
|
|
318
378
|
| --- | --- |
|
|
379
|
+
| `component-props` | Components type props with `interface XProps` declared directly above them; pass-through components may use `ComponentProps<'tag'>`. [Details ↓](#component-props) |
|
|
319
380
|
| `only-export-components` | Component files export only components. `denyTypePattern: 'Props$'` keeps props private. |
|
|
320
381
|
| `route-surface` | Route files export the route only, with no banned hooks or raw JSX. |
|
|
321
382
|
|
|
@@ -369,6 +430,32 @@ The preset enables `no-trivial-functions` with `mode: 'precise'`; standalone def
|
|
|
369
430
|
|
|
370
431
|
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.
|
|
371
432
|
|
|
433
|
+
### `component-props`
|
|
434
|
+
|
|
435
|
+
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.
|
|
436
|
+
|
|
437
|
+
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`.
|
|
438
|
+
|
|
439
|
+
| Option | Default | What it does |
|
|
440
|
+
| --- | --- | --- |
|
|
441
|
+
| `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`. |
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
// ✅ Pass
|
|
445
|
+
interface ProbeCardProps extends ComponentProps<'button'> { tone: string }
|
|
446
|
+
|
|
447
|
+
export function ProbeCard({ tone, ...rest }: ProbeCardProps) { return <button {...rest} /> }
|
|
448
|
+
|
|
449
|
+
export function ProbeCardHeader(props: ComponentProps<'header'>) { return <header {...props} /> }
|
|
450
|
+
|
|
451
|
+
// ❌ Fail: inline, wrong name, wrapped props
|
|
452
|
+
export function ProbeCard({ title }: { title: string }) { return <h2>{title}</h2> }
|
|
453
|
+
export function ProbeCard(props: ProbeCardOptions) { return <h2>{props.title}</h2> }
|
|
454
|
+
export function ProbeCard(props: PropsWithChildren<ProbeCardProps>) { return props.children }
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
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`.
|
|
458
|
+
|
|
372
459
|
### `no-inline-types`
|
|
373
460
|
|
|
374
461
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const testAssertionFunctions: string[];
|