oxlint-plugin-arch 0.2.4 → 0.3.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.
Files changed (4) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +264 -66
  3. package/dist/index.js +2498 -257
  4. package/package.json +4 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - Add `no-promise-all-mutation`, `no-literal-in`, and `test-title-pattern` to replace hand-written `no-restricted-syntax` selectors with scope-aware, configurable rules.
6
+ - Add `no-module-mutable-state`, `no-restricted-constructor`, and `no-comments` (directives, bundler annotations, and `Why:` comments kept; one-pass autofix that skips JSX comments and directive-adjacent lines).
7
+ - Add layout rules `padding-between-statements`, `object-multiline`, `key-value-same-line`, `chain-newline`, `call-array-multiline`, and `jsx-attributes-multiline`. Fixes only change whitespace and are skipped when comments or unusual syntax make them unsafe.
8
+ - Extend `no-extra-exports` with regex `patterns`, `no-restricted-token` with multiple `restrictions` and `member` matching, and `require-paired-call` with `pairs`.
9
+ - Extend `require-object-params` with `maxParams`, `allDeclarations`, and `allowPattern`; `only-export-components` with `allowTypeExports` and `denyTypePattern`; and `no-extra-factory-keys` with `requireKeys` and `keySets`.
10
+ - Extend `no-runtime-in-types` with opt-in `runtimeImports`, `allowImportSources`, and `banReExports`; `no-trivial-functions` with `allowCallees` and `allowAsync`; and `no-inline-types` with `minMembers`.
11
+ - All option changes are backward compatible; existing configs keep today's behavior.
12
+ - Speed up `declaration-name`, `no-type-declarations`, `no-inline-types`, and `no-promise-all-mutation` by using native visitors and per-file option setup instead of whole-AST JavaScript walks.
13
+ - Add `examples/monorepo.oxlint.config.ts`, a strict copy-ready config for `apps/server` + `apps/web` monorepos.
14
+ - Remove the `install-oxlint-arch` agent skill and vendored copy; install the npm package instead.
15
+
16
+ ## 0.2.5
17
+
18
+ - Add `no-inline-types` to require named object and function types in function signatures, with separate parameter, return, and function-type options.
19
+ - Add `only-export-constants` to restrict exports to local `const` bindings, with opt-ins for function values, type exports, and re-exports.
20
+ - Add `no-member-comments` to remove comments on object/type members while preserving directives and optional `Why:` explanations.
21
+ - Document the new rule options and syntax-only limits, extend the full configuration example to all 30 rules, and sync the optional vendored skill assets.
22
+
3
23
  ## 0.2.4
4
24
 
5
25
  - Add `trailingRoles` and `roleSeparators` so `export-file-prefix`, `declaration-name`, and `no-extra-exports` can treat `onchain-utils.ts` like `onchain.utils.ts`.
package/README.md CHANGED
@@ -1,16 +1,26 @@
1
- # oxlint-plugin-arch
1
+ # 🏛️ oxlint-plugin-arch
2
2
 
