@adamaho/nopeus-oxlint-plugin 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/LICENSE +21 -0
- package/README.md +822 -0
- package/dist/base.d.mts +31 -0
- package/dist/base.d.mts.map +1 -0
- package/dist/base.mjs +31 -0
- package/dist/base.mjs.map +1 -0
- package/dist/effect.d.mts +67 -0
- package/dist/effect.d.mts.map +1 -0
- package/dist/effect.mjs +64 -0
- package/dist/effect.mjs.map +1 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2693 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
# @adamaho/nopeus-oxlint-plugin
|
|
2
|
+
|
|
3
|
+
Custom Oxlint rules for AI-assisted TypeScript codebases. The `base` preset
|
|
4
|
+
enables general TypeScript rules; `effect` adds Effect-specific syntax rules.
|
|
5
|
+
Both apply to production and test code.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
Install Nopeus and its Oxlint peer:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add --save-dev @adamaho/nopeus-oxlint-plugin oxlint
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Neither preset requires `effect`, `@effect/tsgo`, `@effect/language-service`,
|
|
16
|
+
`@effect/vitest`, or `oxlint-tsgolint` to be installed. Effect packages listed in
|
|
17
|
+
this repository's devDependencies are for developing and testing Nopeus; they
|
|
18
|
+
are not installed with the published plugin.
|
|
19
|
+
|
|
20
|
+
### General TypeScript rules
|
|
21
|
+
|
|
22
|
+
Use `@adamaho/nopeus-oxlint-plugin/base` in `oxlint.config.ts`:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import base from "@adamaho/nopeus-oxlint-plugin/base";
|
|
26
|
+
import { defineConfig } from "oxlint";
|
|
27
|
+
|
|
28
|
+
export default defineConfig({ extends: [base] });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This enables the general rules for type assertions, type widening, dictionaries,
|
|
32
|
+
parameters, reflection, module mocking, conditional spreads, and public JSDoc.
|
|
33
|
+
It does not enable Effect service, runtime, state-lifetime, or v4 migration rules.
|
|
34
|
+
No compiler patch or Effect-specific tsconfig is needed.
|
|
35
|
+
|
|
36
|
+
The separate `@adamaho/nopeus-oxlint-config` package selects built-in Oxlint rules.
|
|
37
|
+
It does not contain the custom rule implementations in this plugin. To combine
|
|
38
|
+
both, install that package too and use `extends: [builtins, base]`, importing
|
|
39
|
+
`builtins` from `@adamaho/nopeus-oxlint-config`.
|
|
40
|
+
|
|
41
|
+
### Effect syntax rules
|
|
42
|
+
|
|
43
|
+
The `effect` preset includes the general preset, so choose it when the project
|
|
44
|
+
uses Effect; there is no need to extend both plugin presets.
|
|
45
|
+
|
|
46
|
+
Extend the canonical policy from an oxlint.config.ts file:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import packageJson from "./package.json" with { type: "json" };
|
|
50
|
+
import effect from "@adamaho/nopeus-oxlint-plugin/effect";
|
|
51
|
+
import { defineConfig } from "oxlint";
|
|
52
|
+
|
|
53
|
+
export default defineConfig({
|
|
54
|
+
extends: [
|
|
55
|
+
effect({
|
|
56
|
+
packageName: packageJson.name,
|
|
57
|
+
runtimeEntryPoints: ["src/main.ts"],
|
|
58
|
+
}),
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The root package name defines the owned Effect service namespace. Both goho and
|
|
64
|
+
@adamaho/goho require trace names and service keys beginning with @goho/.
|
|
65
|
+
|
|
66
|
+
The package publishes compiled ESM and requires Node.js 22.18 or newer, or
|
|
67
|
+
Node.js 24 or newer.
|
|
68
|
+
|
|
69
|
+
## Rules
|
|
70
|
+
|
|
71
|
+
### nopeus/no-export-assignment
|
|
72
|
+
|
|
73
|
+
Rejects TypeScript's CommonJS `export = value` syntax; use named ESM exports or
|
|
74
|
+
`export default` instead. Enabled in both `/base` and `/effect`. The built-in
|
|
75
|
+
config supplies `typescript/no-require-imports` and `import/no-commonjs` for
|
|
76
|
+
CommonJS imports and JavaScript exports. The policy requires ESM source, not
|
|
77
|
+
ESM-only dependencies.
|
|
78
|
+
|
|
79
|
+
### nopeus/require-test-location
|
|
80
|
+
|
|
81
|
+
Test files belong under the nearest package's `test/` directory, alongside
|
|
82
|
+
`src/`, and use `.test` rather than `.spec` before their source extension.
|
|
83
|
+
The same policy applies to libraries and executable programs, regardless of
|
|
84
|
+
whether Oxlint runs from the repository root or a package directory.
|
|
85
|
+
|
|
86
|
+
Bad: `src/users/service.test.ts`, `tests/users.test.ts`, `test/users.spec.ts`.
|
|
87
|
+
|
|
88
|
+
Good: `test/users/service.test.ts`, `test/integration/users.test.ts`,
|
|
89
|
+
`test/e2e/server.test.ts`, `test/package/installation.test.ts`.
|
|
90
|
+
|
|
91
|
+
Module tests should mirror source paths, but the rule does not require a test
|
|
92
|
+
for each source file or prescribe feature folders. It recognizes `.test` and
|
|
93
|
+
`.spec` filenames with JS, JSX, TS, TSX, MJS, CJS, MTS, and CTS extensions;
|
|
94
|
+
it does not infer tests from arbitrary function calls. Files without a package
|
|
95
|
+
owner are skipped. Test helpers need no `.test` suffix.
|
|
96
|
+
|
|
97
|
+
### nopeus/no-test-imports
|
|
98
|
+
|
|
99
|
+
Files under a package's `src/` cannot import its `test/` resources, another
|
|
100
|
+
package's test resources, or legacy `tests/`, `__tests__`, and test/spec files.
|
|
101
|
+
This includes type-only imports, re-exports, literal dynamic imports, and
|
|
102
|
+
unshadowed `require` calls. Tests and root-level runner configs may use helpers.
|
|
103
|
+
|
|
104
|
+
Bad, in `src/users/service.ts`:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { usersLayer } from "../../test/helpers/users-test-layer.ts";
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Good, in `test/users/service.test.ts`:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { make } from "../../src/users/service.ts";
|
|
114
|
+
import { usersLayer } from "../helpers/users-test-layer.ts";
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Keep helpers under `test/helpers/` and inputs under `test/fixtures/` when they
|
|
118
|
+
need to be shared. Create those directories only when needed.
|
|
119
|
+
|
|
120
|
+
### nopeus/no-cross-package-internals
|
|
121
|
+
|
|
122
|
+
Cross-package imports must use the destination package's own name and public
|
|
123
|
+
entrypoints. Relative/absolute filesystem imports and TypeScript aliases cannot
|
|
124
|
+
reach into another package, even when the destination file is itself exported.
|
|
125
|
+
Tests retain access to their own package's private implementation.
|
|
126
|
+
|
|
127
|
+
Bad:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { authenticate } from "../../auth/src/internal/authenticate.ts";
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Good:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { authenticate } from "@example/auth";
|
|
137
|
+
import { session } from "@example/auth/session";
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Resolution uses `oxc-resolver`, including package export maps, conditional and
|
|
141
|
+
wildcard exports, symlinked workspaces, and automatically discovered tsconfig
|
|
142
|
+
paths. A cross-package alias is accepted only when it spells the destination's
|
|
143
|
+
public package entrypoint and resolves to the same file as that entrypoint.
|
|
144
|
+
Legacy packages without export maps may be imported through their root name;
|
|
145
|
+
deep imports require an explicit export map.
|
|
146
|
+
|
|
147
|
+
Manifests containing only `{"type":"module"}` or `{"type":"commonjs"}`
|
|
148
|
+
select a module format, not a separate package owner. Public exports into these
|
|
149
|
+
directories remain valid; other manifests still establish package boundaries.
|
|
150
|
+
|
|
151
|
+
The import rules inspect static imports/re-exports (including type-only forms),
|
|
152
|
+
TypeScript import types/import-equals, literal dynamic imports, and unshadowed
|
|
153
|
+
`require`. Computed module names, custom bundler-only aliases, and otherwise
|
|
154
|
+
unresolved bare imports remain outside this check; the compiler still owns
|
|
155
|
+
resolution errors. Resolution currently uses `types`, `import`, `node`, and
|
|
156
|
+
`default` conditions. It does not model arbitrary bundler custom conditions.
|
|
157
|
+
CommonJS syntax is rejected separately by the syntax policy; these boundary
|
|
158
|
+
checks do not attempt to resolve `require`-specific export conditions.
|
|
159
|
+
|
|
160
|
+
All three structure rules are errors in both `/base` and `/effect`. Include
|
|
161
|
+
both `src` and `test` in the consuming project's lint command. Narrowly exclude
|
|
162
|
+
deliberately invalid fixture projects, not ordinary test code. The built-in
|
|
163
|
+
config package supplies filename casing separately.
|
|
164
|
+
|
|
165
|
+
### nopeus/no-module-level-mutable-state
|
|
166
|
+
|
|
167
|
+
Rejects module-level `let`/`var` and direct construction of writable global
|
|
168
|
+
`Map`, `Set`, `WeakMap`, or `WeakSet` values. Service state belongs to construction.
|
|
169
|
+
|
|
170
|
+
Bad:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const cache = new Map<UserId, User>();
|
|
174
|
+
export const make = Effect.sync(() => buildUsers(cache));
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Good:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
export const make = Effect.sync(() => {
|
|
181
|
+
const cache = new Map<UserId, User>();
|
|
182
|
+
return buildUsers(cache);
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Immutable lookup tables can use an explicit readonly contract:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
const statusCodes: ReadonlyMap<string, number> = new Map([["ok", 200]]);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`satisfies ReadonlyMap` alone does not remove the mutable methods from an inferred
|
|
193
|
+
Map type and is not an exception. Imported persistent collection constructors
|
|
194
|
+
and function-local state remain allowed. This syntax-level rule does not infer
|
|
195
|
+
arbitrary factories, nested object state, clients, or mutation through aliases.
|
|
196
|
+
Explicitly owned process-wide mutable infrastructure needs a local exception.
|
|
197
|
+
|
|
198
|
+
In v4, separate `Effect.provide` calls can share memoized Layers. Construction
|
|
199
|
+
owns state per acquired service instance, not per provide call. Use `Layer.fresh`
|
|
200
|
+
or `Effect.provide(layer, { local: true })` only where isolation is intentional;
|
|
201
|
+
this rule does not require either mechanism.
|
|
202
|
+
|
|
203
|
+
### nopeus/require-fetch-abort-signal
|
|
204
|
+
|
|
205
|
+
Requires global `fetch` calls directly inside an `Effect.tryPromise` callback to
|
|
206
|
+
forward that callback's AbortSignal in a visible options object.
|
|
207
|
+
|
|
208
|
+
Bad:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
Effect.tryPromise({
|
|
212
|
+
try: () => fetch(url),
|
|
213
|
+
catch: toRequestFailed,
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Good:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
Effect.tryPromise({
|
|
221
|
+
try: (signal) => fetch(url, { ...requestOptions, signal }),
|
|
222
|
+
catch: toRequestFailed,
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Put `signal` after spreads and computed properties that could overwrite it.
|
|
227
|
+
`globalThis.fetch`, aliased Effect imports, renamed callback parameters, and
|
|
228
|
+
shadowed globals are handled. SDK methods and deferred nested callbacks are
|
|
229
|
+
outside this narrow rule. Prebuilt Request/options objects, combined signals,
|
|
230
|
+
and indirect signal aliases require an explicit local exception or forwarding
|
|
231
|
+
the callback parameter directly at the adapter boundary.
|
|
232
|
+
|
|
233
|
+
### nopeus/no-type-assertions
|
|
234
|
+
|
|
235
|
+
Rejects every non-const TypeScript assertion. A comment cannot prove a runtime
|
|
236
|
+
invariant, so the policy has no assertion escape hatch.
|
|
237
|
+
|
|
238
|
+
Bad:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
const user = input as User;
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Good:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
const decodeUser = Schema.decodeUnknownSync(User);
|
|
248
|
+
const user = decodeUser(input);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Effect SQL, Drizzle, and other typed data APIs should carry their result types
|
|
252
|
+
without assertions. Const assertions remain allowed.
|
|
253
|
+
|
|
254
|
+
### nopeus/no-conditional-empty-object-spread
|
|
255
|
+
|
|
256
|
+
Rejects object spreads that use an empty object as one side of a conditional.
|
|
257
|
+
The omission is easier to review when it is expressed as an explicit mutation
|
|
258
|
+
of an owned object.
|
|
259
|
+
|
|
260
|
+
Bad:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
const request = {
|
|
264
|
+
url,
|
|
265
|
+
...(token === undefined ? {} : { token }),
|
|
266
|
+
};
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Good:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
const request: RequestOptions = { url };
|
|
273
|
+
if (token !== undefined) {
|
|
274
|
+
request.token = token;
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### nopeus/no-known-value-widening
|
|
279
|
+
|
|
280
|
+
Rejects explicit broad annotations that discard evidence already present in a
|
|
281
|
+
literal, constructor, function, or stable const binding.
|
|
282
|
+
|
|
283
|
+
Bad:
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
const request: object = {
|
|
287
|
+
method: "GET",
|
|
288
|
+
url,
|
|
289
|
+
};
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Good:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
const request = {
|
|
296
|
+
method: "GET",
|
|
297
|
+
url,
|
|
298
|
+
} satisfies RequestOptions;
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Prefer inference or satisfies when the value is already known.
|
|
302
|
+
|
|
303
|
+
### nopeus/no-module-mocking
|
|
304
|
+
|
|
305
|
+
Rejects Vitest and Jest module mocking. Tests should replace dependencies
|
|
306
|
+
through the same interfaces and Effect Layers used by production code.
|
|
307
|
+
|
|
308
|
+
Bad:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
vi.mock("./users.ts", () => ({
|
|
312
|
+
findById: vi.fn(),
|
|
313
|
+
}));
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Good:
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
const UsersTest = Layer.succeed(Users)({
|
|
320
|
+
findById: () => Effect.succeed(testUser),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const program = loadUser("user-1").pipe(Effect.provide(UsersTest));
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### nopeus/no-object-parameters
|
|
327
|
+
|
|
328
|
+
Rejects the broad object type on function parameters, including aliases that
|
|
329
|
+
resolve to object. Inputs need a named owner contract.
|
|
330
|
+
|
|
331
|
+
Bad:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
function saveUser(user: object) {
|
|
335
|
+
return repository.save(user);
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Good:
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
interface SaveUserInput {
|
|
343
|
+
readonly id: UserId;
|
|
344
|
+
readonly name: string;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function saveUser(user: SaveUserInput) {
|
|
348
|
+
return repository.save(user);
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### nopeus/no-reflect-apply
|
|
353
|
+
|
|
354
|
+
Rejects Reflect.apply because it bypasses an ordinary typed function call.
|
|
355
|
+
|
|
356
|
+
Bad:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
const result = Reflect.apply(handler, receiver, argumentsList);
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Good:
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
const result = handler.call(receiver, request);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
When dispatch is genuinely dynamic, put it behind a named typed interface.
|
|
369
|
+
|
|
370
|
+
### nopeus/no-reflect-get
|
|
371
|
+
|
|
372
|
+
Rejects Reflect.get because it turns property access into unchecked dynamic
|
|
373
|
+
lookup.
|
|
374
|
+
|
|
375
|
+
Bad:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
const userId = Reflect.get(payload, "userId");
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Good:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
const payload = Schema.decodeUnknownSync(UserPayload)(input);
|
|
385
|
+
const userId = payload.userId;
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### nopeus/no-runtime-typeof
|
|
389
|
+
|
|
390
|
+
Rejects runtime typeof checks. Primitive narrowing proves only a JavaScript
|
|
391
|
+
representation, not the domain contract expected by the application.
|
|
392
|
+
|
|
393
|
+
Bad:
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
if (typeof input === "string") {
|
|
397
|
+
return input;
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Good:
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
const decodeName = Schema.decodeUnknownEffect(Name);
|
|
405
|
+
const name = yield * decodeName(input);
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Decode external input at its boundary, then branch on the decoded domain value.
|
|
409
|
+
Explicit type-guard and assertion-function bodies may use typeof because their
|
|
410
|
+
contract makes the narrowing boundary visible.
|
|
411
|
+
|
|
412
|
+
### nopeus/no-unknown-returns
|
|
413
|
+
|
|
414
|
+
Rejects explicit unknown, Promise of unknown, and local aliases that resolve to
|
|
415
|
+
unknown in function return contracts. Parsing responsibility belongs inside the
|
|
416
|
+
boundary function rather than with every caller.
|
|
417
|
+
|
|
418
|
+
Bad:
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
function parseUser(text: string): unknown {
|
|
422
|
+
return JSON.parse(text);
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Good:
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
function parseUser(text: string): User {
|
|
430
|
+
return Schema.decodeUnknownSync(User)(JSON.parse(text));
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### nopeus/no-unknown-type-aliases
|
|
435
|
+
|
|
436
|
+
Rejects aliases that merely hide unknown behind a domain-looking name.
|
|
437
|
+
|
|
438
|
+
Bad:
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
type UserPayload = unknown;
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Good:
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
const UserPayload = Schema.Struct({
|
|
448
|
+
id: UserId,
|
|
449
|
+
name: Schema.String,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
type UserPayload = typeof UserPayload.Type;
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Unknown should remain visible at the parsing boundary.
|
|
456
|
+
|
|
457
|
+
### nopeus/no-unsafe-dictionary-type
|
|
458
|
+
|
|
459
|
+
Rejects dictionaries whose direct value type is unknown, any, object, an empty
|
|
460
|
+
object type, or an alias or union containing one of those escape hatches.
|
|
461
|
+
|
|
462
|
+
Bad:
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
type UsersById = Record<string, unknown>;
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Good:
|
|
469
|
+
|
|
470
|
+
```ts
|
|
471
|
+
type UsersById = Readonly<Record<UserId, User>>;
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Use a concrete owner type for dictionary values.
|
|
475
|
+
|
|
476
|
+
### nopeus/no-effect-runners-in-library
|
|
477
|
+
|
|
478
|
+
Rejects Effect runtime runners outside the files listed in
|
|
479
|
+
`runtimeEntryPoints`. Library modules should return Effects so callers retain
|
|
480
|
+
control of runtime configuration, interruption, and observability. Entrypoints
|
|
481
|
+
are exact repository-relative paths; `src/main.ts` does not allow a nested
|
|
482
|
+
`packages/example/src/main.ts`.
|
|
483
|
+
|
|
484
|
+
Bad outside an entrypoint:
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
export const loadUsers = () => Effect.runPromise(Users.all);
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Good:
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
export const loadUsers = Users.all;
|
|
494
|
+
|
|
495
|
+
// src/main.ts, configured as a runtime entrypoint
|
|
496
|
+
Effect.runPromise(loadUsers);
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### nopeus/no-fallible-effect-promise
|
|
500
|
+
|
|
501
|
+
Rejects Effect.promise. The canonical policy treats every external Promise as
|
|
502
|
+
potentially rejecting; Effect.tryPromise keeps rejection in the typed error
|
|
503
|
+
channel instead of turning it into a defect.
|
|
504
|
+
|
|
505
|
+
Bad:
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
const response = Effect.promise(() => fetch(url));
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Good:
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
const response = Effect.tryPromise({
|
|
515
|
+
try: () => fetch(url),
|
|
516
|
+
catch: (cause) => new RequestFailed({ cause }),
|
|
517
|
+
});
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### nopeus/no-inline-live-layer
|
|
521
|
+
|
|
522
|
+
Rejects live Layer constructors inside Effect.provide, including effectful and
|
|
523
|
+
context constructors nested in Layer composition. Build stable live Layers at
|
|
524
|
+
module scope and provide them at a composition boundary.
|
|
525
|
+
|
|
526
|
+
Bad:
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
const program = load.pipe(Effect.provide(Layer.effect(Users, makeUsers)));
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Good:
|
|
533
|
+
|
|
534
|
+
```ts
|
|
535
|
+
export const usersLayer = Layer.effect(Users, makeUsers);
|
|
536
|
+
|
|
537
|
+
const program = load.pipe(Effect.provide(usersLayer));
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### nopeus/no-unscoped-fork
|
|
541
|
+
|
|
542
|
+
Rejects Effect.forkDetach. Background work needs an explicit lifetime so
|
|
543
|
+
shutdown and interruption remain structured.
|
|
544
|
+
|
|
545
|
+
Bad:
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
yield * Effect.forkDetach(refreshCache);
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Good:
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
yield * Effect.forkScoped(refreshCache);
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
Use Effect.forkIn when an existing Scope should own the fiber.
|
|
558
|
+
|
|
559
|
+
### nopeus/no-untyped-effect-errors
|
|
560
|
+
|
|
561
|
+
Rejects primitive values, object literals, and built-in error classes in
|
|
562
|
+
Effect.fail. This is a syntax-level rule: identifiers and custom error classes
|
|
563
|
+
remain valid, while the common ways of erasing domain error information are
|
|
564
|
+
rejected.
|
|
565
|
+
|
|
566
|
+
Bad:
|
|
567
|
+
|
|
568
|
+
```ts
|
|
569
|
+
yield * Effect.fail("user not found");
|
|
570
|
+
yield * Effect.fail(new Error("user not found"));
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
Good:
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", {
|
|
577
|
+
id: UserId,
|
|
578
|
+
}) {}
|
|
579
|
+
|
|
580
|
+
yield * new UserNotFound({ id });
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### nopeus/prefer-effect-platform-services
|
|
584
|
+
|
|
585
|
+
Rejects direct Node platform APIs only when Effect v4 provides a semantic replacement. Diagnostics
|
|
586
|
+
name both the Effect API and the Node provider (or the runtime-provided service) needed to use it.
|
|
587
|
+
Both `node:x` and bare `x` specifiers are recognized.
|
|
588
|
+
|
|
589
|
+
| Node import | Checked value(s) | Effect replacement | Provider |
|
|
590
|
+
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
591
|
+
| `fs`, `fs/promises` | Every value import (existing behavior) | `FileSystem.FileSystem` | `NodeFileSystem.layer` or `NodeServices.layer` |
|
|
592
|
+
| `path` | Every value import (existing behavior) | `Path.Path` | `NodePath.layer` or `NodeServices.layer` |
|
|
593
|
+
| `child_process` | Every value import (existing behavior) | `ChildProcess` commands and `ChildProcessSpawner.ChildProcessSpawner` | `NodeChildProcessSpawner.layer` or `NodeServices.layer` |
|
|
594
|
+
| `crypto` | `randomUUID` | `Crypto.Crypto.randomUUIDv4` | `NodeCrypto.layer` or `NodeServices.layer` |
|
|
595
|
+
| `crypto` | `randomUUIDv7` | `Crypto.Crypto.randomUUIDv7` | `NodeCrypto.layer` or `NodeServices.layer` |
|
|
596
|
+
| `crypto` | `randomBytes` | `Crypto.Crypto.randomBytes` | `NodeCrypto.layer` or `NodeServices.layer` |
|
|
597
|
+
| `crypto` | `randomInt` | `Crypto.Crypto.randomIntBetween` | `NodeCrypto.layer` or `NodeServices.layer` |
|
|
598
|
+
| `crypto` | `createHash`, `hash`, `subtle.digest`, `webcrypto.subtle.digest` | `Crypto.Crypto.digest` | `NodeCrypto.layer` or `NodeServices.layer` |
|
|
599
|
+
| `url` | `fileURLToPath` | `Path.Path.fromFileUrl` | `NodePath.layer` or `NodeServices.layer` |
|
|
600
|
+
| `url` | `pathToFileURL` | `Path.Path.toFileUrl` | `NodePath.layer` or `NodeServices.layer` |
|
|
601
|
+
| `console` | `assert`, `clear`, `count`, `countReset`, `debug`, `dir`, `dirxml`, `error`, `group`, `groupCollapsed`, `groupEnd`, `info`, `log`, `table`, `time`, `timeEnd`, `timeLog`, `trace`, `warn` | The matching method on `Console.Console` | The runtime-provided `Console.Console` service |
|
|
602
|
+
| `process` | `argv`, `stdin`, `stdout`, `stderr` | `Stdio.Stdio` | `NodeStdio.layer` or `NodeServices.layer` |
|
|
603
|
+
| `process` | `hrtime` | `Clock.Clock.monotonicTimeNanos` | The runtime-provided `Clock.Clock` service |
|
|
604
|
+
| `timers`, `timers/promises` | `setTimeout` | `Effect.sleep` | The runtime-provided `Clock.Clock` service |
|
|
605
|
+
| `timers`, `timers/promises` | `setInterval` | `Effect.repeat` or `Stream.fromEffectSchedule` | The runtime-provided `Clock.Clock` service |
|
|
606
|
+
| `perf_hooks` | `performance.now` | `Clock.Clock.monotonicTimeNanos` | The runtime-provided `Clock.Clock` service |
|
|
607
|
+
| `http`, `https` | `get`, `request` | `HttpClient.get` / `HttpClient.execute` through `HttpClient.HttpClient` | `NodeHttpClient.layerUndici` or `NodeHttpClient.layerNodeHttp` |
|
|
608
|
+
| `http`, `https` | Direct `createServer` use | `HttpServer.HttpServer` for server behavior | `NodeHttpServer.layer` or `NodeHttpServer.layerConfig` |
|
|
609
|
+
| `net` | `connect`, `createConnection` | `Socket.Socket` | `NodeSocket.makeNet` or `NodeSocket.layerNet` |
|
|
610
|
+
| `net` | `createServer` | `SocketServer.SocketServer` | `NodeSocketServer.layer` |
|
|
611
|
+
|
|
612
|
+
Named and aliased imports are checked at the imported symbol. Namespace and default imports are
|
|
613
|
+
checked when a mapped member is accessed, including computed string properties. Type-only imports
|
|
614
|
+
remain allowed because they perform no platform I/O.
|
|
615
|
+
|
|
616
|
+
`node:http` and `node:https` still require a Node server factory at the application boundary. Passing
|
|
617
|
+
`createServer` directly, or calling it inside the lazy factory, as the first argument of
|
|
618
|
+
`NodeHttpServer.layer` or `NodeHttpServer.layerConfig` is allowed. Using it to implement server
|
|
619
|
+
behavior directly is reported.
|
|
620
|
+
|
|
621
|
+
The audit intentionally leaves Node streams, `EventEmitter`, `Buffer`, OS metadata, `util`, TLS,
|
|
622
|
+
DNS, datagrams, HTTP/2, readline/TTY primitives, compression, and worker threads unrestricted.
|
|
623
|
+
Effect exposes adapters or higher-level abstractions for some of these, but not a semantic
|
|
624
|
+
replacement for every use of the imported Node value. The same is true for the remaining process,
|
|
625
|
+
crypto, console, timer, URL, HTTP, and net exports. In particular, `Worker` remains necessary when
|
|
626
|
+
constructing `NodeWorker.layer`, and Node streams remain necessary at `NodeStream` interop
|
|
627
|
+
boundaries.
|
|
628
|
+
|
|
629
|
+
No autofix is offered. These migrations introduce service requirements, Layers, scoped resources,
|
|
630
|
+
or effectful control flow and need application-specific placement.
|
|
631
|
+
|
|
632
|
+
```ts
|
|
633
|
+
import { Crypto, Effect } from "effect";
|
|
634
|
+
|
|
635
|
+
const id = Effect.gen(function* () {
|
|
636
|
+
const crypto = yield* Crypto.Crypto;
|
|
637
|
+
return yield* crypto.randomUUIDv4;
|
|
638
|
+
});
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### nopeus/prefer-effect-void
|
|
642
|
+
|
|
643
|
+
Rejects Effect.succeed(undefined) and Effect.succeed(void 0). Effect.void is the
|
|
644
|
+
canonical shared value and makes intent immediate.
|
|
645
|
+
|
|
646
|
+
Bad:
|
|
647
|
+
|
|
648
|
+
```ts
|
|
649
|
+
const done = Effect.succeed(undefined);
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
Good:
|
|
653
|
+
|
|
654
|
+
```ts
|
|
655
|
+
const done = Effect.void;
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
### nopeus/require-effect-fn-name
|
|
659
|
+
|
|
660
|
+
Requires every Effect.fn call to begin with a static string name. When the
|
|
661
|
+
function has an owning binding or property, the trace name must equal that
|
|
662
|
+
symbol or end with a dot followed by that symbol. This permits both loadUser and
|
|
663
|
+
Users.loadUser while rejecting unrelated names. A variable containing a name is
|
|
664
|
+
not accepted because the trace boundary should be visible at the call.
|
|
665
|
+
|
|
666
|
+
Bad:
|
|
667
|
+
|
|
668
|
+
```ts
|
|
669
|
+
import * as Effect from "effect/Effect";
|
|
670
|
+
|
|
671
|
+
const loadUser = Effect.fn(function* (id: UserId) {
|
|
672
|
+
return yield* Users.findById(id);
|
|
673
|
+
});
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
Good:
|
|
677
|
+
|
|
678
|
+
```ts
|
|
679
|
+
import * as Effect from "effect/Effect";
|
|
680
|
+
|
|
681
|
+
const loadUser = Effect.fn("@goho/Users.loadUser")(function* (id: UserId) {
|
|
682
|
+
return yield* Users.findById(id);
|
|
683
|
+
});
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
Effect.fnUntraced remains valid when tracing would not add value, particularly
|
|
687
|
+
in library implementations and hot paths.
|
|
688
|
+
|
|
689
|
+
### nopeus/require-effect-namespace
|
|
690
|
+
|
|
691
|
+
Enforces readable trace names in `@project/Domain.operation` form. The repository
|
|
692
|
+
prefix comes from the root package name: both `goho` and `@adamaho/goho` use
|
|
693
|
+
`@goho/`. Domain segments use PascalCase; the final operation uses camelCase.
|
|
694
|
+
Nested domains are allowed, for example `@goho/Database.Migrations.run`.
|
|
695
|
+
|
|
696
|
+
| APIs | Checked name |
|
|
697
|
+
| --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
698
|
+
| `Effect.fn` | Traced operation; `require-effect-fn-name` also checks the owning symbol |
|
|
699
|
+
| `Effect.makeSpan`, `makeSpanScoped`, `useSpan`; `Layer.span` | Span name |
|
|
700
|
+
| `Effect.withSpan`, `withSpanScoped`, `withLogSpan`; `Layer`, `Stream`, `Channel`, `RequestResolver` `.withSpan` | Span name in data-first and data-last calls |
|
|
701
|
+
|
|
702
|
+
```ts
|
|
703
|
+
import { Context, Effect, Schema } from "effect";
|
|
704
|
+
|
|
705
|
+
class Users extends Context.Service<Users, Users.Service>()("@goho/Users") {}
|
|
706
|
+
class ReadFailed extends Schema.TaggedError<ReadFailed>()("ReadFailed", {}) {}
|
|
707
|
+
const Outcome = Schema.TaggedStruct("Processed", {});
|
|
708
|
+
const load = Effect.fn("@goho/Users.load")(function* () {
|
|
709
|
+
/* ... */
|
|
710
|
+
});
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
A trace name must be static and include both a domain and an operation.
|
|
714
|
+
`@goho/load`, `@goho/Users`, whitespace, empty segments, and dynamic interpolation
|
|
715
|
+
are rejected. `Effect.fn` names must end in the owning function or property name:
|
|
716
|
+
`load` can use `@goho/Users.load`, but cannot use `@goho/Users.save`.
|
|
717
|
+
Choose domains that identify the work, such as `Receipts.process` or
|
|
718
|
+
`GoogleDrive.listFiles`; avoid repeating the repository name inside the domain.
|
|
719
|
+
Lint checks structure and owner matching; choosing a meaningful domain still
|
|
720
|
+
requires review. `Effect.fnUntraced` remains available for internal helpers.
|
|
721
|
+
|
|
722
|
+
The rule resolves imports from `effect` and direct `effect/Module` paths, including
|
|
723
|
+
aliases and `import * as E from "effect"`. Shadowed local bindings are ignored.
|
|
724
|
+
String literals and templates without interpolation are accepted, including
|
|
725
|
+
`satisfies` and type assertion wrappers. Re-exports and locally assigned aliases
|
|
726
|
+
are not followed. Unstable subpackages are outside the supported API catalogue.
|
|
727
|
+
|
|
728
|
+
Schema identifiers, data/error/request tags, metric names, ordinary values, and
|
|
729
|
+
log messages are not trace names and are not checked by this rule. In particular,
|
|
730
|
+
a trace-policy upgrade must not require changing serialized `_tag` values.
|
|
731
|
+
Existing API schemas and matching code can keep their established tags.
|
|
732
|
+
|
|
733
|
+
When upgrading from 0.5.0, keep or restore your original data/error tags and metric
|
|
734
|
+
names. Add domain/operation structure to trace names that previously had only a
|
|
735
|
+
prefix. Trace renames may require updating trace queries. No automatic fix is
|
|
736
|
+
provided because lint cannot choose meaningful operation names.
|
|
737
|
+
|
|
738
|
+
### nopeus/require-service-key-prefix
|
|
739
|
+
|
|
740
|
+
Separately enforces static, nonempty repository-prefixed identity keys for
|
|
741
|
+
`Context.Service` and `Context.Reference`, including curried service declarations.
|
|
742
|
+
Both this rule and the trace rule are enabled by the Effect preset using the same
|
|
743
|
+
repository prefix. Service keys need no `.operation` suffix: `@goho/Users` is valid.
|
|
744
|
+
Import aliases and namespace imports are resolved by scope, so local bindings with
|
|
745
|
+
the same name are ignored.
|
|
746
|
+
|
|
747
|
+
### nopeus/require-service-constructor-names
|
|
748
|
+
|
|
749
|
+
Name exported Layers and Layer factories `layer` or `layerX`, and service
|
|
750
|
+
constructors `make` or `makeX`. The suffix starts with an uppercase letter:
|
|
751
|
+
`layerConfig`, `layerMemory`, and `makeServiceAccount` are valid;
|
|
752
|
+
`authLayer`, `defaultLayer`, `createAuth`, and `make_client` are not.
|
|
753
|
+
Use lowercase `layer` and `make`; uppercase `Layer` names the imported Effect
|
|
754
|
+
module.
|
|
755
|
+
|
|
756
|
+
Neither export requires the other, and suffixes do not need to match. A service
|
|
757
|
+
may expose only a Layer, only a constructor, or both. Constructors may remain
|
|
758
|
+
private. Layer values, parameterized factories, inline construction, config
|
|
759
|
+
wrappers, and composed layers are supported.
|
|
760
|
+
|
|
761
|
+
Bad:
|
|
762
|
+
|
|
763
|
+
```ts
|
|
764
|
+
export const createAuth = (options: Options) => Service.of(options);
|
|
765
|
+
export const authLayer = (options: Options) => Layer.succeed(Service, createAuth(options));
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
Good, with a reusable constructor:
|
|
769
|
+
|
|
770
|
+
```ts
|
|
771
|
+
export const make = (options: Options) => Service.of(options);
|
|
772
|
+
export const layer = (options: Options) => Layer.succeed(Service, make(options));
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
Good, with inline construction and no separate `make`:
|
|
776
|
+
|
|
777
|
+
```ts
|
|
778
|
+
export const layer = (options: Options) =>
|
|
779
|
+
Layer.effect(
|
|
780
|
+
Service,
|
|
781
|
+
Effect.gen(function* () {
|
|
782
|
+
const database = yield* Database;
|
|
783
|
+
return Service.of({ find: (id) => database.find(id, options) });
|
|
784
|
+
}),
|
|
785
|
+
);
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
Good, resolving configuration through the existing constructor:
|
|
789
|
+
|
|
790
|
+
```ts
|
|
791
|
+
export const make = (options: Options) => Effect.succeed(Service.of(options));
|
|
792
|
+
export const layerConfig = (options: Config.Wrap<Options>) =>
|
|
793
|
+
Layer.effect(Service, Config.unwrap(options).pipe(Effect.flatMap(make)));
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
Good, composing existing Layers without creating another service:
|
|
797
|
+
|
|
798
|
+
```ts
|
|
799
|
+
export const layer = (options: Options) =>
|
|
800
|
+
Layer.merge(Auth.layer(options.auth), Database.layer(options.database));
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
This syntax rule recognizes imported Effect Layer constructors/composition,
|
|
804
|
+
local service `.of` construction, constructors passed to `Layer.effect`,
|
|
805
|
+
`Layer.succeed`, or `Layer.sync`, and explicit Layer/service return types.
|
|
806
|
+
It follows local aliases and returned expressions, including Effect generators
|
|
807
|
+
and functions. Local export aliases are checked by their public name; recognized
|
|
808
|
+
constructors must use named exports rather than a default export.
|
|
809
|
+
|
|
810
|
+
Private helpers and unrelated functions are not subject to this naming rule.
|
|
811
|
+
Opaque imported factories, cross-file re-exports, and arbitrary type inference
|
|
812
|
+
are outside the syntax check. An explicit `Layer.Layer<...>` return annotation
|
|
813
|
+
makes an otherwise opaque Layer factory recognizable. The rule does not verify
|
|
814
|
+
construction behavior or require particular files, interfaces, or export pairs.
|
|
815
|
+
|
|
816
|
+
This replaces `require-service-make-layer`; the old rule is removed, not retained
|
|
817
|
+
as an optional policy. The naming rule is always enabled in the Effect preset.
|
|
818
|
+
|
|
819
|
+
## Credits
|
|
820
|
+
|
|
821
|
+
The initial policy and rule set were inspired by
|
|
822
|
+
[anti-slop](https://github.com/dmmulroy/anti-slop).
|