@noctcore/eslint-plugin-contracts 0.1.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 +47 -0
- package/dist/index.cjs +544 -0
- package/dist/index.d.cts +77 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +516 -0
- package/docs/rules/money-must-be-decimal.md +52 -0
- package/docs/rules/no-direct-process-env.md +51 -0
- package/docs/rules/no-error-stringify.md +47 -0
- package/docs/rules/wire-message-naming.md +51 -0
- package/docs/rules/zod-schema-naming.md +54 -0
- package/package.json +65 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# `noctcore-contracts/no-error-stringify`
|
|
2
|
+
|
|
3
|
+
> Stringifying an error with `${error}`, `error.toString()`, or `error + ""` drops its cause chain.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
`` `${error}` ``, `error.toString()`, and `error + ""` all coerce an `Error` to its `message` alone,
|
|
8
|
+
discarding `error.cause`, the stack, and any custom fields. The value that reaches your logs is a bare
|
|
9
|
+
sentence with no chain to the underlying failure. The guarded extractor idiom preserves the object for
|
|
10
|
+
structured loggers and stays legal:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
error instanceof Error ? error.message : String(error)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What it flags
|
|
17
|
+
|
|
18
|
+
Only the three unambiguous cause-chain-dropping forms, and only when the operand is a known error
|
|
19
|
+
identifier (default `error`, `err`, `e`, `cause`):
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// ✗
|
|
23
|
+
logger.error(`request failed: ${error}`);
|
|
24
|
+
const msg = err.toString();
|
|
25
|
+
const msg = error + "";
|
|
26
|
+
const msg = "" + e;
|
|
27
|
+
|
|
28
|
+
// ✓ the guarded idiom (bare String(error) is intentionally NOT policed)
|
|
29
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
30
|
+
const m = `${error.message}`;
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Options
|
|
34
|
+
|
|
35
|
+
| Option | Type | Default | Meaning |
|
|
36
|
+
| --- | --- | --- | --- |
|
|
37
|
+
| `errorIdentifierNames` | `string[]` | `['error', 'err', 'e', 'cause']` | Identifier names treated as errors for the three flagged forms. |
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
'noctcore-contracts/no-error-stringify': ['error', { errorIdentifierNames: ['error', 'cause', 'failure'] }]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## When not to use it
|
|
44
|
+
|
|
45
|
+
If your codebase deliberately renders errors to strings at the boundary (and has already extracted the
|
|
46
|
+
cause), or names error bindings unpredictably, this rule may be noisy — narrow or widen
|
|
47
|
+
`errorIdentifierNames` to fit.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# `noctcore-contracts/wire-message-naming`
|
|
2
|
+
|
|
3
|
+
> A message-schema's `type` discriminant must be the kebab-case of its const name minus its role suffix. 🔧
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
When wire messages are modelled as zod objects with a `type: z.literal('…')` discriminant, the const
|
|
8
|
+
name and the on-the-wire discriminant are two spellings of the same fact. Deriving one from the other
|
|
9
|
+
by convention (`TaskCompletedEvent` ⇒ `'task-completed'`) removes a whole class of copy-paste
|
|
10
|
+
drift where the const is renamed but the literal is not.
|
|
11
|
+
|
|
12
|
+
## What it flags
|
|
13
|
+
|
|
14
|
+
For every `export const` whose name ends in a role suffix (default `Event` / `Command` / `Query`) and
|
|
15
|
+
whose zod object declares a `type: z.literal('…')` property, the literal must equal
|
|
16
|
+
`kebab(constName minus the role suffix)`. A mismatch is reported and **autofixed** to the expected
|
|
17
|
+
value.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// ✗ camelCase discriminant → autofixes to 'task-completed'
|
|
21
|
+
export const TaskCompletedEvent = z.object({ type: z.literal('taskCompleted') });
|
|
22
|
+
|
|
23
|
+
// ✗ wrong value → autofixes to 'run-task'
|
|
24
|
+
export const RunTaskCommand = z.object({ type: z.literal('run') });
|
|
25
|
+
|
|
26
|
+
// ✓
|
|
27
|
+
export const TaskCompletedEvent = z.object({ type: z.literal('task-completed') });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Consts without a role suffix, and role-suffixed consts without a `type` literal, are ignored.
|
|
31
|
+
|
|
32
|
+
## Options
|
|
33
|
+
|
|
34
|
+
| Option | Type | Default | Meaning |
|
|
35
|
+
| --- | --- | --- | --- |
|
|
36
|
+
| `roleSuffixes` | `string[]` | `['Event', 'Command', 'Query']` | Const-name suffixes that mark a schema as a wire message. |
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
'noctcore-contracts/wire-message-naming': ['error', { roleSuffixes: ['Message'] }]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// with roleSuffixes: ['Message'] → autofixes to 'task-done'
|
|
44
|
+
export const TaskDoneMessage = z.object({ type: z.literal('done') });
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## When not to use it
|
|
48
|
+
|
|
49
|
+
If your messages do not carry a role-suffixed const name plus a `type: z.literal(...)` discriminant,
|
|
50
|
+
this rule never fires. Pair it with [`zod-schema-naming`](./zod-schema-naming.md) (listing the same
|
|
51
|
+
suffixes there) so every export is covered by exactly one naming contract.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# `noctcore-contracts/zod-schema-naming`
|
|
2
|
+
|
|
3
|
+
> Every exported zod schema is a PascalCase const suffixed `Schema`, paired with a same-named inferred type.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
A contracts package is a shared spine. A uniform `FooSchema` + `Foo` pairing keeps the schema and
|
|
8
|
+
its TypeScript type discoverable and prevents a hand-authored duplicate type from drifting away from
|
|
9
|
+
the schema it is supposed to mirror.
|
|
10
|
+
|
|
11
|
+
## What it flags
|
|
12
|
+
|
|
13
|
+
For every `export const` whose initializer is rooted at the `z` identifier (`z.object(...)`,
|
|
14
|
+
`z.string().min(1)`, `z.union([...])`, …):
|
|
15
|
+
|
|
16
|
+
- the const name must match `^[A-Z][A-Za-z0-9]*Schema$` — otherwise `schemaNaming`;
|
|
17
|
+
- a correctly-named `FooSchema` must have a sibling `export type Foo` (a `type` alias or
|
|
18
|
+
`interface`) — otherwise `missingType`.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// ✗ not suffixed `Schema`
|
|
22
|
+
export const Task = z.object({});
|
|
23
|
+
|
|
24
|
+
// ✗ no sibling inferred type
|
|
25
|
+
export const TaskSchema = z.object({});
|
|
26
|
+
|
|
27
|
+
// ✓
|
|
28
|
+
export const TaskSchema = z.object({ id: z.string() });
|
|
29
|
+
export type Task = z.infer<typeof TaskSchema>;
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Options
|
|
33
|
+
|
|
34
|
+
| Option | Type | Default | Meaning |
|
|
35
|
+
| --- | --- | --- | --- |
|
|
36
|
+
| `roleSuffixes` | `string[]` | `[]` | Const-name suffixes that opt a schema **out** of the `*Schema` rule (their naming is owned by [`wire-message-naming`](./wire-message-naming.md)). |
|
|
37
|
+
|
|
38
|
+
With the default empty list the base convention applies to every exported zod schema. Codebases that
|
|
39
|
+
model wire messages as `*Event` / `*Command` / `*Query` discriminated-union members pass those
|
|
40
|
+
suffixes to carve them out:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
'noctcore-contracts/zod-schema-naming': ['error', { roleSuffixes: ['Event', 'Command', 'Query'] }]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// carved out only when 'Command' is listed in roleSuffixes
|
|
48
|
+
export const RunTaskCommand = z.object({ type: z.literal('run-task') });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## When not to use it
|
|
52
|
+
|
|
53
|
+
If your codebase does not import zod as `z`, or does not pair schemas with inferred types, this rule
|
|
54
|
+
will not fit — it keys off the `z` root identifier and the `FooSchema`/`Foo` pairing convention.
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@noctcore/eslint-plugin-contracts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ESLint rules for shared contract, config, error-handling, and money-precision conventions (zod schema naming, wire discriminants, no-direct-process-env, decimal money).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"docs",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"keywords": [
|
|
25
|
+
"eslint",
|
|
26
|
+
"eslintplugin",
|
|
27
|
+
"eslint-plugin",
|
|
28
|
+
"contracts",
|
|
29
|
+
"zod",
|
|
30
|
+
"noctcore"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"provenance": true
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/noctcore/eslint-plugins.git",
|
|
39
|
+
"directory": "packages/eslint-plugin-contracts"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/noctcore/eslint-plugins/tree/main/packages/eslint-plugin-contracts",
|
|
42
|
+
"bugs": "https://github.com/noctcore/eslint-plugins/issues",
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"test": "vitest run"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@noctcore/eslint-utils": "^0.1.0",
|
|
50
|
+
"@typescript-eslint/utils": "^8.61.1"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"eslint": ">=9.0.0",
|
|
54
|
+
"typescript": ">=5.0.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@noctcore/eslint-test-utils": "workspace:*",
|
|
58
|
+
"@types/node": "^22.0.0",
|
|
59
|
+
"@typescript-eslint/parser": "^8.61.1",
|
|
60
|
+
"@typescript-eslint/rule-tester": "^8.61.1",
|
|
61
|
+
"tsup": "^8.5.1",
|
|
62
|
+
"typescript": "^5.6.0",
|
|
63
|
+
"vitest": "^3"
|
|
64
|
+
}
|
|
65
|
+
}
|