@ddd-ts-kit/eslint-plugin 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/project/config.d.ts +6 -0
- package/dist/project/config.d.ts.map +1 -1
- package/dist/project/is-judged.d.ts +5 -0
- package/dist/project/is-judged.d.ts.map +1 -0
- package/dist/project/is-judged.js +7 -0
- package/dist/project/is-judged.js.map +1 -0
- package/dist/project/parse-config.d.ts.map +1 -1
- package/dist/project/parse-config.js +6 -0
- package/dist/project/parse-config.js.map +1 -1
- package/dist/project/resolve-import.d.ts.map +1 -1
- package/dist/project/resolve-import.js +4 -1
- package/dist/project/resolve-import.js.map +1 -1
- package/dist/project/specifier-suffix.d.ts +6 -0
- package/dist/project/specifier-suffix.d.ts.map +1 -0
- package/dist/project/specifier-suffix.js +8 -0
- package/dist/project/specifier-suffix.js.map +1 -0
- package/dist/project/to-alias.d.ts +3 -2
- package/dist/project/to-alias.d.ts.map +1 -1
- package/dist/project/to-alias.js +4 -3
- package/dist/project/to-alias.js.map +1 -1
- package/dist/rules/clean-one-export-per-file.js +7 -3
- package/dist/rules/clean-one-export-per-file.js.map +1 -1
- package/dist/rules/declarations-constructor-injection.d.ts +5 -0
- package/dist/rules/declarations-constructor-injection.d.ts.map +1 -0
- package/dist/rules/declarations-constructor-injection.js +80 -0
- package/dist/rules/declarations-constructor-injection.js.map +1 -0
- package/dist/rules/domain-no-generic-error.js +1 -1
- package/dist/rules/domain-no-generic-error.js.map +1 -1
- package/dist/rules/errors-constructor-not-public.d.ts +2 -0
- package/dist/rules/errors-constructor-not-public.d.ts.map +1 -0
- package/dist/rules/errors-constructor-not-public.js +41 -0
- package/dist/rules/errors-constructor-not-public.js.map +1 -0
- package/dist/rules/errors-factory-name.d.ts +4 -0
- package/dist/rules/errors-factory-name.d.ts.map +1 -0
- package/dist/rules/errors-factory-name.js +88 -0
- package/dist/rules/errors-factory-name.js.map +1 -0
- package/dist/rules/errors-through-factory.d.ts +2 -0
- package/dist/rules/errors-through-factory.d.ts.map +1 -0
- package/dist/rules/errors-through-factory.js +39 -0
- package/dist/rules/errors-through-factory.js.map +1 -0
- package/dist/rules/imports-alias-only.d.ts.map +1 -1
- package/dist/rules/imports-alias-only.js +7 -9
- package/dist/rules/imports-alias-only.js.map +1 -1
- package/dist/rules/imports-extension.d.ts +5 -0
- package/dist/rules/imports-extension.d.ts.map +1 -0
- package/dist/rules/imports-extension.js +45 -0
- package/dist/rules/imports-extension.js.map +1 -0
- package/dist/rules/ports-no-concrete-dependency.d.ts +3 -1
- package/dist/rules/ports-no-concrete-dependency.d.ts.map +1 -1
- package/dist/rules/ports-no-concrete-dependency.js +51 -3
- package/dist/rules/ports-no-concrete-dependency.js.map +1 -1
- package/dist/rules/rules.d.ts.map +1 -1
- package/dist/rules/rules.js +10 -0
- package/dist/rules/rules.js.map +1 -1
- package/dist/support/declarations/declared-roles.d.ts.map +1 -1
- package/dist/support/declarations/declared-roles.js +1 -0
- package/dist/support/declarations/declared-roles.js.map +1 -1
- package/dist/support/errors/constant-case.d.ts +3 -0
- package/dist/support/errors/constant-case.d.ts.map +1 -0
- package/dist/support/errors/constant-case.js +6 -0
- package/dist/support/errors/constant-case.js.map +1 -0
- package/dist/support/errors/is-error-class.d.ts +4 -0
- package/dist/support/errors/is-error-class.d.ts.map +1 -0
- package/dist/support/errors/is-error-class.js +6 -0
- package/dist/support/errors/is-error-class.js.map +1 -0
- package/dist/support/errors/is-error-file.d.ts +4 -0
- package/dist/support/errors/is-error-file.d.ts.map +1 -0
- package/dist/support/errors/is-error-file.js +10 -0
- package/dist/support/errors/is-error-file.js.map +1 -0
- package/dist/support/injection/language-types.d.ts +3 -0
- package/dist/support/injection/language-types.d.ts.map +1 -0
- package/dist/support/injection/language-types.js +20 -0
- package/dist/support/injection/language-types.js.map +1 -0
- package/dist/support/injection/not-classes.d.ts +4 -0
- package/dist/support/injection/not-classes.d.ts.map +1 -0
- package/dist/support/injection/not-classes.js +20 -0
- package/dist/support/injection/not-classes.js.map +1 -0
- package/dist/support/injection/parameter-reading.d.ts +9 -0
- package/dist/support/injection/parameter-reading.d.ts.map +1 -0
- package/dist/support/injection/parameter-reading.js +2 -0
- package/dist/support/injection/parameter-reading.js.map +1 -0
- package/dist/support/injection/read-parameter.d.ts +9 -0
- package/dist/support/injection/read-parameter.d.ts.map +1 -0
- package/dist/support/injection/read-parameter.js +41 -0
- package/dist/support/injection/read-parameter.js.map +1 -0
- package/dist/support/injection/written-name.d.ts +4 -0
- package/dist/support/injection/written-name.d.ts.map +1 -0
- package/dist/support/injection/written-name.js +13 -0
- package/dist/support/injection/written-name.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/rules/clean-one-export-per-file.md +1 -1
- package/docs/rules/context-declared-relationship.md +2 -2
- package/docs/rules/context-extractable.md +3 -3
- package/docs/rules/context-published-language-is-a-contract.md +1 -1
- package/docs/rules/context-published-language-only.md +2 -2
- package/docs/rules/declarations-constructor-injection.md +89 -0
- package/docs/rules/declarations-role-declared.md +15 -9
- package/docs/rules/domain-events-transactional-before-commit.md +1 -1
- package/docs/rules/errors-constructor-not-public.md +53 -0
- package/docs/rules/errors-factory-name.md +50 -0
- package/docs/rules/errors-through-factory.md +45 -0
- package/docs/rules/imports-alias-only.md +10 -9
- package/docs/rules/imports-extension.md +83 -0
- package/docs/rules/layer-application-inward.md +3 -3
- package/docs/rules/layer-domain-purity.md +3 -3
- package/docs/rules/layer-infrastructure-no-presentation.md +2 -2
- package/docs/rules/layer-presentation-no-infrastructure.md +2 -2
- package/docs/rules/ports-no-concrete-dependency.md +4 -3
- package/docs/rules/test-no-mocks.md +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
import { LANGUAGE_TYPES } from './language-types.js';
|
|
3
|
+
const ALWAYS_GIVEN = 'the composition root gives every parameter a value, always';
|
|
4
|
+
/**
|
|
5
|
+
* Reads one constructor parameter of a declared class. `notClasses` are the names the file
|
|
6
|
+
* declares as an interface, a type alias or an enum: what another file declares is read by
|
|
7
|
+
* `ddd-ts-kit check`, which sees the project.
|
|
8
|
+
*/
|
|
9
|
+
export function readParameter(node, notClasses) {
|
|
10
|
+
const parameter = node.type === AST_NODE_TYPES.TSParameterProperty ? node.parameter : node;
|
|
11
|
+
if (parameter.type === AST_NODE_TYPES.RestElement) {
|
|
12
|
+
return { flaw: `is a rest parameter: ${ALWAYS_GIVEN}` };
|
|
13
|
+
}
|
|
14
|
+
if (parameter.type === AST_NODE_TYPES.AssignmentPattern) {
|
|
15
|
+
return { flaw: `has a default value: ${ALWAYS_GIVEN}` };
|
|
16
|
+
}
|
|
17
|
+
if (parameter.type !== AST_NODE_TYPES.Identifier) {
|
|
18
|
+
return { flaw: `is destructured: ${ALWAYS_GIVEN}` };
|
|
19
|
+
}
|
|
20
|
+
if (parameter.optional) {
|
|
21
|
+
return { flaw: `is optional: ${ALWAYS_GIVEN}` };
|
|
22
|
+
}
|
|
23
|
+
return typed(parameter.typeAnnotation?.typeAnnotation, notClasses);
|
|
24
|
+
}
|
|
25
|
+
function typed(type, notClasses) {
|
|
26
|
+
if (type === undefined) {
|
|
27
|
+
return { flaw: 'has no type' };
|
|
28
|
+
}
|
|
29
|
+
if (type.type !== AST_NODE_TYPES.TSTypeReference || type.typeName.type !== AST_NODE_TYPES.Identifier) {
|
|
30
|
+
return { flaw: 'is typed by something that names no single class' };
|
|
31
|
+
}
|
|
32
|
+
const { name } = type.typeName;
|
|
33
|
+
const what = notClasses.get(name);
|
|
34
|
+
if (what !== undefined) {
|
|
35
|
+
return { flaw: `is typed \`${name}\`, ${what}: only a class exists at run time and can be an injection token` };
|
|
36
|
+
}
|
|
37
|
+
return LANGUAGE_TYPES.has(name)
|
|
38
|
+
? { flaw: `is typed \`${name}\`, a type of the language: it names nothing the composition root binds` }
|
|
39
|
+
: { token: name };
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=read-parameter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-parameter.js","sourceRoot":"","sources":["../../../src/support/injection/read-parameter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAiB,MAAM,0BAA0B,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGrD,MAAM,YAAY,GAAG,4DAA4D,CAAC;AAElF;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,IAAwB,EAAE,UAAuC;IAC7F,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,KAAK,cAAc,CAAC,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;IAE3F,IAAI,SAAS,CAAC,IAAI,KAAK,cAAc,CAAC,WAAW,EAAE,CAAC;QAClD,OAAO,EAAE,IAAI,EAAE,wBAAwB,YAAY,EAAE,EAAE,CAAC;IAC1D,CAAC;IAED,IAAI,SAAS,CAAC,IAAI,KAAK,cAAc,CAAC,iBAAiB,EAAE,CAAC;QACxD,OAAO,EAAE,IAAI,EAAE,wBAAwB,YAAY,EAAE,EAAE,CAAC;IAC1D,CAAC;IAED,IAAI,SAAS,CAAC,IAAI,KAAK,cAAc,CAAC,UAAU,EAAE,CAAC;QACjD,OAAO,EAAE,IAAI,EAAE,oBAAoB,YAAY,EAAE,EAAE,CAAC;IACtD,CAAC;IAED,IAAI,SAAS,CAAC,QAAQ,EAAE,CAAC;QACvB,OAAO,EAAE,IAAI,EAAE,gBAAgB,YAAY,EAAE,EAAE,CAAC;IAClD,CAAC;IAED,OAAO,KAAK,CAAC,SAAS,CAAC,cAAc,EAAE,cAAc,EAAE,UAAU,CAAC,CAAC;AACrE,CAAC;AAED,SAAS,KAAK,CAAC,IAAmC,EAAE,UAAuC;IACzF,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;IACjC,CAAC;IAED,IAAI,IAAI,CAAC,IAAI,KAAK,cAAc,CAAC,eAAe,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,cAAc,CAAC,UAAU,EAAE,CAAC;QACrG,OAAO,EAAE,IAAI,EAAE,kDAAkD,EAAE,CAAC;IACtE,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC;IAC/B,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAElC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,EAAE,IAAI,EAAE,cAAc,IAAI,OAAO,IAAI,iEAAiE,EAAE,CAAC;IAClH,CAAC;IAED,OAAO,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC;QAC7B,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,IAAI,yEAAyE,EAAE;QACvG,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"written-name.d.ts","sourceRoot":"","sources":["../../../src/support/injection/written-name.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEzE,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,SAAS,GAAG,MAAM,CAW5D"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
/** The name a constructor parameter is written under, or its source shape when it has none. */
|
|
3
|
+
export function writtenName(node) {
|
|
4
|
+
const parameter = node.type === AST_NODE_TYPES.TSParameterProperty ? node.parameter : node;
|
|
5
|
+
const target = parameter.type === AST_NODE_TYPES.AssignmentPattern ? parameter.left : parameter;
|
|
6
|
+
if (target.type === AST_NODE_TYPES.Identifier) {
|
|
7
|
+
return target.name;
|
|
8
|
+
}
|
|
9
|
+
return target.type === AST_NODE_TYPES.RestElement && target.argument.type === AST_NODE_TYPES.Identifier
|
|
10
|
+
? target.argument.name
|
|
11
|
+
: 'the destructured one';
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=written-name.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"written-name.js","sourceRoot":"","sources":["../../../src/support/injection/written-name.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAiB,MAAM,0BAA0B,CAAC;AAEzE,+FAA+F;AAC/F,MAAM,UAAU,WAAW,CAAC,IAAwB;IAClD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,KAAK,cAAc,CAAC,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3F,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,KAAK,cAAc,CAAC,iBAAiB,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAEhG,IAAI,MAAM,CAAC,IAAI,KAAK,cAAc,CAAC,UAAU,EAAE,CAAC;QAC9C,OAAO,MAAM,CAAC,IAAI,CAAC;IACrB,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,KAAK,cAAc,CAAC,WAAW,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,KAAK,cAAc,CAAC,UAAU;QACrG,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI;QACtB,CAAC,CAAC,sBAAsB,CAAC;AAC7B,CAAC"}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.3.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.3.0';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
|
@@ -42,7 +42,7 @@ export function retry(options: RetryOptions): Aspect {}
|
|
|
42
42
|
export class RabbitMqEventBus {}
|
|
43
43
|
|
|
44
44
|
// src/platform/index.ts
|
|
45
|
-
export { retry, type RetryOptions } from '@/platform/retry
|
|
45
|
+
export { retry, type RetryOptions } from '@/platform/retry';
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
## Details
|
|
@@ -17,14 +17,14 @@ Incorrect:
|
|
|
17
17
|
```ts
|
|
18
18
|
// ddd-ts-kit.config.json: "shipping": { "upstreams": [] }
|
|
19
19
|
// src/contexts/shipping/application/integration/handlers/billing-invoice-issued.integration-handler.ts
|
|
20
|
-
import type { InvoiceIssuedV1 } from '@/contexts/billing/published-language/index
|
|
20
|
+
import type { InvoiceIssuedV1 } from '@/contexts/billing/published-language/index';
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Correct:
|
|
24
24
|
|
|
25
25
|
```ts
|
|
26
26
|
// ddd-ts-kit.config.json: "shipping": { "upstreams": [{ "context": "billing", "relationship": "published-language" }] }
|
|
27
|
-
import type { InvoiceIssuedV1 } from '@/contexts/billing/published-language/index
|
|
27
|
+
import type { InvoiceIssuedV1 } from '@/contexts/billing/published-language/index';
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
## Details
|
|
@@ -41,11 +41,11 @@ export function remember(currency: string, rate: number): void {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// billing/billing.module.ts
|
|
44
|
-
import { ProductReader } from '@/contexts/catalog/application/ports/product-reader.port
|
|
44
|
+
import { ProductReader } from '@/contexts/catalog/application/ports/product-reader.port';
|
|
45
45
|
builder.query(GetPrice).public().handledBy(GetPriceHandler, [ProductReader]);
|
|
46
46
|
|
|
47
47
|
// declared style: billing/application/queries/get-price/get-price.handler.ts
|
|
48
|
-
import { ProductReader } from '@/contexts/catalog/application/ports/product-reader.port
|
|
48
|
+
import { ProductReader } from '@/contexts/catalog/application/ports/product-reader.port';
|
|
49
49
|
|
|
50
50
|
@QueryHandler(GetPriceQuery, [ProductReader])
|
|
51
51
|
export class GetPriceHandler {}
|
|
@@ -62,7 +62,7 @@ builder.bind(PriceSource).toClass(InProcessPriceSource, [QueryBus]).as('gateway'
|
|
|
62
62
|
builder.query(GetPrice).public().handledBy(GetPriceHandler, [PriceSource]);
|
|
63
63
|
|
|
64
64
|
// declared style: the port is one of billing, its gateway calls catalog
|
|
65
|
-
import { PriceCatalog } from '@/contexts/billing/application/ports/price-catalog.port
|
|
65
|
+
import { PriceCatalog } from '@/contexts/billing/application/ports/price-catalog.port';
|
|
66
66
|
|
|
67
67
|
@QueryHandler(GetPriceQuery, [PriceCatalog])
|
|
68
68
|
export class GetPriceHandler {}
|
|
@@ -16,7 +16,7 @@ Incorrect:
|
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
18
|
// src/contexts/billing/published-language/invoice-issued.v1.integration-event.ts
|
|
19
|
-
import type { Invoice } from '@/contexts/billing/domain/aggregates/invoice.aggregate
|
|
19
|
+
import type { Invoice } from '@/contexts/billing/domain/aggregates/invoice.aggregate';
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Correct:
|
|
@@ -16,13 +16,13 @@ Incorrect:
|
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
18
|
// src/contexts/billing/application/...
|
|
19
|
-
import { Product } from '@/contexts/catalog/domain/aggregates/product.aggregate
|
|
19
|
+
import { Product } from '@/contexts/catalog/domain/aggregates/product.aggregate';
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Correct:
|
|
23
23
|
|
|
24
24
|
```ts
|
|
25
|
-
import type { ProductListedV1 } from '@/contexts/catalog/published-language/index
|
|
25
|
+
import type { ProductListedV1 } from '@/contexts/catalog/published-language/index';
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
## Details
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# ddd-ts-kit/declarations-constructor-injection
|
|
2
|
+
|
|
3
|
+
Architecture id: `declarations/constructor-injection`. Default severity: `error` in `recommended`, `error` in `strict`.
|
|
4
|
+
|
|
5
|
+
Applies to: every role file of a context in a declared-style project (`stack.style: "declared"` in `ddd-ts-kit.config.json`), tests excluded. Nothing is reported in an explicit-style project, where the module file lists what each class is given.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
The constructor signature is the one statement of what a declared class depends on. `ddd-ts-kit docs` reads it and writes, in the generated file of the context, the list the composition root injects: nothing is repeated in the decorator, and nothing can disagree with the constructor. That only works when each parameter names exactly one injection token, and a token is a class: an interface, a type alias or a primitive is erased by the compiler; a union names two things; an optional or defaulted parameter says the container might give nothing, and it always gives a value.
|
|
10
|
+
|
|
11
|
+
The compiler is not asked for the types at run time. `emitDecoratorMetadata` exists only with legacy decorators compiled by `tsc` or by swc with its option: standard decorators have none, `tsx` and esbuild emit none, and a port imported with `import type` is recorded as `Object`.
|
|
12
|
+
|
|
13
|
+
## Reported
|
|
14
|
+
|
|
15
|
+
On the class of a role file that carries a declaration decorator other than `@Requires` and `@Public`, when the decorator lists no dependency:
|
|
16
|
+
|
|
17
|
+
- a constructor parameter that is optional, has a default value, is a rest parameter or is destructured
|
|
18
|
+
- a constructor parameter without a type, or typed by anything but one class name, type arguments aside (`Cache<Session>` names `Cache`): a union, an intersection, an array, a function, a literal, an object type, a qualified name
|
|
19
|
+
- a constructor parameter typed by a type of the language (`string`, `number`, `Date`, `Map`...) or by an interface, a type alias or an enum the file declares
|
|
20
|
+
|
|
21
|
+
Under `"stack.dependencies": "signature"`, when the decorator carries a list and the class writes its constructor (or extends nothing):
|
|
22
|
+
|
|
23
|
+
- a list that names what the constructor is typed by, in order: it is redundant, and the fix removes it
|
|
24
|
+
- a list that differs from the constructor: it wins at run time, so the signature lies; no fix
|
|
25
|
+
|
|
26
|
+
Incorrect:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
@CommandHandler(IssueInvoiceCommand)
|
|
30
|
+
export class IssueInvoiceHandler implements CommandHandler<IssueInvoiceCommand> {
|
|
31
|
+
constructor(
|
|
32
|
+
private readonly invoices: InvoiceRepository | undefined,
|
|
33
|
+
private readonly retries = 3,
|
|
34
|
+
private readonly currency: string,
|
|
35
|
+
) {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// "stack.dependencies": "signature"
|
|
39
|
+
@CommandHandler(IssueInvoiceCommand, [InvoiceRepository, Clock])
|
|
40
|
+
export class IssueInvoiceHandler implements CommandHandler<IssueInvoiceCommand> {
|
|
41
|
+
constructor(
|
|
42
|
+
private readonly invoices: InvoiceRepository,
|
|
43
|
+
private readonly clock: Clock,
|
|
44
|
+
) {}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Correct:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import type { Clock } from '@ddd-ts-kit/kernel';
|
|
52
|
+
import { CommandHandler } from '@ddd-ts-kit/kernel/declarations';
|
|
53
|
+
import type { InvoiceSettings } from '@/contexts/billing/application/ports/invoice-settings.port';
|
|
54
|
+
import type { InvoiceRepository } from '@/contexts/billing/domain/repositories/invoice.repository';
|
|
55
|
+
|
|
56
|
+
@CommandHandler(IssueInvoiceCommand)
|
|
57
|
+
export class IssueInvoiceHandler implements CommandHandler<IssueInvoiceCommand> {
|
|
58
|
+
constructor(
|
|
59
|
+
private readonly invoices: InvoiceRepository,
|
|
60
|
+
private readonly settings: InvoiceSettings, // an abstract class with readonly members, bound to a value in the platform
|
|
61
|
+
private readonly clock: Clock,
|
|
62
|
+
) {}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Details
|
|
67
|
+
|
|
68
|
+
A parameter decorator of another library and the modifiers `public`, `private`, `protected`, `readonly` and `override` are looked through. A class that writes no constructor and extends nothing takes nothing.
|
|
69
|
+
|
|
70
|
+
The fix of a redundant list removes the list and the comma before it: `@CommandHandler(IssueInvoiceCommand, [InvoiceRepository, Clock])` becomes `@CommandHandler(IssueInvoiceCommand)`. It is the codemod that moves a project to `"stack.dependencies": "signature"`: set the key, run `eslint --fix`, then `ddd-ts-kit docs`, which registers in the generated file of each context what the constructors take. A port that now only types a parameter may be imported with `import type`, which `@typescript-eslint/consistent-type-imports` fixes in a project compiled without `emitDecoratorMetadata`.
|
|
71
|
+
|
|
72
|
+
A project whose config does not say, or says `"stack.dependencies": "listed"`, lists its dependencies in its decorators as 0.1 did: its lists are never reported, and a class of it that lists nothing is held to the same rules about its parameters.
|
|
73
|
+
|
|
74
|
+
## Options
|
|
75
|
+
|
|
76
|
+
None.
|
|
77
|
+
|
|
78
|
+
`rules."declarations/constructor-injection"` in `ddd-ts-kit.config.json` set to `off` silences the rule, and `// ddd-ts-kit-disable-next-line declarations/constructor-injection -- <reason>` silences one line, exactly as for `ddd-ts-kit check`.
|
|
79
|
+
|
|
80
|
+
## Limits
|
|
81
|
+
|
|
82
|
+
- An interface, a type alias or an enum imported from another file is not known to be one here: `ddd-ts-kit check` reads the imported file of the project and reports it. A name imported from a package is trusted, and the compiler judges the generated file, which imports it as a value.
|
|
83
|
+
- A class that inherits its constructor is not judged here: `ddd-ts-kit check` follows the base class through the project and reports one it cannot read (a class of a package).
|
|
84
|
+
- That the generated file registers the list of the current constructor is `declarations/barrel-current` of `ddd-ts-kit check`: it is a fact of the project, not of one file.
|
|
85
|
+
- That a parameter is typed by a port and not by an adapter or a fake is `layer/application-inward` and `ports/no-concrete-dependency`.
|
|
86
|
+
|
|
87
|
+
## With the CLI checker
|
|
88
|
+
|
|
89
|
+
`ddd-ts-kit check` reports the same id from the text of the files, across files, and stays the fast edit-time guard. This rule is the authority on what one file shows, and the only one that fixes.
|
|
@@ -22,7 +22,7 @@ On any class of `application/`, `infrastructure/`, `presentation/` or the contex
|
|
|
22
22
|
|
|
23
23
|
Under `domain/`:
|
|
24
24
|
|
|
25
|
-
- a class carrying any declaration decorator: `Requires`, `Public`, `CommandHandler`, `QueryHandler`, `IntegrationEventHandler`, `DomainEventHandler`, `Scheduled`, `Repository`, `Gateway`, `Adapter`, `Policy`
|
|
25
|
+
- a class carrying any declaration decorator: `Requires`, `Public`, `CommandHandler`, `QueryHandler`, `IntegrationEventHandler`, `DomainEventHandler`, `Scheduled`, `Projection`, `Repository`, `Gateway`, `Adapter`, `Policy`
|
|
26
26
|
|
|
27
27
|
| File, relative to the context | Exported class carries |
|
|
28
28
|
|---|---|
|
|
@@ -33,6 +33,7 @@ Under `domain/`:
|
|
|
33
33
|
| `application/integration/publishers/**/*.publisher.ts` | `@DomainEventHandler` |
|
|
34
34
|
| `application/authorization/**/*.policy.ts` | `@Policy` |
|
|
35
35
|
| `application/scheduling/**/*.job.ts` | `@Scheduled` |
|
|
36
|
+
| `application/projections/**/*.projection.ts` | `@Projection` |
|
|
36
37
|
| `infrastructure/persistence/**/*.repository.ts` | `@Repository` |
|
|
37
38
|
| `infrastructure/**/*.gateway.ts` | `@Gateway` |
|
|
38
39
|
| `infrastructure/**/*.adapter.ts` | `@Adapter` |
|
|
@@ -44,7 +45,7 @@ Incorrect:
|
|
|
44
45
|
export class IssueInvoiceHandler {}
|
|
45
46
|
|
|
46
47
|
// application/queries/search-invoices/search-invoices.handler.ts
|
|
47
|
-
@CommandHandler(SearchInvoicesQuery
|
|
48
|
+
@CommandHandler(SearchInvoicesQuery)
|
|
48
49
|
export class SearchInvoicesHandler {}
|
|
49
50
|
|
|
50
51
|
// application/authorization/only-issuer-voids.policy.ts
|
|
@@ -64,12 +65,17 @@ Correct:
|
|
|
64
65
|
|
|
65
66
|
```ts
|
|
66
67
|
// application/commands/issue-invoice/issue-invoice.handler.ts
|
|
67
|
-
import { Clock } from '@ddd-ts-kit/kernel';
|
|
68
|
+
import type { Clock } from '@ddd-ts-kit/kernel';
|
|
68
69
|
import { CommandHandler } from '@ddd-ts-kit/kernel/declarations';
|
|
69
|
-
import { InvoiceRepository } from '@/contexts/billing/domain/repositories/invoice.repository
|
|
70
|
+
import type { InvoiceRepository } from '@/contexts/billing/domain/repositories/invoice.repository';
|
|
70
71
|
|
|
71
|
-
@CommandHandler(IssueInvoiceCommand
|
|
72
|
-
export class IssueInvoiceHandler implements CommandHandler<IssueInvoiceCommand> {
|
|
72
|
+
@CommandHandler(IssueInvoiceCommand)
|
|
73
|
+
export class IssueInvoiceHandler implements CommandHandler<IssueInvoiceCommand> {
|
|
74
|
+
constructor(
|
|
75
|
+
private readonly invoices: InvoiceRepository,
|
|
76
|
+
private readonly clock: Clock,
|
|
77
|
+
) {}
|
|
78
|
+
}
|
|
73
79
|
|
|
74
80
|
// application/authorization/only-issuer-voids.policy.ts
|
|
75
81
|
@Policy(billingPermissions['invoice.void'], { name: 'billing.only-issuer-voids' })
|
|
@@ -82,7 +88,7 @@ export class InvoiceIssuedEventHandler {}
|
|
|
82
88
|
|
|
83
89
|
## Details
|
|
84
90
|
|
|
85
|
-
A decorator counts when its name is one of the
|
|
91
|
+
A decorator counts when its name is one of the twelve declaration decorators and that name is imported from `@ddd-ts-kit/kernel/declarations` in the file: `{ Repository }`, an alias `{ Repository as Repo }`, or a namespace `declarations.Repository`. A home-made decorator of the same name, or one of another package (`@Retry` of `@ddd-ts-kit/aspects/decorators`), is ignored.
|
|
86
92
|
|
|
87
93
|
The role of a file is read from its suffix and its folder, as the generator writes them. A class is exported inline, by `export { A }` or by `export default A`. When a class carries another declaration decorator, only that is reported: fixing it also answers the missing one.
|
|
88
94
|
|
|
@@ -96,9 +102,9 @@ None.
|
|
|
96
102
|
|
|
97
103
|
## Limits
|
|
98
104
|
|
|
99
|
-
- An event class reached through a barrel (`@/contexts/billing/domain/events/index
|
|
105
|
+
- An event class reached through a barrel (`@/contexts/billing/domain/events/index`), a renaming export, a default import or a namespace is not followed: its name is not compared. `ddd-ts-kit check` resolves the same import in the project.
|
|
100
106
|
- A decorator applied through a variable (`const declare = Repository(InvoiceRepository); @declare class ...`) is not recognised.
|
|
101
|
-
-
|
|
107
|
+
- What a class is given is `declarations/constructor-injection`; that a list matches the constructor and that a handler handles its message is checked by the compiler, not here; that an adapter implements its port is `ports/implements-never-extends`, and where a job lives and what it is named are `scheduling/job-triggers-use-case` and `scheduling/job-name`.
|
|
102
108
|
- That each role file holds a single declared class is `naming/one-concept-per-file` of `ddd-ts-kit check`.
|
|
103
109
|
- That the generated barrel `<context>.declarations.ts` lists every role file is `declarations/barrel-current` of `ddd-ts-kit check`: it is a fact of the project, not of one file.
|
|
104
110
|
- A role file written outside the folders of the table is not known to be one: `naming/suffix` and `tree/closed` of `ddd-ts-kit check` hold the tree.
|
|
@@ -30,7 +30,7 @@ Incorrect:
|
|
|
30
30
|
// src/contexts/billing/application/event-handlers/invoice-issued.event-handler.ts
|
|
31
31
|
import { Cache } from '@ddd-ts-kit/kernel';
|
|
32
32
|
import { DomainEventHandler } from '@ddd-ts-kit/kernel/declarations';
|
|
33
|
-
import { InvoiceRepository } from '@/contexts/billing/domain/repositories/invoice.repository
|
|
33
|
+
import { InvoiceRepository } from '@/contexts/billing/domain/repositories/invoice.repository';
|
|
34
34
|
|
|
35
35
|
@DomainEventHandler(InvoiceIssued, { name: 'billing.invoice.issued' }, [InvoiceRepository, Cache])
|
|
36
36
|
export class InvoiceIssuedEventHandler {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# ddd-ts-kit/errors-constructor-not-public
|
|
2
|
+
|
|
3
|
+
Architecture id: `errors/constructor-not-public`. Default severity: `error` in `recommended`, `error` in `strict`.
|
|
4
|
+
|
|
5
|
+
Applies to: the production files of `domain/errors/` of a context, in a project whose config says `"stack.refusals": "factories"`.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
A public constructor lets any caller build a refusal with a code and a message of its own. `ConceptError` of `@ddd-ts-kit/kernel` has a protected constructor, and a class that extends it declares none: a refusal then exists only through a static factory.
|
|
10
|
+
|
|
11
|
+
## Reported
|
|
12
|
+
|
|
13
|
+
- a class that extends another one and declares a public constructor
|
|
14
|
+
- a class that extends `DomainError` and declares no constructor: it inherits a public one
|
|
15
|
+
|
|
16
|
+
An abstract class is not judged.
|
|
17
|
+
|
|
18
|
+
Incorrect:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
export class SlotTakenError extends DomainError {
|
|
22
|
+
readonly code = 'SLOT_TAKEN';
|
|
23
|
+
|
|
24
|
+
constructor(calendarId: string) {
|
|
25
|
+
super(`The slot on calendar ${calendarId} is already held.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Correct:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
export class AppointmentError extends ConceptError {
|
|
34
|
+
static slotTaken(calendarId: string) {
|
|
35
|
+
return AppointmentError.refusal('SLOT_TAKEN', `The slot on calendar ${calendarId} is already held.`, { calendarId });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Options
|
|
41
|
+
|
|
42
|
+
None.
|
|
43
|
+
|
|
44
|
+
`rules."errors/constructor-not-public"` in `ddd-ts-kit.config.json` set to `off` silences the rule, and `// ddd-ts-kit-disable-next-line errors/constructor-not-public -- <reason>` silences one line, exactly as for `ddd-ts-kit check`.
|
|
45
|
+
|
|
46
|
+
## Limits
|
|
47
|
+
|
|
48
|
+
- A class that extends a base of the project and declares no constructor is trusted: the visibility it inherits is in another file.
|
|
49
|
+
- Nothing is fixed: making a constructor private breaks every `new` of the class. `ddd-ts-kit error-factories` moves a project to factories as a whole.
|
|
50
|
+
|
|
51
|
+
## With the CLI checker
|
|
52
|
+
|
|
53
|
+
Held by this plugin alone: the shape of a class needs a syntax tree.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# ddd-ts-kit/errors-factory-name
|
|
2
|
+
|
|
3
|
+
Architecture id: `errors/factory-name`. Default severity: `error` in `recommended`, `error` in `strict`.
|
|
4
|
+
|
|
5
|
+
Applies to: the production files of `domain/errors/` of a context, in a project whose config says `"stack.refusals": "factories"`.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
The factory is read by the developer who refuses, the code by the client, the mapper, the audit trail and the tests. They must meet the same words, and those words are the business's: `slotTaken` and `SLOT_TAKEN`, never `create` and `BOOKING_FAILED`.
|
|
10
|
+
|
|
11
|
+
## Reported
|
|
12
|
+
|
|
13
|
+
A factory is a public static method of an error class that calls `refusal(...)`.
|
|
14
|
+
|
|
15
|
+
- a name that says an error is built and not which rule refused: `create`, `make`, `of`, `from`, `build`, `new`, `error`, `failure`, `fail`, `refusal`, `refuse`, or a name holding a digit (`error1`)
|
|
16
|
+
- a code that is not a string literal, or not in constant case
|
|
17
|
+
- a code that is not the name of the factory in constant case, alone or with the name of the concept before or after it: `AppointmentError.notFound` sets `NOT_FOUND`, `APPOINTMENT_NOT_FOUND` or `NOT_FOUND_APPOINTMENT`
|
|
18
|
+
|
|
19
|
+
Incorrect:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
static slotTaken(calendarId: string) {
|
|
23
|
+
return AppointmentError.refusal('BOOKING_FAILED', 'The slot is already held.', { calendarId });
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Correct:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
static slotTaken(calendarId: string) {
|
|
31
|
+
return AppointmentError.refusal('SLOT_TAKEN', 'The slot is already held.', { calendarId });
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Options
|
|
36
|
+
|
|
37
|
+
None.
|
|
38
|
+
|
|
39
|
+
`rules."errors/factory-name"` in `ddd-ts-kit.config.json` set to `off` silences the rule, and `// ddd-ts-kit-disable-next-line errors/factory-name -- <reason>` silences one line, exactly as for `ddd-ts-kit check`.
|
|
40
|
+
|
|
41
|
+
A code already published under other words cannot change: rename the factory after it, or keep the name and silence that line with the reason.
|
|
42
|
+
|
|
43
|
+
## Limits
|
|
44
|
+
|
|
45
|
+
- Whether a name is a business phrase cannot be decided by a tool: only the names that are certainly not are refused. `language/glossary-term` holds the name of the class.
|
|
46
|
+
- The first `refusal(...)` call of the method is the one read.
|
|
47
|
+
|
|
48
|
+
## With the CLI checker
|
|
49
|
+
|
|
50
|
+
Held by this plugin alone. That a code is unique in its context is `errors/code-unique`, held by the checker.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# ddd-ts-kit/errors-through-factory
|
|
2
|
+
|
|
3
|
+
Architecture id: `errors/through-factory`. Default severity: `error` in `recommended`, `error` in `strict`.
|
|
4
|
+
|
|
5
|
+
Applies to: every file of a context, tests included, in a project whose config says `"stack.refusals": "factories"`. A config without the key was written when an error was a class built with `new`: nothing is reported there.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
The code that refuses reads as a sentence, and the refusals of one concept live in one class: `throw AppointmentError.slotTaken(calendar.id, slot.startsAt)`. The factory writes the code, the message and the facts once. A `new` outside the class is a refusal whose code and message are whatever the caller wrote.
|
|
10
|
+
|
|
11
|
+
## Reported
|
|
12
|
+
|
|
13
|
+
- `new X(...)` where `X` is imported from a file of `domain/errors/`
|
|
14
|
+
|
|
15
|
+
Incorrect:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { SlotTakenError } from '@/contexts/scheduling/domain/errors/slot-taken.error';
|
|
19
|
+
|
|
20
|
+
throw new SlotTakenError(calendar.id, slot.startsAt);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Correct:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { AppointmentError } from '@/contexts/scheduling/domain/errors/appointment.error';
|
|
27
|
+
|
|
28
|
+
throw AppointmentError.slotTaken(calendar.id, slot.startsAt);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Options
|
|
32
|
+
|
|
33
|
+
None.
|
|
34
|
+
|
|
35
|
+
`rules."errors/through-factory"` in `ddd-ts-kit.config.json` set to `off` silences the rule, and `// ddd-ts-kit-disable-next-line errors/through-factory -- <reason>` silences one line, exactly as for `ddd-ts-kit check`.
|
|
36
|
+
|
|
37
|
+
## Limits
|
|
38
|
+
|
|
39
|
+
- An error class is known by where it is imported from: a class declared elsewhere than `domain/errors/` is not seen (`naming/suffix` keeps errors there).
|
|
40
|
+
- The errors of the kit (`NotFoundError`, `InvariantViolation`, `ValidationError`...) are each one refusal with a fixed code and stay built with `new`.
|
|
41
|
+
- Nothing is fixed: which factory replaces a constructor is a decision. `ddd-ts-kit error-factories` writes the plan of a migration and applies it.
|
|
42
|
+
|
|
43
|
+
## With the CLI checker
|
|
44
|
+
|
|
45
|
+
Held by this plugin alone: the checker lists the rule and judges nothing. The compiler refuses the same thing once the class extends `ConceptError`, whose constructor is protected.
|
|
@@ -12,7 +12,7 @@ The alias is declared once, under `paths` in `tsconfig.json`, which the compiler
|
|
|
12
12
|
|
|
13
13
|
## Reported
|
|
14
14
|
|
|
15
|
-
- a relative specifier, `./x
|
|
15
|
+
- a relative specifier, `./x` or `../x`, with or without an extension, the same directory included: fixed to the alias of the file
|
|
16
16
|
- a relative specifier that names a file under no aliased directory (a manifest, a build product): reported, not fixed
|
|
17
17
|
- an alias specifier with a `.` or `..` segment, which could name a file outside the directory of the alias: reported, not fixed
|
|
18
18
|
|
|
@@ -20,22 +20,22 @@ Incorrect:
|
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
22
|
// src/contexts/billing/application/commands/issue-invoice/issue-invoice.handler.ts
|
|
23
|
-
import { Invoice } from '../../../domain/aggregates/invoice.aggregate
|
|
24
|
-
import { IssueInvoiceCommand } from './issue-invoice.command
|
|
23
|
+
import { Invoice } from '../../../domain/aggregates/invoice.aggregate';
|
|
24
|
+
import { IssueInvoiceCommand } from './issue-invoice.command';
|
|
25
25
|
|
|
26
26
|
// test/e2e/issue-invoice.e2e.spec.ts
|
|
27
|
-
import { startApplication } from './support/start-application
|
|
27
|
+
import { startApplication } from './support/start-application';
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
Correct:
|
|
31
31
|
|
|
32
32
|
```ts
|
|
33
33
|
// src/contexts/billing/application/commands/issue-invoice/issue-invoice.handler.ts
|
|
34
|
-
import { IssueInvoiceCommand } from '@/contexts/billing/application/commands/issue-invoice/issue-invoice.command
|
|
35
|
-
import { Invoice } from '@/contexts/billing/domain/aggregates/invoice.aggregate
|
|
34
|
+
import { IssueInvoiceCommand } from '@/contexts/billing/application/commands/issue-invoice/issue-invoice.command';
|
|
35
|
+
import { Invoice } from '@/contexts/billing/domain/aggregates/invoice.aggregate';
|
|
36
36
|
|
|
37
37
|
// test/e2e/issue-invoice.e2e.spec.ts
|
|
38
|
-
import { startApplication } from '$test/e2e/support/start-application
|
|
38
|
+
import { startApplication } from '$test/e2e/support/start-application';
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
## Details
|
|
@@ -51,7 +51,7 @@ The aliases of a project:
|
|
|
51
51
|
| `$scripts/` | `scripts/` | fixed |
|
|
52
52
|
| `$generated/` | `generated/`, with `stack.orm` `prisma` only | fixed |
|
|
53
53
|
|
|
54
|
-
The fix computes the alias from the place of the file and the config: the relative specifier is resolved from the directory of the file, and the result is written from the directory its alias names, with
|
|
54
|
+
The fix computes the alias from the place of the file and the config: the relative specifier is resolved from the directory of the file, and the result is written from the directory its alias names, ending as `importExtension` of the config says: with nothing under `none`, with `.js` under `js` (a config without the key). It keeps the quotes of the specifier. A fixed import is often longer than the line: run the formatter after the fix (`npm run format`). Fixing twice changes nothing.
|
|
55
55
|
|
|
56
56
|
Not judged: a file outside the three directories (a config file at the project root, a JavaScript migration), the client under `generated/`, which a tool writes, and a specifier that is neither relative nor one of the aliases (a package, a Node built-in).
|
|
57
57
|
|
|
@@ -59,13 +59,14 @@ An alias that names no file is not this rule's concern: the compiler reports it
|
|
|
59
59
|
|
|
60
60
|
## Options
|
|
61
61
|
|
|
62
|
-
None. `sourceRoot` and `importAlias` of `ddd-ts-kit.config.json` say what the alias of the sources is.
|
|
62
|
+
None. `sourceRoot` and `importAlias` of `ddd-ts-kit.config.json` say what the alias of the sources is, `importExtension` what the fixed specifier ends with.
|
|
63
63
|
|
|
64
64
|
`rules."imports/alias-only"` in `ddd-ts-kit.config.json` set to `off` silences the rule, and `// ddd-ts-kit-disable-next-line imports/alias-only -- <reason>` silences one line, exactly as for `ddd-ts-kit check`.
|
|
65
65
|
|
|
66
66
|
## Limits
|
|
67
67
|
|
|
68
68
|
- Only a module the build emits can be named by the alias: a JSON file or another asset is read at run time instead of imported.
|
|
69
|
+
- The ending of an alias specifier is the business of [`imports/extension`](imports-extension.md).
|
|
69
70
|
- The rule does not check that `tsconfig.json` declares the alias, nor that the build rewrites it: `imports/alias-declared` of `ddd-ts-kit check` does, with everything else that has to follow the one declaration.
|
|
70
71
|
|
|
71
72
|
## With the CLI checker
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# ddd-ts-kit/imports-extension
|
|
2
|
+
|
|
3
|
+
Architecture id: `imports/extension`. Default severity: `error` in `recommended`, `error` in `strict`. Fixable (`eslint --fix`).
|
|
4
|
+
|
|
5
|
+
Applies to: every TypeScript file under the source root, under `test/` and under `scripts/`.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
An import of a file of the project has one spelling. `importExtension` in `ddd-ts-kit.config.json` says which:
|
|
10
|
+
|
|
11
|
+
| `importExtension` | An import is written | The compiler resolves it | Who holds the other spelling |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| `none`, the default of a new project | `@/contexts/billing/domain/aggregates/invoice.aggregate` | as a bundler does (`"moduleResolution": "Bundler"`, with the `node` condition), which accepts both spellings | this rule: the compiler would let `.js` through |
|
|
14
|
+
| `js`, also what a config without the key means | `@/contexts/billing/domain/aggregates/invoice.aggregate.js` | as Node does (`"moduleResolution": "NodeNext"`), which requires the extension | the compiler (`TS2835`), and this rule, which fixes it |
|
|
15
|
+
|
|
16
|
+
Node needs the extension at run time whatever the sources write: `ddd-ts-kit build-aliases`, the second step of `npm run build`, rewrites each alias of the compiled output into a relative path ending in `.js`. The build names one file per specifier and guesses nothing, so a specifier written the other way stops it: this rule reports the mistake in the editor instead. See [Import aliases](../../../../docs/tools/import-aliases.md).
|
|
17
|
+
|
|
18
|
+
## Reported
|
|
19
|
+
|
|
20
|
+
- under `none`: an alias specifier that ends with `.js`, `.mjs`, `.cjs`, `.ts`, `.mts` or `.cts`: fixed by removing the extension
|
|
21
|
+
- under `js`: an alias specifier that does not end with `.js` (`.mjs` and `.cjs` are accepted, they name what an `.mts` or a `.cts` file emits): fixed by writing `.js` in place of what it ends with
|
|
22
|
+
|
|
23
|
+
Incorrect under `none`:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { Invoice } from '@/contexts/billing/domain/aggregates/invoice.aggregate.js';
|
|
27
|
+
import { startApplication } from '$test/e2e/support/start-application.ts';
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Correct under `none`:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { Invoice } from '@/contexts/billing/domain/aggregates/invoice.aggregate';
|
|
34
|
+
import { startApplication } from '$test/e2e/support/start-application';
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Incorrect under `js`:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { Invoice } from '@/contexts/billing/domain/aggregates/invoice.aggregate';
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Correct under `js`:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { Invoice } from '@/contexts/billing/domain/aggregates/invoice.aggregate.js';
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Details
|
|
50
|
+
|
|
51
|
+
Static imports, type-only imports, `export ... from`, `export *`, dynamic `import()`, `import()` types and `require()` are all read. A dynamic import whose specifier is computed is not judged.
|
|
52
|
+
|
|
53
|
+
Only a specifier that starts with an alias of the project is judged (`importAlias`, `$test/`, `$scripts/`, and `$generated/` with `stack.orm` `prisma`). A package is never a file of the project, whatever its subpath ends with: `zod/v4/core.js` and `@scope/package/file.js` are left alone, and so is a Node built-in. A relative import is reported by [`imports/alias-only`](imports-alias-only.md), whose fix writes the alias with the right ending.
|
|
54
|
+
|
|
55
|
+
A dot in a file name is not an extension: `invoice.aggregate`, `issue-invoice.command` and `load-config` are all read as written without one.
|
|
56
|
+
|
|
57
|
+
The fix keeps the quotes of the specifier. Under `none` a fixed import is three characters shorter and may now fit on one line: run the formatter after the fix (`npm run format`). Fixing twice changes nothing.
|
|
58
|
+
|
|
59
|
+
Not judged: a file outside the three directories (a config file at the project root, a JavaScript migration, which Node runs as written and which therefore keeps its extensions) and the client under `generated/`, which a tool writes.
|
|
60
|
+
|
|
61
|
+
## Moving a project to the other spelling
|
|
62
|
+
|
|
63
|
+
A project generated before the key existed is read as `js` and keeps working untouched. To drop the extensions:
|
|
64
|
+
|
|
65
|
+
1. set `"importExtension": "none"` in `ddd-ts-kit.config.json`;
|
|
66
|
+
2. run `ddd-ts-kit check`: `imports/alias-declared` gives the `module`, `moduleResolution` and `customConditions` to write in `tsconfig.json`, and the jest mapping when the project runs jest;
|
|
67
|
+
3. run `eslint --fix`, then `npm run format`;
|
|
68
|
+
4. regenerate what the kit generates (`ddd-ts-kit generate` or `ddd-ts-kit docs` rewrites the declarations barrels), then `npm run verify`.
|
|
69
|
+
|
|
70
|
+
## Options
|
|
71
|
+
|
|
72
|
+
None. `importExtension` of `ddd-ts-kit.config.json` decides.
|
|
73
|
+
|
|
74
|
+
`rules."imports/extension"` in `ddd-ts-kit.config.json` set to `off` silences the rule, and `// ddd-ts-kit-disable-next-line imports/extension -- <reason>` silences one line, exactly as for `ddd-ts-kit check`.
|
|
75
|
+
|
|
76
|
+
## Limits
|
|
77
|
+
|
|
78
|
+
- A specifier that names a directory (`@/contexts/billing/published-language`, resolved by a bundler to its `index.ts`) is not reported here: this rule reads no disk. `ddd-ts-kit check` reports it under the same id, and `ddd-ts-kit build-aliases` refuses it: an alias names one file, `.../published-language/index`.
|
|
79
|
+
- The rule does not check that `tsconfig.json` resolves as the config says: `imports/alias-declared` of `ddd-ts-kit check` does.
|
|
80
|
+
|
|
81
|
+
## With the CLI checker
|
|
82
|
+
|
|
83
|
+
`ddd-ts-kit check` reports the same id from the import specifiers it reads in the text of the files, under the source root, `test/` and `scripts/`, gives the specifier to write in its fix sentence, and also reports an alias that names a directory. This rule is the one that rewrites the import.
|