@starci/eslint-canon-be 1.2.2 → 1.7.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 +47 -0
- package/README.md +46 -7
- package/async-discipline.mjs +83 -0
- package/authorization.mjs +106 -106
- package/comments.mjs +7 -55
- package/config-owner.mjs +252 -0
- package/cqrs.mjs +7 -141
- package/data-access.mjs +151 -5
- package/default-deny.mjs +116 -0
- package/e2e-flow.mjs +222 -222
- package/error-handling.mjs +207 -0
- package/idempotency.mjs +101 -0
- package/index.mjs +34 -8
- package/input-bounds.mjs +84 -0
- package/lib/ast.mjs +81 -0
- package/lib/config.mjs +49 -0
- package/lib/path.mjs +15 -0
- package/lib/slots.mjs +83 -0
- package/log-safety.mjs +148 -0
- package/module-layering.mjs +87 -105
- package/module-shape.mjs +202 -0
- package/observability.mjs +275 -275
- package/package.json +4 -1
- package/query-safety.mjs +274 -0
- package/resilience.mjs +178 -0
- package/schema-authority.mjs +187 -0
- package/size-budget.mjs +106 -0
- package/spec-quality.mjs +132 -0
- package/suppression.mjs +60 -0
- package/temporal.mjs +71 -0
- package/testing.mjs +31 -19
- package/transport.mjs +168 -168
- package/type-safety.mjs +98 -57
- package/user-copy.mjs +106 -0
- package/cdc.mjs +0 -150
- package/event-delivery.mjs +0 -93
- package/exception-identity.mjs +0 -200
- package/exceptions.mjs +0 -262
package/module-shape.mjs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules that hold the shape of a Nest module in HFS (catalog R45 `BE_MODULE_SHAPE`).
|
|
3
|
+
*
|
|
4
|
+
* - `global-module-allowlist` lets `@Global()` appear only on the platform capabilities the slot manifest
|
|
5
|
+
* names (`ruleParams.be.globalModules`; read through `lib/slots.mjs`).
|
|
6
|
+
* A global module hides who depends on whom, so it is reserved for the three every capability needs.
|
|
7
|
+
* - `typed-module-definition` requires `ConfigurableModuleBuilder<Options>` to state its options type.
|
|
8
|
+
* - `static-module-register` requires `register`, `registerAsync`, `forRoot` and `forFeature` to be `static`.
|
|
9
|
+
* - `no-new-injectable` refuses `new SomeService()` (and other injectable-shaped names) outside specs: the
|
|
10
|
+
* container builds providers, so construction by hand bypasses its scope and its overrides.
|
|
11
|
+
* - `no-module-let` refuses mutable module-level state (`let` and `var` at the top of a file).
|
|
12
|
+
* - `one-module-per-file` allows one `@Module` class in a file, so one transport or application module has
|
|
13
|
+
* one home.
|
|
14
|
+
*
|
|
15
|
+
* `no-self-global-module` of the earlier canon is gone: it refused `@Global()` everywhere, which also refused
|
|
16
|
+
* the three platform capabilities HFS requires to be global.
|
|
17
|
+
*/
|
|
18
|
+
import { decoratorName } from "./lib/ast.mjs"
|
|
19
|
+
import { isDeclarationFile, isTestLane, normalizePath } from "./lib/path.mjs"
|
|
20
|
+
import { hfsParams } from "./lib/slots.mjs"
|
|
21
|
+
|
|
22
|
+
/** `platform/<capability>` of a file inside `src/modules/platform`, else null. */
|
|
23
|
+
const platformCapability = (filename) => {
|
|
24
|
+
const match = /\/src\/modules\/(platform\/[^/]+)\//.exec(filename)
|
|
25
|
+
return match ? match[1] : null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** `@Global()` only on an allowlisted platform capability. */
|
|
29
|
+
export const globalModuleAllowlist = {
|
|
30
|
+
meta: {
|
|
31
|
+
type: "problem",
|
|
32
|
+
docs: { description: "`@Global()` only for the manifest's platform allowlist." },
|
|
33
|
+
schema: [
|
|
34
|
+
{
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: { allowed: { type: "array", items: { type: "string" } } },
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
messages: {
|
|
41
|
+
global: "`@Global()` is allowed only on {{allowed}}. A global module hides its dependants; import the module where it is used, or pass options through `register`.",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
create(context) {
|
|
45
|
+
const filename = normalizePath(context.filename || context.getFilename())
|
|
46
|
+
if (isDeclarationFile(filename) || isTestLane(filename)) return {}
|
|
47
|
+
const allowed = context.options[0]?.allowed ?? hfsParams.globalModules
|
|
48
|
+
const capability = platformCapability(filename)
|
|
49
|
+
if (capability !== null && allowed.includes(capability)) return {}
|
|
50
|
+
return {
|
|
51
|
+
Decorator(node) {
|
|
52
|
+
if (decoratorName(node) === "Global") context.report({ node, messageId: "global", data: { allowed: allowed.map((name) => `\`${name}\``).join(", ") } })
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A configurable module builder names its options type. */
|
|
59
|
+
export const typedModuleDefinition = {
|
|
60
|
+
meta: {
|
|
61
|
+
type: "problem",
|
|
62
|
+
docs: { description: "`new ConfigurableModuleBuilder<Options>()` states its options type." },
|
|
63
|
+
schema: [],
|
|
64
|
+
messages: {
|
|
65
|
+
untyped: "`ConfigurableModuleBuilder` has no options type. Write `new ConfigurableModuleBuilder<{{example}}Options>()` so `register` is checked.",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
create(context) {
|
|
69
|
+
if (isDeclarationFile(context.filename || context.getFilename())) return {}
|
|
70
|
+
return {
|
|
71
|
+
NewExpression(node) {
|
|
72
|
+
if (node.callee.type !== "Identifier" || node.callee.name !== "ConfigurableModuleBuilder") return
|
|
73
|
+
const typed = (node.typeArguments ?? node.typeParameters)?.params?.length > 0
|
|
74
|
+
if (!typed) context.report({ node, messageId: "untyped", data: { example: "Capability" } })
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const REGISTER_NAMES = new Set(["register", "registerAsync", "forRoot", "forRootAsync", "forFeature"])
|
|
81
|
+
|
|
82
|
+
/** The dynamic-module factory methods are `static`. */
|
|
83
|
+
export const staticModuleRegister = {
|
|
84
|
+
meta: {
|
|
85
|
+
type: "problem",
|
|
86
|
+
docs: { description: "`register`, `registerAsync`, `forRoot` and `forFeature` are static." },
|
|
87
|
+
schema: [],
|
|
88
|
+
messages: {
|
|
89
|
+
notStatic: "`{{name}}` builds a dynamic module and must be `static`; an instance method cannot be called from `imports`.",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
create(context) {
|
|
93
|
+
if (isDeclarationFile(context.filename || context.getFilename())) return {}
|
|
94
|
+
return {
|
|
95
|
+
ClassDeclaration(node) {
|
|
96
|
+
if (!node.decorators?.some((decorator) => decoratorName(decorator) === "Module")) return
|
|
97
|
+
for (const member of node.body.body) {
|
|
98
|
+
if (member.type !== "MethodDefinition" || member.static || member.computed) continue
|
|
99
|
+
if (member.key.type === "Identifier" && REGISTER_NAMES.has(member.key.name)) {
|
|
100
|
+
context.report({ node: member, messageId: "notStatic", data: { name: member.key.name } })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const INJECTABLE_SUFFIX = /(?:Service|Repository|Guard|Interceptor|Resolver|Consumer|UseCase)$/
|
|
109
|
+
|
|
110
|
+
/** Providers are built by the container, not by `new`. */
|
|
111
|
+
export const noNewInjectable = {
|
|
112
|
+
meta: {
|
|
113
|
+
type: "problem",
|
|
114
|
+
docs: { description: "No `new SomeService()` for an injectable-shaped class outside a spec." },
|
|
115
|
+
schema: [],
|
|
116
|
+
messages: {
|
|
117
|
+
construct: "`new {{name}}()` builds a provider by hand, bypassing the container's scope and overrides. Inject it, or register it as a provider.",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
create(context) {
|
|
121
|
+
const filename = normalizePath(context.filename || context.getFilename())
|
|
122
|
+
if (isDeclarationFile(filename) || isTestLane(filename)) return {}
|
|
123
|
+
return {
|
|
124
|
+
NewExpression(node) {
|
|
125
|
+
if (node.callee.type === "Identifier" && INJECTABLE_SUFFIX.test(node.callee.name)) {
|
|
126
|
+
context.report({ node, messageId: "construct", data: { name: node.callee.name } })
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** No mutable state at module scope. */
|
|
134
|
+
export const noModuleLet = {
|
|
135
|
+
meta: {
|
|
136
|
+
type: "problem",
|
|
137
|
+
docs: { description: "No `let` or `var` at module scope." },
|
|
138
|
+
schema: [],
|
|
139
|
+
messages: {
|
|
140
|
+
mutable: "`{{kind}}` at module scope is process-wide mutable state shared by every request and test. Keep the state in an injected provider.",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
create(context) {
|
|
144
|
+
const filename = normalizePath(context.filename || context.getFilename())
|
|
145
|
+
if (isDeclarationFile(filename) || isTestLane(filename)) return {}
|
|
146
|
+
const check = (node) => {
|
|
147
|
+
if (node.type === "VariableDeclaration" && node.kind !== "const") context.report({ node, messageId: "mutable", data: { kind: node.kind } })
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
Program(node) {
|
|
151
|
+
for (const statement of node.body) {
|
|
152
|
+
check(statement)
|
|
153
|
+
if (statement.type === "ExportNamedDeclaration" && statement.declaration) check(statement.declaration)
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** One `@Module` class per file. */
|
|
161
|
+
export const oneModulePerFile = {
|
|
162
|
+
meta: {
|
|
163
|
+
type: "problem",
|
|
164
|
+
docs: { description: "A file declares at most one `@Module` class." },
|
|
165
|
+
schema: [],
|
|
166
|
+
messages: {
|
|
167
|
+
many: "This file declares a second `@Module` class. One transport or application module has one file; split this one out.",
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
create(context) {
|
|
171
|
+
const filename = normalizePath(context.filename || context.getFilename())
|
|
172
|
+
if (isDeclarationFile(filename) || isTestLane(filename)) return {}
|
|
173
|
+
let seen = 0
|
|
174
|
+
return {
|
|
175
|
+
ClassDeclaration(node) {
|
|
176
|
+
if (!node.decorators?.some((decorator) => decoratorName(decorator) === "Module")) return
|
|
177
|
+
seen += 1
|
|
178
|
+
if (seen > 1) context.report({ node, messageId: "many" })
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The rules this law contributes to the plugin. */
|
|
185
|
+
export const rules = {
|
|
186
|
+
"global-module-allowlist": globalModuleAllowlist,
|
|
187
|
+
"typed-module-definition": typedModuleDefinition,
|
|
188
|
+
"static-module-register": staticModuleRegister,
|
|
189
|
+
"no-new-injectable": noNewInjectable,
|
|
190
|
+
"no-module-let": noModuleLet,
|
|
191
|
+
"one-module-per-file": oneModulePerFile,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** All start at error: HFS has one module shape and no baseline. */
|
|
195
|
+
export const recommended = {
|
|
196
|
+
"starci-be/global-module-allowlist": "error",
|
|
197
|
+
"starci-be/typed-module-definition": "error",
|
|
198
|
+
"starci-be/static-module-register": "error",
|
|
199
|
+
"starci-be/no-new-injectable": "error",
|
|
200
|
+
"starci-be/no-module-let": "error",
|
|
201
|
+
"starci-be/one-module-per-file": "error",
|
|
202
|
+
}
|