3
- Configurable, filename-aware architecture rules for Oxlint.
3
+ > Architecture rules for [Oxlint](https://oxc.rs), driven by filenames and fully configurable.
4
4
 
5
- Install the package, then put repository paths and policy in your Oxlint config. The npm package ships compiled JavaScript; `src/` stays TypeScript for development. Rules contain no application-specific names or folders.
5
+ Keep a codebase shaped the way you designed it: files in the right folders, exports named after their file, types where they belong, and no shortcuts across boundaries. Every rule is generic, so **you** decide the paths, names, and policy in your own Oxlint config.
6
6
 
7
- ## Install
7
+ - 🗂️ **Filename-aware.** Rules derive expected names from the file they run on (`user-action.create.ts` → `makeUserActionCreate`).
8
+ - 🎛️ **Configurable.** No app names, folders, or libraries are baked in. Globs and policy live in your config.
9
+ - 🛟 **Safe autofix.** Fixable rules only touch whitespace or comments, and never guess when a fix could change your code.
10
+ - ⚡ **Fast.** Built on Oxlint's JS plugin API and ESTree, with no extra parser.
11
+
12
+ ---
13
+
14
+ ## 📦 Install
8
15
 
9
16
  ```bash
10
17
  bun add -d oxlint-plugin-arch oxlint @oxlint/plugins
11
18
  ```
12
19
 
13
- Use matching `oxlint` and `@oxlint/plugins` versions. Then register the package:
20
+ > [!NOTE]
21
+ > Keep `oxlint` and `@oxlint/plugins` on matching versions.
22
+
23
+ Register the plugin in `oxlint.config.ts`:
14
24
 
15
25
  ```ts
16
26
  import { defineConfig } from 'oxlint'
@@ -22,82 +32,270 @@ export default defineConfig({
22
32
  })
23
33
  ```
24
34
 
25
- ## Recommended companion: anti-slop
35
+ ## 🚀 Quick start
26
36
 
27
- We recommend [Dillon Mulroy's anti-slop](https://github.com/dmmulroy/anti-slop) alongside this plugin. Anti-slop catches low-evidence TypeScript and JavaScript implementation patterns; oxlint-plugin-arch enforces configurable file, export, boundary, and API structure.
37
+ Rules are switched on per folder with `overrides`. Here are three small ones:
28
38
 
29
- Register both in the same Oxlint config under their separate `anti-slop/*` and `arch/*` rule namespaces.
39
+ ```ts
40
+ export default defineConfig({
41
+ jsPlugins: [{ name: 'arch', specifier: 'oxlint-plugin-arch' }],
42
+ rules: {
43
+ 'arch/no-literal-in': 'error',
44
+ },
45
+ overrides: [
46
+ {
47
+ files: ['**/src/components/**/*.tsx'],
48
+ rules: {
49
+ 'arch/only-export-components': ['error', { matchFileName: true }],
50
+ },
51
+ },
52
+ {
53
+ files: ['**/*.test.ts'],
54
+ rules: {
55
+ 'arch/test-title-pattern': ['error', { forbid: '^should\\b', flags: 'i' }],
56
+ },
57
+ },
58
+ ],
59
+ })
60
+ ```
30
61
 
31
- ## Optional: vendor a local copy
62
+ ## 📚 Examples
32
63
 
33
- If you want to inspect or fork the rules in-tree, copy `src/index.ts`, `src/rules/`, and `src/utils/`, or use the agent skill:
64
+ | File | Use it when |
65
+ | --- | --- |
66
+ | [`minimal.oxlint.config.ts`](examples/minimal.oxlint.config.ts) | You want the smallest possible starting point. |
67
+ | [`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. |
68
+ | [`full.oxlint.config.ts`](examples/full.oxlint.config.ts) | You want to see every one of the 42 rules with its options. |
34
69
 
35
- ```bash
36
- npx skills add nikuscs/oxlint-plugin-arch --skill install-oxlint-arch
70
+ > [!TIP]
71
+ > `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.
72
+
73
+ ---
74
+
75
+ ## 🧩 Rules
76
+
77
+ 42 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.
78
+
79
+ ### 📁 Files & folders
80
+
81
+ | Rule | What it enforces |
82
+ | --- | --- |
83
+ | `filename-match` | Filenames match a pattern you configure. |
84
+ | `folder-prefix` | Filenames start with their parent folder name, or the folders after a root you choose. |
85
+ | `no-restricted-files` | Files matching a glob you forbid are reported, for example misplaced `*.types.ts`. |
86
+
87
+ ### 🏷️ Naming & exports
88
+
89
+ | Rule | What it enforces |
90
+ | --- | --- |
91
+ | `declaration-name` | Selected declarations start with a prefix taken from the filename, or match a pattern. |
92
+ | `export-file-prefix` | Export names (or every function and type) start with the filename prefix. `trailingRoles` treats `onchain-utils.ts` like `onchain.utils.ts`. |
93
+ | `export-name-pattern` | Export names (or every function and type) match a regular expression. |
94
+ | `filename-export-name` | Function names follow a template built from the filename. |
95
+ | `no-extra-exports` | A file exports only the names you allow. `patterns` accepts families such as `{Domain}[A-Z]\w*`. |
96
+ | `only-export-constants` | Constant files export only local `const` values. [Details ↓](#only-export-constants) |
97
+
98
+ ### 🏭 Functions & factories
99
+
100
+ | Rule | What it enforces |
101
+ | --- | --- |
102
+ | `require-file-factory` | The file defines the factory function its filename promises. |
103
+ | `no-extra-factory-keys` | A factory returns only allowed keys. `requireKeys` makes keys mandatory; `keySets` sets keys per factory pattern. |
104
+ | `require-object-params` | Functions take object parameters instead of long positional lists. `maxParams: 2` allows `(deps, params)`. |
105
+ | `no-file-level-helpers` | No stray helper functions at module scope. |
106
+ | `no-top-level-functions` | No top-level functions (and optionally no re-exports) in matched files. |
107
+ | `no-trivial-functions` | No empty or pass-through wrapper functions. `allowCallees` keeps intentional wrappers. |
108
+ | `no-module-mutable-state` | No module-level `let`/`var`, which is shared across requests and tests. |
109
+
110
+ ### 🔷 Types
111
+
112
+ | Rule | What it enforces |
113
+ | --- | --- |
114
+ | `no-inline-types` | Function signatures use named types instead of inline `{ ... }` objects. [Details ↓](#no-inline-types) |
115
+ | `no-type-declarations` | No type aliases or interfaces in matched files, so types live in one place. |
116
+ | `no-runtime-in-types` | Type modules stay free of runtime code. Opt in to also ban runtime imports and re-exports. |
117
+ | `no-imported-type-alias` | No exported aliases that only rename an imported type. |
118
+
119
+ ### 🧪 Schemas & APIs
120
+
121
+ | Rule | What it enforces |
122
+ | --- | --- |
123
+ | `no-local-schema-construction` | No schemas built outside the files that own them. |
124
+ | `no-inline-schema-elements` | Named schemas inside combinators like `z.array(...)`. |
125
+ | `no-rederive-schema` | No re-deriving types from schemas imported from elsewhere. |
126
+ | `no-single-use-scalar-schema` | Scalar Zod aliases used once get inlined. |
127
+ | `require-orpc-output` | oRPC procedures declare a named `.output()` schema. |
128
+ | `no-unescaped-like` | `LIKE` / `ILIKE` values pass through your sanitizer. |
129
+
130
+ ### 🚧 Boundaries & safety
131
+
132
+ | Rule | What it enforces |
133
+ | --- | --- |
134
+ | `no-restricted-token` | Identifiers or member calls appear only in owner files. `member: '*.insertInto'` keeps writes out of query files. [Details ↓](#no-restricted-token) |
135
+ | `no-restricted-constructor` | No `new Error()` (or any constructor you list) where typed errors are required. |
136
+ | `require-paired-call` | Calling one function requires calling another, for example `useForm` with a schema resolver. |
137
+ | `no-promise-all-mutation` | No `results.push(...)` or `total += x` on outer variables inside `Promise.all`. Return values instead. |
138
+ | `no-literal-in` | No `'key' in value`. Use `Object.hasOwn()` or a discriminated union. |
139
+
140
+ ### ⚛️ React
141
+
142
+ | Rule | What it enforces |
143
+ | --- | --- |
144
+ | `only-export-components` | Component files export only components. `denyTypePattern: 'Props$'` keeps props private. |
145
+ | `route-surface` | Route files export the route only, with no banned hooks or raw JSX. |
146
+
147
+ ### 💬 Comments & tests
148
+
149
+ | Rule | What it enforces |
150
+ | --- | --- |
151
+ | `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. |
152
+ | `no-member-comments` 🔧 | No comments on interface, type, and object members. [Details ↓](#no-member-comments) |
153
+ | `test-title-pattern` | Test titles match your rules, for example no leading "should". |
154
+
155
+ ### 🎨 Layout
156
+
157
+ | Rule | What it enforces |
158
+ | --- | --- |
159
+ | `padding-between-statements` 🔧 | Blank lines around functions and classes, around control flow, and before `return` in longer blocks. |
160
+ | `object-multiline` 🔧 | Objects with 3+ properties passed to a call go one property per line (`scope: 'all'` for every object). |
161
+ | `key-value-same-line` 🔧 | An object key and the start of its value stay on the same line. |
162
+ | `chain-newline` 🔧 | Long method chains go one call per line. You pick which chains with `groups`. |
163
+ | `call-array-multiline` 🔧 | Arrays passed to `Promise.all` (or callees you list) go one element per line. |
164
+ | `jsx-attributes-multiline` 🔧 | JSX tags with 3+ attributes go one attribute per line. |
165
+
166
+ > [!WARNING]
167
+ > 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.
168
+
169
+ ---
170
+
171
+ ## 🔍 Rule details
172
+
173
+ ### `no-inline-types`
174
+
175
+ 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.
176
+
177
+ | Option | Default | What it does |
178
+ | --- | --- | --- |
179
+ | `parameters` | `true` | Check parameter annotations, including destructuring, defaults, rest, and constructor parameter properties. |
180
+ | `returns` | `true` | Check explicit return annotations. |
181
+ | `functionTypes` | `true` | Also reject inline function and constructor types. |
182
+ | `minMembers` | `1` | Allow inline object types with fewer members than this. `1` also rejects `{}`. |
183
+
184
+ Nested types count too, so `Promise<{ id: string }>` fails. An outer type is reported once, not again for each member inside it.
185
+
186
+ ```ts
187
+ // ✅ Pass
188
+ function save(input: SaveInput): SaveResult { return persist(input) }
189
+
190
+ // ❌ Fail
191
+ function save(input: { id: string }): { ok: boolean } { return persist(input) }
37
192
  ```
38
193
 
39
- Then point `jsPlugins[].specifier` at the copied entry file instead of the package name.
40
-
41
- ## Configuration examples
42
-
43
- - [`examples/minimal.oxlint.config.ts`](examples/minimal.oxlint.config.ts) — three small rules showing plugin registration, glob ownership, and options.
44
- - [`examples/full.oxlint.config.ts`](examples/full.oxlint.config.ts) — all 27 rules across representative component, action, service, route, API, schema, and database scopes.
45
-
46
- Copy the shapes that match your repository; do not copy globs or naming policy blindly.
47
-
48
- ## Rules
49
-
50
- - `declaration-name` — require selected declarations to match a filename-derived prefix or a consumer pattern. Optional `trailingRoles` treat `onchain-utils.ts` like `onchain.utils.ts`.
51
- - `export-file-prefix` — require export names, or every function and type, to start with a filename-derived prefix, optionally singularized. Optional `trailingRoles` drop a configured role suffix from that prefix.
52
- - `export-name-pattern` — require export names, or every function and type, to match a configured regular expression.
53
- - `filename-export-name` — derive expected function names from filename templates, optionally including locals.
54
- - `filename-match` — require filenames to match a configured pattern.
55
- - `folder-prefix` — require filenames to start with their parent folder name, or the folders after a configured root.
56
- - `no-extra-exports` — restrict files to configured export templates. Optional `trailingRoles` apply to `{Domain}` the same way as the prefix rules.
57
- - `no-extra-factory-keys` — restrict direct factory return keys.
58
- - `no-file-level-helpers` — keep unapproved helpers out of module scope.
59
- - `no-imported-type-alias` — reject exported aliases that only rename imported types.
60
- - `no-inline-schema-elements` — require named schemas inside configured combinators.
61
- - `no-local-schema-construction` — reject runtime schema-library imports and local schema construction, with consumer-chosen severity, messages, and path exceptions.
62
- - `no-rederive-schema` — reject type derivation from configured imported schemas.
63
- - `no-restricted-files` — reject files selected by a consumer-owned forbidden glob.
64
- - `no-restricted-token` — restrict an identifier to configured owner paths.
65
- - `no-runtime-in-types` — keep selected type modules runtime-free.
66
- - `no-single-use-scalar-schema` — reject local scalar Zod aliases used once where they can be safely inlined.
67
- - `no-top-level-functions` — reject top-level functions and optional re-exports.
68
- - `no-trivial-functions` — reject empty or passthrough top-level functions.
69
- - `no-type-declarations` — reject type aliases and interfaces in matched files.
70
- - `no-unescaped-like` — require configured sanitizers for configured query methods.
71
- - `only-export-components` — allow only React component and type exports.
72
- - `require-file-factory` — derive and require a filename-based factory function.
73
- - `require-object-params` — require exported functions to use object-shaped parameters.
74
- - `require-orpc-output` — require named oRPC output schemas.
75
- - `require-paired-call` — require one configured call when another appears.
76
- - `route-surface` — constrain route exports, hooks, and intrinsic JSX.
77
-
78
- Every rule file includes plain-English behavior and examples. Rule tests under `src/tests/` show complete option shapes.
79
-
80
- ## Development
194
+ <details>
195
+ <summary>More about <code>no-inline-types</code></summary>
81
196
 
82
- ```bash
83
- bun install
84
- bun run check
197
+ Use `no-type-declarations` separately to control where named types may live. With `functionTypes: false`, object types nested inside function types are still checked.
198
+
199
+ </details>
200
+
201
+ ### `only-export-constants`
202
+
203
+ Every runtime export must point to a local `const`. Direct, destructured, renamed, and default exports of local constants all work.
204
+
205
+ | Option | Default | What it does |
206
+ | --- | --- | --- |
207
+ | `allowFunctionValues` | `false` | Allow function-valued `const` bindings (not function declarations). |
208
+ | `allowTypeExports` | `false` | Allow type aliases, interfaces, and type-only exports. |
209
+ | `allowReExports` | `false` | Allow `export ... from`, `export *`, and exports of imported bindings. |
210
+
211
+ ```ts
212
+ // ✅ Pass
213
+ const retryLimit = 3
214
+ export { retryLimit }
215
+
216
+ // ❌ Fail
217
+ export let retryCount = 0
218
+ export const retry = () => run()
85
219
  ```
86
220
 
87
- After production changes, run `bun run sync:skill-assets`; CI verifies the bundled installer copy matches `src/`.
221
+ <details>
222
+ <summary>More about <code>only-export-constants</code></summary>
88
223
 
89
- ## Release
224
+ `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.
90
225
 
91
- Update `CHANGELOG.md`, then run the full lifecycle from a clean `main`:
226
+ 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()`.
92
227
 
93
- ```bash
94
- bun run release # prompt for patch/minor/major
95
- bun run release:patch # check, bump, tag, push, npm publish
96
- bun run release:dry-run # check + npm pack dry-run only
228
+ </details>
229
+
230
+ ### `no-restricted-token`
231
+
232
+ Keeps identifiers and member calls inside the files that own them.
233
+
234
+ ```ts
235
+ 'arch/no-restricted-token': ['error', {
236
+ restrictions: [
237
+ { token: 'RouterClient', allowIn: ['/src/services/rpc.client.ts'] },
238
+ { member: 'process.platform', allowIn: ['/src/runtime.ts'] },
239
+ { member: '*.insertInto', message: 'Queries are read-only; move writes to an action.' },
240
+ ],
241
+ }]
97
242
  ```
98
243
 
99
- `scripts/release.sh` refuses dirty trees and requires `npm login`. `prepublishOnly` runs `bun run check` again before the tarball goes out.
244
+ | Field | What it does |
245
+ | --- | --- |
246
+ | `token` | An identifier name to restrict. Type-only imports are ignored. |
247
+ | `member` | A dotted member path such as `process.platform`. `*.name` matches any receiver, including `this.db` and chained calls. |
248
+ | `allowIn` | Filename suffixes where the token is allowed. |
249
+ | `allowPathPatterns` | Regular expressions for allowed paths. |
250
+ | `message` | Extra guidance added to the report. |
251
+
252
+ The older single form `{ token, allowIn }` still works.
253
+
254
+ ### `no-member-comments`
255
+
256
+ Removes comments on object-type members: interface properties and methods, index, call, and construct signatures, type-literal members, object literal properties, and spreads.
257
+
258
+ | Kept ✅ | Removed ❌ |
259
+ | --- | --- |
260
+ | `Why: ...` notes (with `allowWhy: true`, the default) | Other leading and same-line trailing member comments |
261
+ | Directives such as `@ts-expect-error`, `eslint-disable`, `prettier-ignore` | |
262
+
263
+ <details>
264
+ <summary>Autofix and scope details</summary>
265
+
266
+ 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.
267
+
268
+ `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.
269
+
270
+ 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`.
271
+
272
+ </details>
273
+
274
+ ---
275
+
276
+ ## 🤝 Works well with anti-slop
277
+
278
+ We recommend [Dillon Mulroy's anti-slop](https://github.com/dmmulroy/anti-slop) alongside this plugin:
279
+
280
+ | Plugin | Catches |
281
+ | --- | --- |
282
+ | **anti-slop** | Low-evidence TypeScript and JavaScript patterns inside functions. |
283
+ | **oxlint-plugin-arch** | File, export, boundary, API, and layout structure. |
284
+
285
+ Register both in the same config; they use separate `anti-slop/*` and `arch/*` namespaces.
286
+
287
+ ## ⚡ Benchmarks
288
+
289
+ Wall time for a full `oxlint` run over three real TypeScript monorepos, best of 5, including process startup.
290
+
291
+ | Project | Files | Lines | Plugin loaded, no rules | [Monorepo example](examples/monorepo.oxlint.config.ts) | All 42 rules on every file |
292
+ | --- | ---: | ---: | ---: | ---: | ---: |
293
+ | Small | 246 | 14.5k | 0.09s | **0.20s** | 0.23s |
294
+ | Medium | 792 | 100.5k | 0.10s | **0.49s** | 0.69s |
295
+ | Large | 1,880 | 190k | 0.10s | **0.72s** | 1.08s |
296
+
297
+ <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>
100
298
 
101
- ## License
299
+ ## 📄 License
102
300
 
103
301
  MIT