@pjts/eslint-style-rules 0.0.0-stage → 0.0.3
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 +138 -2
- package/dist/index.d.ts +38 -0
- package/dist/index.js +232 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/ast.d.ts +27 -0
- package/dist/lib/ast.js +60 -0
- package/dist/lib/ast.js.map +1 -0
- package/dist/lib/db.d.ts +13 -0
- package/dist/lib/db.js +50 -0
- package/dist/lib/db.js.map +1 -0
- package/dist/lib/messages.d.ts +13 -0
- package/dist/lib/messages.js +76 -0
- package/dist/lib/messages.js.map +1 -0
- package/dist/lib/rule.d.ts +9 -0
- package/dist/lib/rule.js +10 -0
- package/dist/lib/rule.js.map +1 -0
- package/dist/rules/app-class-suffix.d.ts +3 -0
- package/dist/rules/app-class-suffix.js +28 -0
- package/dist/rules/app-class-suffix.js.map +1 -0
- package/dist/rules/app-service-endpoints.d.ts +12 -0
- package/dist/rules/app-service-endpoints.js +79 -0
- package/dist/rules/app-service-endpoints.js.map +1 -0
- package/dist/rules/auth-access.d.ts +14 -0
- package/dist/rules/auth-access.js +122 -0
- package/dist/rules/auth-access.js.map +1 -0
- package/dist/rules/db-client-dependency.d.ts +11 -0
- package/dist/rules/db-client-dependency.js +74 -0
- package/dist/rules/db-client-dependency.js.map +1 -0
- package/dist/rules/db-writes-in-repositories.d.ts +12 -0
- package/dist/rules/db-writes-in-repositories.js +105 -0
- package/dist/rules/db-writes-in-repositories.js.map +1 -0
- package/dist/rules/decorated-class-contract.d.ts +6 -0
- package/dist/rules/decorated-class-contract.js +37 -0
- package/dist/rules/decorated-class-contract.js.map +1 -0
- package/dist/rules/entity-constructor-data.d.ts +6 -0
- package/dist/rules/entity-constructor-data.js +35 -0
- package/dist/rules/entity-constructor-data.js.map +1 -0
- package/dist/rules/event-sourced-aggregate.d.ts +15 -0
- package/dist/rules/event-sourced-aggregate.js +134 -0
- package/dist/rules/event-sourced-aggregate.js.map +1 -0
- package/dist/rules/find-one-naming.d.ts +7 -0
- package/dist/rules/find-one-naming.js +38 -0
- package/dist/rules/find-one-naming.js.map +1 -0
- package/dist/rules/import-only-in.d.ts +7 -0
- package/dist/rules/import-only-in.js +52 -0
- package/dist/rules/import-only-in.js.map +1 -0
- package/dist/rules/knex-first.d.ts +3 -0
- package/dist/rules/knex-first.js +28 -0
- package/dist/rules/knex-first.js.map +1 -0
- package/dist/rules/message-file.d.ts +18 -0
- package/dist/rules/message-file.js +118 -0
- package/dist/rules/message-file.js.map +1 -0
- package/dist/rules/message-handler.d.ts +19 -0
- package/dist/rules/message-handler.js +114 -0
- package/dist/rules/message-handler.js.map +1 -0
- package/dist/rules/message-name-reference.d.ts +7 -0
- package/dist/rules/message-name-reference.js +52 -0
- package/dist/rules/message-name-reference.js.map +1 -0
- package/dist/rules/message-names.d.ts +7 -0
- package/dist/rules/message-names.js +80 -0
- package/dist/rules/message-names.js.map +1 -0
- package/dist/rules/module-layout.d.ts +9 -0
- package/dist/rules/module-layout.js +71 -0
- package/dist/rules/module-layout.js.map +1 -0
- package/dist/rules/module-service-getters.d.ts +19 -0
- package/dist/rules/module-service-getters.js +132 -0
- package/dist/rules/module-service-getters.js.map +1 -0
- package/dist/rules/no-async-in-domain.d.ts +2 -0
- package/dist/rules/no-async-in-domain.js +116 -0
- package/dist/rules/no-async-in-domain.js.map +1 -0
- package/dist/rules/no-duplicate-sdk-types.d.ts +6 -0
- package/dist/rules/no-duplicate-sdk-types.js +40 -0
- package/dist/rules/no-duplicate-sdk-types.js.map +1 -0
- package/dist/rules/no-entity-assembly-in-app.d.ts +7 -0
- package/dist/rules/no-entity-assembly-in-app.js +37 -0
- package/dist/rules/no-entity-assembly-in-app.js.map +1 -0
- package/dist/rules/no-entity-mutation-outside-domain.d.ts +11 -0
- package/dist/rules/no-entity-mutation-outside-domain.js +68 -0
- package/dist/rules/no-entity-mutation-outside-domain.js.map +1 -0
- package/dist/rules/no-message-construction-outside-domain.d.ts +15 -0
- package/dist/rules/no-message-construction-outside-domain.js +45 -0
- package/dist/rules/no-message-construction-outside-domain.js.map +1 -0
- package/dist/rules/no-reconstitute.d.ts +3 -0
- package/dist/rules/no-reconstitute.js +22 -0
- package/dist/rules/no-reconstitute.js.map +1 -0
- package/dist/rules/port-file.d.ts +9 -0
- package/dist/rules/port-file.js +94 -0
- package/dist/rules/port-file.js.map +1 -0
- package/dist/rules/port-has-adapters.d.ts +9 -0
- package/dist/rules/port-has-adapters.js +62 -0
- package/dist/rules/port-has-adapters.js.map +1 -0
- package/dist/rules/repository-naming.d.ts +19 -0
- package/dist/rules/repository-naming.js +122 -0
- package/dist/rules/repository-naming.js.map +1 -0
- package/dist/rules/repository-serialization.d.ts +6 -0
- package/dist/rules/repository-serialization.js +50 -0
- package/dist/rules/repository-serialization.js.map +1 -0
- package/dist/rules/return-entities.d.ts +6 -0
- package/dist/rules/return-entities.js +38 -0
- package/dist/rules/return-entities.js.map +1 -0
- package/dist/rules/save-contract.d.ts +7 -0
- package/dist/rules/save-contract.js +41 -0
- package/dist/rules/save-contract.js.map +1 -0
- package/dist/rules/test-location.d.ts +14 -0
- package/dist/rules/test-location.js +94 -0
- package/dist/rules/test-location.js.map +1 -0
- package/dist/rules/throw-app-error.d.ts +12 -0
- package/dist/rules/throw-app-error.js +68 -0
- package/dist/rules/throw-app-error.js.map +1 -0
- package/dist/rules/trackable-repository.d.ts +10 -0
- package/dist/rules/trackable-repository.js +97 -0
- package/dist/rules/trackable-repository.js.map +1 -0
- package/package.json +46 -4
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const rule_1 = require("../lib/rule");
|
|
4
|
+
/**
|
|
5
|
+
* The domain layer is pure and synchronous — I/O and awaiting belong to the app layer.
|
|
6
|
+
*
|
|
7
|
+
* The one exception is password hashing from the modules in `allowedModules` (default `bcrypt`),
|
|
8
|
+
* which only exists asynchronously:
|
|
9
|
+
*
|
|
10
|
+
* public async verify(plain: string): Promise<boolean> {
|
|
11
|
+
* return bcrypt.compare(plain, this.hash);
|
|
12
|
+
* }
|
|
13
|
+
*
|
|
14
|
+
* An `await` is allowed only on such a call; an `async` function only when it awaits or returns
|
|
15
|
+
* such a call and awaits nothing else; a `Promise<…>` type only as the return type of a function
|
|
16
|
+
* that qualifies (async or returning such a call directly).
|
|
17
|
+
*/
|
|
18
|
+
const FUNCTIONS = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression']);
|
|
19
|
+
exports.default = (0, rule_1.defineRule)({
|
|
20
|
+
meta: {
|
|
21
|
+
type: 'problem',
|
|
22
|
+
docs: { description: 'Forbid async code in the domain layer, except bcrypt' },
|
|
23
|
+
messages: { async: 'Domain layer must be synchronous — move {{what}} to the app layer{{exception}}.' },
|
|
24
|
+
schema: [{
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: { allowedModules: { type: 'array', items: { type: 'string' } } },
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
}],
|
|
29
|
+
},
|
|
30
|
+
create(context) {
|
|
31
|
+
const allowedModules = context.options[0]?.allowedModules ?? ['bcrypt'];
|
|
32
|
+
const exception = allowedModules.length ? ` (only ${allowedModules.join(', ')} calls may be awaited)` : '';
|
|
33
|
+
/** `bcrypt` (default/namespace import) → call as `bcrypt.x()`; `compare` (named) → call as `compare()`. */
|
|
34
|
+
const namespaces = new Set();
|
|
35
|
+
const functions = new Set();
|
|
36
|
+
const isAllowedCall = (node) => {
|
|
37
|
+
if (node?.type !== 'CallExpression')
|
|
38
|
+
return false;
|
|
39
|
+
const callee = node.callee;
|
|
40
|
+
if (callee.type === 'Identifier')
|
|
41
|
+
return functions.has(callee.name);
|
|
42
|
+
return callee.type === 'MemberExpression' && callee.object.type === 'Identifier' && namespaces.has(callee.object.name);
|
|
43
|
+
};
|
|
44
|
+
/** Per-function bookkeeping, innermost last. */
|
|
45
|
+
const stack = [];
|
|
46
|
+
const report = (node, what) => context.report({ node, messageId: 'async', data: { what, exception } });
|
|
47
|
+
const enter = (fn) => {
|
|
48
|
+
const frame = { fn, allowedAwaits: 0, otherAwaits: 0, allowedReturns: 0, otherReturns: 0 };
|
|
49
|
+
// `async () => bcrypt.hash(x)` — an expression body is its return.
|
|
50
|
+
if (fn.type === 'ArrowFunctionExpression' && fn.expression) {
|
|
51
|
+
if (isAllowedCall(fn.body) || (fn.body.type === 'AwaitExpression' && isAllowedCall(fn.body.argument)))
|
|
52
|
+
frame.allowedReturns++;
|
|
53
|
+
else
|
|
54
|
+
frame.otherReturns++;
|
|
55
|
+
}
|
|
56
|
+
stack.push(frame);
|
|
57
|
+
};
|
|
58
|
+
const exit = (fn) => {
|
|
59
|
+
const frame = stack.pop();
|
|
60
|
+
const usesAllowed = frame.allowedAwaits + frame.allowedReturns > 0;
|
|
61
|
+
const qualifies = usesAllowed && frame.otherAwaits === 0;
|
|
62
|
+
if (fn.async && !qualifies)
|
|
63
|
+
report(fn, 'the async function');
|
|
64
|
+
const returnType = fn.returnType?.typeAnnotation;
|
|
65
|
+
const returnsPromise = returnType?.type === 'TSTypeReference' && returnType.typeName.name === 'Promise';
|
|
66
|
+
// A non-async function may return a Promise only when every return hands back an allowed call.
|
|
67
|
+
const passesThrough = !fn.async && frame.allowedReturns > 0 && frame.otherReturns === 0;
|
|
68
|
+
if (returnsPromise && !(fn.async ? qualifies : passesThrough))
|
|
69
|
+
report(returnType, 'the Promise');
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
ImportDeclaration(node) {
|
|
73
|
+
if (!allowedModules.includes(node.source.value))
|
|
74
|
+
return;
|
|
75
|
+
for (const specifier of node.specifiers) {
|
|
76
|
+
if (specifier.type === 'ImportSpecifier')
|
|
77
|
+
functions.add(specifier.local.name);
|
|
78
|
+
else
|
|
79
|
+
namespaces.add(specifier.local.name);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
':function': enter,
|
|
83
|
+
':function:exit': exit,
|
|
84
|
+
AwaitExpression(node) {
|
|
85
|
+
const frame = stack[stack.length - 1];
|
|
86
|
+
if (isAllowedCall(node.argument)) {
|
|
87
|
+
if (frame)
|
|
88
|
+
frame.allowedAwaits++;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (frame)
|
|
92
|
+
frame.otherAwaits++;
|
|
93
|
+
report(node, 'the await');
|
|
94
|
+
},
|
|
95
|
+
ReturnStatement(node) {
|
|
96
|
+
const frame = stack[stack.length - 1];
|
|
97
|
+
if (!frame || !node.argument)
|
|
98
|
+
return;
|
|
99
|
+
const value = node.argument.type === 'AwaitExpression' ? node.argument.argument : node.argument;
|
|
100
|
+
if (isAllowedCall(value))
|
|
101
|
+
frame.allowedReturns++;
|
|
102
|
+
else
|
|
103
|
+
frame.otherReturns++;
|
|
104
|
+
},
|
|
105
|
+
// Promise types anywhere except a function's own return type (judged on exit above).
|
|
106
|
+
'TSTypeReference[typeName.name="Promise"]'(node) {
|
|
107
|
+
const parent = node.parent; // TSTypeAnnotation
|
|
108
|
+
const owner = parent?.parent;
|
|
109
|
+
const isReturnType = parent?.type === 'TSTypeAnnotation' && FUNCTIONS.has(owner?.type) && owner.returnType === parent;
|
|
110
|
+
if (!isReturnType)
|
|
111
|
+
report(node, 'the Promise');
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
//# sourceMappingURL=no-async-in-domain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-async-in-domain.js","sourceRoot":"","sources":["../../src/rules/no-async-in-domain.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* the frontend imports wire types from the generated SDK (`api/client/types.d.ts`)
|
|
3
|
+
* instead of re-declaring them. Matches by name, so a renamed hand-written mirror slips through.
|
|
4
|
+
*/
|
|
5
|
+
declare const _default: import("eslint").Rule.RuleModule;
|
|
6
|
+
export default _default;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const node_fs_1 = tslib_1.__importDefault(require("node:fs"));
|
|
5
|
+
const rule_1 = require("../lib/rule");
|
|
6
|
+
/** typesFile → Set of type names it declares (per lint run). */
|
|
7
|
+
const sdkNames = new Map();
|
|
8
|
+
const namesIn = (typesFile) => {
|
|
9
|
+
if (!sdkNames.has(typesFile)) {
|
|
10
|
+
const source = node_fs_1.default.readFileSync(typesFile, 'utf8');
|
|
11
|
+
sdkNames.set(typesFile, new Set([...source.matchAll(/\b(?:interface|type|enum)\s+([A-Z]\w*)/g)].map((match) => match[1])));
|
|
12
|
+
}
|
|
13
|
+
return sdkNames.get(typesFile);
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* the frontend imports wire types from the generated SDK (`api/client/types.d.ts`)
|
|
17
|
+
* instead of re-declaring them. Matches by name, so a renamed hand-written mirror slips through.
|
|
18
|
+
*/
|
|
19
|
+
exports.default = (0, rule_1.defineRule)({
|
|
20
|
+
meta: {
|
|
21
|
+
type: 'problem',
|
|
22
|
+
docs: { description: 'Forbid re-declaring types that exist in the generated SDK' },
|
|
23
|
+
messages: { duplicate: '"{{name}}" already exists in the generated SDK — import it from api/client/types.' },
|
|
24
|
+
schema: [{
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: { typesFile: { type: 'string' } },
|
|
27
|
+
required: ['typesFile'],
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
}],
|
|
30
|
+
},
|
|
31
|
+
create(context) {
|
|
32
|
+
const names = namesIn(context.options[0].typesFile);
|
|
33
|
+
const check = (node) => {
|
|
34
|
+
if (names.has(node.id.name))
|
|
35
|
+
context.report({ node: node.id, messageId: 'duplicate', data: { name: node.id.name } });
|
|
36
|
+
};
|
|
37
|
+
return { TSInterfaceDeclaration: check, TSTypeAliasDeclaration: check, TSEnumDeclaration: check };
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
//# sourceMappingURL=no-duplicate-sdk-types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-duplicate-sdk-types.js","sourceRoot":"","sources":["../../src/rules/no-duplicate-sdk-types.ts"],"names":[],"mappings":";;;AAAA,8DAAyB;AACzB,sCAAyC;AAEzC,gEAAgE;AAChE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAE,CAAC;AAE3B,MAAM,OAAO,GAAG,CAAC,SAAS,EAAE,EAAE;IAC5B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,iBAAE,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAClD,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,yCAAyC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7H,CAAC;IACD,OAAO,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AACjC,CAAC,CAAC;AAEF;;;GAGG;AACH,kBAAe,IAAA,iBAAU,EAAC;IACxB,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,EAAE,WAAW,EAAE,2DAA2D,EAAE;QAClF,QAAQ,EAAE,EAAE,SAAS,EAAE,mFAAmF,EAAE;QAC5G,MAAM,EAAE,CAAC;gBACP,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;gBAC7C,QAAQ,EAAE,CAAC,WAAW,CAAC;gBACvB,oBAAoB,EAAE,KAAK;aAC5B,CAAC;KACH;IACD,MAAM,CAAC,OAAO;QACZ,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACpD,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,EAAE;YACrB,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;gBAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACvH,CAAC,CAAC;QACF,OAAO,EAAE,sBAAsB,EAAE,KAAK,EAAE,sBAAsB,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,CAAC;IACpG,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* the app layer must not map DTOs into entities field by field; the entity's static
|
|
3
|
+
* `fromDTO(...)` owns that. Flags `new Entity({...})` and `Entity.create({...})` for any value
|
|
4
|
+
* imported from a module's `domain/` (value objects in `domain/values` are exempt).
|
|
5
|
+
*/
|
|
6
|
+
declare const _default: import("eslint").Rule.RuleModule;
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const rule_1 = require("../lib/rule");
|
|
4
|
+
/**
|
|
5
|
+
* the app layer must not map DTOs into entities field by field; the entity's static
|
|
6
|
+
* `fromDTO(...)` owns that. Flags `new Entity({...})` and `Entity.create({...})` for any value
|
|
7
|
+
* imported from a module's `domain/` (value objects in `domain/values` are exempt).
|
|
8
|
+
*/
|
|
9
|
+
exports.default = (0, rule_1.defineRule)({
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'suggestion',
|
|
12
|
+
docs: { description: 'Forbid assembling domain entities from object literals in the app layer' },
|
|
13
|
+
messages: { assemble: 'Don\'t assemble "{{name}}" from an object literal here — move the mapping into {{name}}.fromDTO(...).' },
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
const entities = new Set();
|
|
18
|
+
const isEntityImport = (source) => /(^|\/)domain(\/|$)/.test(source) && !/\/domain\/values(\/|$)/.test(source);
|
|
19
|
+
const check = (call, name) => {
|
|
20
|
+
if (entities.has(name))
|
|
21
|
+
context.report({ node: call, messageId: 'assemble', data: { name } });
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
ImportDeclaration(node) {
|
|
25
|
+
if (node.importKind === 'type' || !isEntityImport(node.source.value))
|
|
26
|
+
return;
|
|
27
|
+
for (const specifier of node.specifiers) {
|
|
28
|
+
if (specifier.importKind !== 'type' && !/Error$/.test(specifier.local.name))
|
|
29
|
+
entities.add(specifier.local.name);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
'NewExpression[callee.type="Identifier"][arguments.0.type="ObjectExpression"]': (node) => check(node, node.callee.name),
|
|
33
|
+
'CallExpression[callee.property.name="create"][callee.object.type="Identifier"][arguments.0.type="ObjectExpression"]': (node) => check(node, node.callee.object.name),
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
//# sourceMappingURL=no-entity-assembly-in-app.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-entity-assembly-in-app.js","sourceRoot":"","sources":["../../src/rules/no-entity-assembly-in-app.ts"],"names":[],"mappings":";;AAAA,sCAAyC;AACzC;;;;GAIG;AACH,kBAAe,IAAA,iBAAU,EAAC;IACxB,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE,EAAE,WAAW,EAAE,yEAAyE,EAAE;QAChG,QAAQ,EAAE,EAAE,QAAQ,EAAE,uGAAuG,EAAE;QAC/H,MAAM,EAAE,EAAE;KACX;IACD,MAAM,CAAC,OAAO;QACZ,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAE,CAAC;QAC3B,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC/G,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;YAC3B,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;QAChG,CAAC,CAAC;QACF,OAAO;YACL,iBAAiB,CAAC,IAAI;gBACpB,IAAI,IAAI,CAAC,UAAU,KAAK,MAAM,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;oBAAE,OAAO;gBAC7E,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;oBACxC,IAAI,SAAS,CAAC,UAAU,KAAK,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC;wBAAE,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAClH,CAAC;YACH,CAAC;YACD,8EAA8E,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;YACvH,qHAAqH,EAAE,CAAC,IAAI,EAAE,EAAE,CAC9H,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;SACvC,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outside the domain, a loaded entity changes only through its own methods (`config.update(dto)`),
|
|
3
|
+
* never by assigning its fields (`config.billingCycle = dto.billingCycle`). Field writes skip the
|
|
4
|
+
* domain's invariants and, on @trackable / event-sourced aggregates, the events those methods emit.
|
|
5
|
+
*
|
|
6
|
+
* "Loaded entity" = a variable initialised from an awaited lookup on the service itself:
|
|
7
|
+
* `await this.findOne(…)`, `await this.<repository>.find…(…)` / `.get…(…)` / `.load…(…)`.
|
|
8
|
+
* Local accumulators and response objects are left alone.
|
|
9
|
+
*/
|
|
10
|
+
declare const _default: import("eslint").Rule.RuleModule;
|
|
11
|
+
export default _default;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const rule_1 = require("../lib/rule");
|
|
4
|
+
/**
|
|
5
|
+
* Outside the domain, a loaded entity changes only through its own methods (`config.update(dto)`),
|
|
6
|
+
* never by assigning its fields (`config.billingCycle = dto.billingCycle`). Field writes skip the
|
|
7
|
+
* domain's invariants and, on @trackable / event-sourced aggregates, the events those methods emit.
|
|
8
|
+
*
|
|
9
|
+
* "Loaded entity" = a variable initialised from an awaited lookup on the service itself:
|
|
10
|
+
* `await this.findOne(…)`, `await this.<repository>.find…(…)` / `.get…(…)` / `.load…(…)`.
|
|
11
|
+
* Local accumulators and response objects are left alone.
|
|
12
|
+
*/
|
|
13
|
+
exports.default = (0, rule_1.defineRule)({
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'suggestion',
|
|
16
|
+
docs: { description: 'Change loaded entities through domain methods only' },
|
|
17
|
+
messages: { mutation: '"{{variable}}" is a loaded entity — change it through a domain method (e.g. `{{variable}}.update(dto)`), not `{{variable}}.{{property}} = …`.' },
|
|
18
|
+
schema: [{
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: { loaderPattern: { type: 'string' } },
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
}],
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
const loader = new RegExp(context.options[0]?.loaderPattern ?? '^(findOne|find|get|load|require)');
|
|
26
|
+
const entities = new Set(); // scope variables
|
|
27
|
+
const isLoad = (init) => {
|
|
28
|
+
const call = init?.type === 'AwaitExpression' ? init.argument : null;
|
|
29
|
+
if (call?.type !== 'CallExpression' || call.callee.type !== 'MemberExpression')
|
|
30
|
+
return false;
|
|
31
|
+
const method = call.callee.property.name ?? '';
|
|
32
|
+
const target = call.callee.object;
|
|
33
|
+
// this.findOne(...) or this.<repo>.find...(...)
|
|
34
|
+
const onThis = target.type === 'ThisExpression' || (target.type === 'MemberExpression' && target.object.type === 'ThisExpression');
|
|
35
|
+
return onThis && loader.test(method);
|
|
36
|
+
};
|
|
37
|
+
const variableOf = (node, identifier) => {
|
|
38
|
+
let scope = context.sourceCode.getScope(node);
|
|
39
|
+
while (scope) {
|
|
40
|
+
const variable = scope.set.get(identifier.name);
|
|
41
|
+
if (variable)
|
|
42
|
+
return variable;
|
|
43
|
+
scope = scope.upper;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
const check = (node, target) => {
|
|
48
|
+
if (target?.type !== 'MemberExpression' || target.computed || target.object.type !== 'Identifier')
|
|
49
|
+
return;
|
|
50
|
+
const variable = variableOf(node, target.object);
|
|
51
|
+
if (variable && entities.has(variable)) {
|
|
52
|
+
context.report({ node, messageId: 'mutation', data: { variable: target.object.name, property: target.property.name } });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
VariableDeclarator(node) {
|
|
57
|
+
if (node.id.type === 'Identifier' && isLoad(node.init)) {
|
|
58
|
+
const variable = variableOf(node, node.id);
|
|
59
|
+
if (variable)
|
|
60
|
+
entities.add(variable);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
AssignmentExpression: (node) => check(node, node.left),
|
|
64
|
+
UpdateExpression: (node) => check(node, node.argument),
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
//# sourceMappingURL=no-entity-mutation-outside-domain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-entity-mutation-outside-domain.js","sourceRoot":"","sources":["../../src/rules/no-entity-mutation-outside-domain.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message (event) bodies are built by the domain — an aggregate method returns or emits them:
|
|
3
|
+
*
|
|
4
|
+
* // domain
|
|
5
|
+
* public changed(actorId: string): ServiceOfferChanged {
|
|
6
|
+
* return { uuid: v4(), timestamp: new Date(), name: MessageNames.ServiceOfferChanged, aggregateId: …, actorId, therapistId: … };
|
|
7
|
+
* }
|
|
8
|
+
* // app
|
|
9
|
+
* await this.messagePublisher.publish(offering.changed(authToken.userId));
|
|
10
|
+
*
|
|
11
|
+
* An object literal pinning `name: MessageNames.X` anywhere outside `allowedPaths` (default `/domain/`)
|
|
12
|
+
* is a message constructed in the wrong layer.
|
|
13
|
+
*/
|
|
14
|
+
declare const _default: import("eslint").Rule.RuleModule;
|
|
15
|
+
export default _default;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const rule_1 = require("../lib/rule");
|
|
4
|
+
const ast_1 = require("../lib/ast");
|
|
5
|
+
const messages_1 = require("../lib/messages");
|
|
6
|
+
/**
|
|
7
|
+
* Message (event) bodies are built by the domain — an aggregate method returns or emits them:
|
|
8
|
+
*
|
|
9
|
+
* // domain
|
|
10
|
+
* public changed(actorId: string): ServiceOfferChanged {
|
|
11
|
+
* return { uuid: v4(), timestamp: new Date(), name: MessageNames.ServiceOfferChanged, aggregateId: …, actorId, therapistId: … };
|
|
12
|
+
* }
|
|
13
|
+
* // app
|
|
14
|
+
* await this.messagePublisher.publish(offering.changed(authToken.userId));
|
|
15
|
+
*
|
|
16
|
+
* An object literal pinning `name: MessageNames.X` anywhere outside `allowedPaths` (default `/domain/`)
|
|
17
|
+
* is a message constructed in the wrong layer.
|
|
18
|
+
*/
|
|
19
|
+
exports.default = (0, rule_1.defineRule)({
|
|
20
|
+
meta: {
|
|
21
|
+
type: 'problem',
|
|
22
|
+
docs: { description: 'Construct message bodies only in the domain layer' },
|
|
23
|
+
messages: {
|
|
24
|
+
construct: '{{enumName}}.{{message}} is built here — the domain returns or emits message bodies; call the aggregate method that produces it.',
|
|
25
|
+
},
|
|
26
|
+
schema: [{
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: { allowedPaths: { type: 'array', items: { type: 'string' } }, enumName: { type: 'string' } },
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
}],
|
|
31
|
+
},
|
|
32
|
+
create(context) {
|
|
33
|
+
const { allowedPaths = ['/domain/'], enumName = 'MessageNames' } = context.options[0] ?? {};
|
|
34
|
+
if (allowedPaths.some((fragment) => (0, ast_1.filePath)(context).includes(fragment)))
|
|
35
|
+
return {};
|
|
36
|
+
return {
|
|
37
|
+
ObjectExpression(node) {
|
|
38
|
+
const property = (0, messages_1.nameProperty)(node, enumName);
|
|
39
|
+
if (property?.name)
|
|
40
|
+
context.report({ node, messageId: 'construct', data: { enumName, message: property.name } });
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=no-message-construction-outside-domain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-message-construction-outside-domain.js","sourceRoot":"","sources":["../../src/rules/no-message-construction-outside-domain.ts"],"names":[],"mappings":";;AAAA,sCAAyC;AACzC,oCAAsC;AACtC,8CAA+C;AAE/C;;;;;;;;;;;;GAYG;AACH,kBAAe,IAAA,iBAAU,EAAC;IACxB,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,EAAE,WAAW,EAAE,mDAAmD,EAAE;QAC1E,QAAQ,EAAE;YACR,SAAS,EAAE,kIAAkI;SAC9I;QACD,MAAM,EAAE,CAAC;gBACP,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;gBACxG,oBAAoB,EAAE,KAAK;aAC5B,CAAC;KACH;IACD,MAAM,CAAC,OAAO;QACZ,MAAM,EAAE,YAAY,GAAG,CAAC,UAAU,CAAC,EAAE,QAAQ,GAAG,cAAc,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5F,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,QAAgB,EAAE,EAAE,CAAC,IAAA,cAAQ,EAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAAE,OAAO,EAAE,CAAC;QAC7F,OAAO;YACL,gBAAgB,CAAC,IAAI;gBACnB,MAAM,QAAQ,GAAG,IAAA,uBAAY,EAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;gBAC9C,IAAI,QAAQ,EAAE,IAAI;oBAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACnH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const rule_1 = require("../lib/rule");
|
|
4
|
+
/** entities have no `reconstitute`; repositories deserialize, services call `existing.update()`. */
|
|
5
|
+
exports.default = (0, rule_1.defineRule)({
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'suggestion',
|
|
8
|
+
docs: { description: 'Forbid defining or calling `reconstitute`' },
|
|
9
|
+
messages: {
|
|
10
|
+
definition: 'Entities must not have `reconstitute` — the repository deserializes into the constructor.',
|
|
11
|
+
call: 'Don\'t rebuild entities with `reconstitute` — fetch, call `existing.update(dto)`, save.',
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
'MethodDefinition[key.name="reconstitute"]': (node) => context.report({ node: node.key, messageId: 'definition' }),
|
|
18
|
+
'CallExpression[callee.property.name="reconstitute"]': (node) => context.report({ node, messageId: 'call' }),
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
//# sourceMappingURL=no-reconstitute.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-reconstitute.js","sourceRoot":"","sources":["../../src/rules/no-reconstitute.ts"],"names":[],"mappings":";;AAAA,sCAAyC;AACzC,oGAAoG;AACpG,kBAAe,IAAA,iBAAU,EAAC;IACxB,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE,EAAE,WAAW,EAAE,2CAA2C,EAAE;QAClE,QAAQ,EAAE;YACR,UAAU,EAAE,2FAA2F;YACvG,IAAI,EAAE,yFAAyF;SAChG;QACD,MAAM,EAAE,EAAE;KACX;IACD,MAAM,CAAC,OAAO;QACZ,OAAO;YACL,2CAA2C,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;YAClH,qDAAqD,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;SAC7G,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ports live one per file in `src/modules/<module>/app/ports/<Name>Port.ts`, each declared as an
|
|
3
|
+
* exported interface named exactly like the file. Supporting types (DTO-ish interfaces the port
|
|
4
|
+
* signature uses) may sit next to it; runtime code (classes, functions, values) may not — that is an
|
|
5
|
+
* adapter. `index.ts` barrels in `app/ports/` are allowed. Outside `app/ports/`, nothing may declare
|
|
6
|
+
* a `…Port` type.
|
|
7
|
+
*/
|
|
8
|
+
declare const _default: import("eslint").Rule.RuleModule;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const node_path_1 = tslib_1.__importDefault(require("node:path"));
|
|
5
|
+
const ast_1 = require("../lib/ast");
|
|
6
|
+
const rule_1 = require("../lib/rule");
|
|
7
|
+
const PORT_NAME = /Port$/;
|
|
8
|
+
const TYPE_ONLY = new Set(['TSInterfaceDeclaration', 'TSTypeAliasDeclaration', 'ImportDeclaration', 'ExportAllDeclaration']);
|
|
9
|
+
/** The declaration behind a top-level statement (`export interface X` → `interface X`). */
|
|
10
|
+
const declarationOf = (statement) => statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration' ? statement.declaration : statement;
|
|
11
|
+
/**
|
|
12
|
+
* Ports live one per file in `src/modules/<module>/app/ports/<Name>Port.ts`, each declared as an
|
|
13
|
+
* exported interface named exactly like the file. Supporting types (DTO-ish interfaces the port
|
|
14
|
+
* signature uses) may sit next to it; runtime code (classes, functions, values) may not — that is an
|
|
15
|
+
* adapter. `index.ts` barrels in `app/ports/` are allowed. Outside `app/ports/`, nothing may declare
|
|
16
|
+
* a `…Port` type.
|
|
17
|
+
*/
|
|
18
|
+
exports.default = (0, rule_1.defineRule)({
|
|
19
|
+
meta: {
|
|
20
|
+
type: 'suggestion',
|
|
21
|
+
docs: { description: 'Enforce the location, shape and naming of port files' },
|
|
22
|
+
messages: {
|
|
23
|
+
fileName: 'Port files are named "<Name>Port.ts" — "{{file}}.ts" is not.',
|
|
24
|
+
missing: '"{{file}}.ts" must declare `export interface {{file}}`.',
|
|
25
|
+
notInterface: 'Port "{{name}}" must be an interface, not a {{kind}}.',
|
|
26
|
+
notExported: 'Port "{{name}}" must be exported.',
|
|
27
|
+
onePerFile: 'Port "{{name}}" must live in its own file app/ports/{{name}}.ts (one port per file).',
|
|
28
|
+
runtime: 'Port files contain only types — move this {{kind}} to infrastructure/ (it is an adapter).',
|
|
29
|
+
location: 'Port "{{name}}" must be declared in src/modules/<module>/app/ports/{{name}}.ts.',
|
|
30
|
+
},
|
|
31
|
+
schema: [],
|
|
32
|
+
},
|
|
33
|
+
create(context) {
|
|
34
|
+
const location = (0, ast_1.moduleLocation)(context);
|
|
35
|
+
const inPorts = location?.rest[0] === 'app' && location.rest[1] === 'ports' && location.rest.length === 3;
|
|
36
|
+
const file = node_path_1.default.basename(context.filename).replace(/\.ts$/, '');
|
|
37
|
+
if (!inPorts) {
|
|
38
|
+
// Anywhere else: a `…Port` declaration is a port in the wrong place.
|
|
39
|
+
const report = (node) => {
|
|
40
|
+
if (node.id && PORT_NAME.test(node.id.name))
|
|
41
|
+
context.report({ node: node.id, messageId: 'location', data: { name: node.id.name } });
|
|
42
|
+
};
|
|
43
|
+
return { TSInterfaceDeclaration: report, TSTypeAliasDeclaration: report, ClassDeclaration: report };
|
|
44
|
+
}
|
|
45
|
+
if (file === 'index')
|
|
46
|
+
return {};
|
|
47
|
+
return {
|
|
48
|
+
Program(program) {
|
|
49
|
+
if (!PORT_NAME.test(file)) {
|
|
50
|
+
context.report({ node: program, loc: { line: 1, column: 0 }, messageId: 'fileName', data: { file } });
|
|
51
|
+
}
|
|
52
|
+
let found = false;
|
|
53
|
+
for (const statement of program.body) {
|
|
54
|
+
const declaration = declarationOf(statement);
|
|
55
|
+
const exported = declaration !== statement;
|
|
56
|
+
// Re-exports (`export { X } from …`, `export type { X }`) carry no declaration of their own.
|
|
57
|
+
if (!declaration)
|
|
58
|
+
continue;
|
|
59
|
+
if (!TYPE_ONLY.has(declaration.type) && declaration.type !== 'TSModuleDeclaration') {
|
|
60
|
+
const kind = { ClassDeclaration: 'class', FunctionDeclaration: 'function', VariableDeclaration: 'value', TSEnumDeclaration: 'enum' }[declaration.type] ?? 'statement';
|
|
61
|
+
if (declaration.id?.name === file) {
|
|
62
|
+
// `export abstract class XPort` — the port itself, declared with the wrong construct.
|
|
63
|
+
found = true;
|
|
64
|
+
context.report({ node: declaration.id, messageId: 'notInterface', data: { name: file, kind } });
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
context.report({ node: declaration, messageId: 'runtime', data: { kind } });
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const name = declaration.id?.name;
|
|
72
|
+
if (!name)
|
|
73
|
+
continue;
|
|
74
|
+
if (name === file) {
|
|
75
|
+
found = true;
|
|
76
|
+
if (declaration.type !== 'TSInterfaceDeclaration') {
|
|
77
|
+
context.report({ node: declaration.id, messageId: 'notInterface', data: { name, kind: 'type alias' } });
|
|
78
|
+
}
|
|
79
|
+
else if (!exported) {
|
|
80
|
+
context.report({ node: declaration.id, messageId: 'notExported', data: { name } });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else if (PORT_NAME.test(name)) {
|
|
84
|
+
context.report({ node: declaration.id, messageId: 'onePerFile', data: { name } });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!found && PORT_NAME.test(file)) {
|
|
88
|
+
context.report({ node: program, loc: { line: 1, column: 0 }, messageId: 'missing', data: { file } });
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
//# sourceMappingURL=port-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-file.js","sourceRoot":"","sources":["../../src/rules/port-file.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every `<X>RepositoryPort` declared in `app/ports/` has at least one implementation in the module's
|
|
3
|
+
* `infrastructure/`: a class that `implements` the port, or a prefixed `…<X>Repository`
|
|
4
|
+
* (`PgXRepository`, `PgNatsXRepository`, `InMemoryXRepository`, …). Which prefix is right for its
|
|
5
|
+
* folder is `repository-naming`'s job.
|
|
6
|
+
* Reads sibling files, so editors only re-check it when the port file itself is re-linted.
|
|
7
|
+
*/
|
|
8
|
+
declare const _default: import("eslint").Rule.RuleModule;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const node_fs_1 = tslib_1.__importDefault(require("node:fs"));
|
|
5
|
+
const node_path_1 = tslib_1.__importDefault(require("node:path"));
|
|
6
|
+
const ast_1 = require("../lib/ast");
|
|
7
|
+
const rule_1 = require("../lib/rule");
|
|
8
|
+
/** infrastructure dir → [{ name, implements: Set<string> }] for every class declared in it (per lint run). */
|
|
9
|
+
const infrastructureClasses = new Map();
|
|
10
|
+
const classesIn = (dir) => {
|
|
11
|
+
if (!infrastructureClasses.has(dir)) {
|
|
12
|
+
const classes = [];
|
|
13
|
+
const walk = (current) => {
|
|
14
|
+
if (!node_fs_1.default.existsSync(current))
|
|
15
|
+
return;
|
|
16
|
+
for (const entry of node_fs_1.default.readdirSync(current, { withFileTypes: true })) {
|
|
17
|
+
const full = node_path_1.default.join(current, entry.name);
|
|
18
|
+
if (entry.isDirectory())
|
|
19
|
+
walk(full);
|
|
20
|
+
else if (/\.ts$/.test(entry.name) && !/\.test\.ts$/.test(entry.name)) {
|
|
21
|
+
for (const match of node_fs_1.default.readFileSync(full, 'utf8').matchAll(/\bclass\s+(\w+)([^{]*)\{/g)) {
|
|
22
|
+
const implemented = match[2].match(/\bimplements\s+([\w\s,<>.]+)/)?.[1] ?? '';
|
|
23
|
+
classes.push({ name: match[1], implements: new Set(implemented.match(/\w+/g) ?? []) });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
walk(dir);
|
|
29
|
+
infrastructureClasses.set(dir, classes);
|
|
30
|
+
}
|
|
31
|
+
return infrastructureClasses.get(dir);
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Every `<X>RepositoryPort` declared in `app/ports/` has at least one implementation in the module's
|
|
35
|
+
* `infrastructure/`: a class that `implements` the port, or a prefixed `…<X>Repository`
|
|
36
|
+
* (`PgXRepository`, `PgNatsXRepository`, `InMemoryXRepository`, …). Which prefix is right for its
|
|
37
|
+
* folder is `repository-naming`'s job.
|
|
38
|
+
* Reads sibling files, so editors only re-check it when the port file itself is re-linted.
|
|
39
|
+
*/
|
|
40
|
+
exports.default = (0, rule_1.defineRule)({
|
|
41
|
+
meta: {
|
|
42
|
+
type: 'suggestion',
|
|
43
|
+
docs: { description: 'Require every repository port to have at least one infrastructure implementation' },
|
|
44
|
+
messages: { missing: 'Port "{{port}}" has no implementation in {{module}}/infrastructure/ (expected e.g. Pg{{base}}).' },
|
|
45
|
+
schema: [],
|
|
46
|
+
},
|
|
47
|
+
create(context) {
|
|
48
|
+
const location = (0, ast_1.moduleLocation)(context);
|
|
49
|
+
if (!location || location.rest[0] !== 'app' || location.rest[1] !== 'ports')
|
|
50
|
+
return {};
|
|
51
|
+
return {
|
|
52
|
+
'TSInterfaceDeclaration[id.name=/RepositoryPort$/]'(node) {
|
|
53
|
+
const port = node.id.name;
|
|
54
|
+
const base = port.replace(/Port$/, '');
|
|
55
|
+
const implemented = classesIn(node_path_1.default.join(location.root, 'infrastructure')).some((cls) => cls.implements.has(port) || (cls.name !== base && cls.name.endsWith(base) && /^[A-Z]/.test(cls.name)));
|
|
56
|
+
if (!implemented)
|
|
57
|
+
context.report({ node: node.id, messageId: 'missing', data: { port, base, module: location.module } });
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
//# sourceMappingURL=port-has-adapters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-has-adapters.js","sourceRoot":"","sources":["../../src/rules/port-has-adapters.ts"],"names":[],"mappings":"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"}
|