@living-architecture/riviere-extract-conventions-published-language 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +69 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +69 -0
  6. package/dist/published-language/decorators.d.ts +83 -0
  7. package/dist/published-language/decorators.d.ts.map +1 -0
  8. package/dist/published-language/decorators.js +130 -0
  9. package/package.json +44 -0
  10. package/src/published-language/default-extraction.config.json +153 -0
  11. package/src/published-language/eslint-plugin/api-controller-requires-route-and-method.cjs +85 -0
  12. package/src/published-language/eslint-plugin/api-controller-requires-route-and-method.d.cts +6 -0
  13. package/src/published-language/eslint-plugin/event-handler-requires-subscribed-events.cjs +47 -0
  14. package/src/published-language/eslint-plugin/event-handler-requires-subscribed-events.d.cts +6 -0
  15. package/src/published-language/eslint-plugin/event-publisher-method-signature.cjs +179 -0
  16. package/src/published-language/eslint-plugin/event-publisher-method-signature.d.cts +6 -0
  17. package/src/published-language/eslint-plugin/event-requires-type-property.cjs +47 -0
  18. package/src/published-language/eslint-plugin/event-requires-type-property.d.cts +6 -0
  19. package/src/published-language/eslint-plugin/http-call-requires-http-client-container.cjs +68 -0
  20. package/src/published-language/eslint-plugin/http-call-requires-http-client-container.d.cts +5 -0
  21. package/src/published-language/eslint-plugin/http-call-requires-route.cjs +104 -0
  22. package/src/published-language/eslint-plugin/http-call-requires-route.d.cts +5 -0
  23. package/src/published-language/eslint-plugin/http-client-public-methods-require-http-call.cjs +80 -0
  24. package/src/published-language/eslint-plugin/http-client-public-methods-require-http-call.d.cts +5 -0
  25. package/src/published-language/eslint-plugin/http-client-requires-remote-name.cjs +77 -0
  26. package/src/published-language/eslint-plugin/http-client-requires-remote-name.d.cts +5 -0
  27. package/src/published-language/eslint-plugin/index.cjs +64 -0
  28. package/src/published-language/eslint-plugin/index.d.ts +57 -0
  29. package/src/published-language/eslint-plugin/interface-ast-predicates.cjs +83 -0
  30. package/src/published-language/eslint-plugin/no-fetch-outside-http-client.cjs +60 -0
  31. package/src/published-language/eslint-plugin/no-fetch-outside-http-client.d.cts +5 -0
  32. package/src/published-language/eslint-plugin/require-component-decorator.cjs +56 -0
  33. package/src/published-language/eslint-plugin/require-component-decorator.d.cts +4 -0
  34. package/src/published-language/eslint-plugin/rule-module-declarations.d.ts +59 -0
  35. package/src/published-language/eslint-plugin/ui-page-requires-route.cjs +47 -0
  36. package/src/published-language/eslint-plugin/ui-page-requires-route.d.cts +6 -0
@@ -0,0 +1,153 @@
1
+ {
2
+ "$schema": "https://living-architecture.dev/schemas/extraction-config.schema.json",
3
+ "modules": [
4
+ {
5
+ "name": "default",
6
+ "domain": "default",
7
+ "path": ".",
8
+ "glob": "**/*.ts",
9
+ "api": {
10
+ "find": "methods",
11
+ "where": {
12
+ "inClassWith": {
13
+ "hasDecorator": {
14
+ "name": "APIContainer",
15
+ "from": "@living-architecture/riviere-extract-conventions-published-language"
16
+ }
17
+ }
18
+ },
19
+ "extract": {
20
+ "apiType": { "literal": "REST" },
21
+ "route": { "fromProperty": { "name": "route", "kind": "instance" } },
22
+ "method": { "fromProperty": { "name": "method", "kind": "instance" } }
23
+ }
24
+ },
25
+ "useCase": {
26
+ "find": "classes",
27
+ "where": {
28
+ "hasDecorator": {
29
+ "name": "UseCase",
30
+ "from": "@living-architecture/riviere-extract-conventions-published-language"
31
+ }
32
+ }
33
+ },
34
+ "domainOp": {
35
+ "find": "methods",
36
+ "where": {
37
+ "inClassWith": {
38
+ "hasDecorator": {
39
+ "name": "DomainOpContainer",
40
+ "from": "@living-architecture/riviere-extract-conventions-published-language"
41
+ }
42
+ }
43
+ },
44
+ "extract": {
45
+ "operationName": { "fromMethodName": true }
46
+ }
47
+ },
48
+ "event": {
49
+ "find": "classes",
50
+ "where": {
51
+ "hasDecorator": {
52
+ "name": "Event",
53
+ "from": "@living-architecture/riviere-extract-conventions-published-language"
54
+ }
55
+ },
56
+ "extract": {
57
+ "eventName": { "fromClassName": true }
58
+ }
59
+ },
60
+ "eventHandler": {
61
+ "find": "methods",
62
+ "where": {
63
+ "inClassWith": {
64
+ "hasDecorator": {
65
+ "name": "EventHandlerContainer",
66
+ "from": "@living-architecture/riviere-extract-conventions-published-language"
67
+ }
68
+ }
69
+ },
70
+ "extract": {
71
+ "subscribedEvents": { "fromProperty": { "name": "subscribedEvents", "kind": "instance" } }
72
+ }
73
+ },
74
+ "ui": {
75
+ "find": "classes",
76
+ "where": {
77
+ "hasDecorator": {
78
+ "name": "UI",
79
+ "from": "@living-architecture/riviere-extract-conventions-published-language"
80
+ }
81
+ },
82
+ "extract": {
83
+ "route": { "fromProperty": { "name": "route", "kind": "static" } }
84
+ }
85
+ },
86
+ "customTypes": {
87
+ "eventPublisher": {
88
+ "find": "methods",
89
+ "where": {
90
+ "inClassWith": {
91
+ "hasDecorator": {
92
+ "name": "EventPublisherContainer",
93
+ "from": "@living-architecture/riviere-extract-conventions-published-language"
94
+ }
95
+ }
96
+ },
97
+ "extract": {
98
+ "publishedEventType": { "fromParameterType": { "position": 0 } }
99
+ }
100
+ },
101
+ "httpCall": {
102
+ "find": "methods",
103
+ "where": {
104
+ "and": [
105
+ {
106
+ "hasDecorator": {
107
+ "name": "HttpCall",
108
+ "from": "@living-architecture/riviere-extract-conventions-published-language"
109
+ }
110
+ },
111
+ {
112
+ "inClassWith": {
113
+ "hasDecorator": {
114
+ "name": "HttpClient",
115
+ "from": "@living-architecture/riviere-extract-conventions-published-language"
116
+ }
117
+ }
118
+ }
119
+ ]
120
+ },
121
+ "extract": {
122
+ "serviceName": {
123
+ "fromClassDecoratorArg": {
124
+ "decorator": "HttpClient",
125
+ "position": 0
126
+ }
127
+ },
128
+ "route": {
129
+ "fromDecoratorArg": {
130
+ "decorator": "HttpCall",
131
+ "position": 0
132
+ }
133
+ },
134
+ "method": {
135
+ "fromDecoratorArg": {
136
+ "decorator": "HttpCall",
137
+ "position": 1
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ ],
145
+ "connections": {
146
+ "eventPublishers": [
147
+ {
148
+ "fromType": "eventPublisher",
149
+ "metadataKey": "publishedEventType"
150
+ }
151
+ ]
152
+ }
153
+ }
@@ -0,0 +1,85 @@
1
+ const {
2
+ hasDecorator,
3
+ findInstanceProperty,
4
+ hasStringLiteralValue,
5
+ getLiteralValue,
6
+ getValueTypeDescription,
7
+ } = require('./interface-ast-predicates.cjs')
8
+
9
+ const VALID_HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
10
+
11
+ module.exports = {
12
+ meta: {
13
+ type: 'problem',
14
+ docs: { description: 'Require @APIContainer classes to have route and method with literal values' },
15
+ schema: [],
16
+ messages: {
17
+ missingRoute: "Class '{{className}}' decorated with @APIContainer but is missing 'route' property",
18
+ missingMethod: "Class '{{className}}' decorated with @APIContainer but is missing 'method' property",
19
+ routeNotLiteral: "Class '{{className}}' has 'route' property but value must be a string literal, not {{actualType}}",
20
+ methodNotLiteral: "Class '{{className}}' has 'method' property but value must be a string literal, not {{actualType}}",
21
+ invalidHttpMethod: "Class '{{className}}' has invalid HTTP method '{{value}}'. Must be one of: GET, POST, PUT, PATCH, DELETE",
22
+ },
23
+ },
24
+ create(context) {
25
+ return {
26
+ ClassDeclaration(node) {
27
+ /* v8 ignore next -- ClassDeclaration always has id (name) */
28
+ if (!node.id) return
29
+ if (!hasDecorator(node, 'APIContainer')) return
30
+
31
+ const className = node.id.name
32
+ const routeProperty = findInstanceProperty(node, 'route')
33
+ const methodProperty = findInstanceProperty(node, 'method')
34
+
35
+ // Check route property
36
+ if (!routeProperty) {
37
+ context.report({
38
+ node: node.id,
39
+ messageId: 'missingRoute',
40
+ data: { className },
41
+ })
42
+ } else if (!hasStringLiteralValue(routeProperty)) {
43
+ context.report({
44
+ node: routeProperty,
45
+ messageId: 'routeNotLiteral',
46
+ data: {
47
+ className,
48
+ actualType: getValueTypeDescription(routeProperty),
49
+ },
50
+ })
51
+ }
52
+
53
+ // Check method property
54
+ if (!methodProperty) {
55
+ context.report({
56
+ node: node.id,
57
+ messageId: 'missingMethod',
58
+ data: { className },
59
+ })
60
+ } else if (!hasStringLiteralValue(methodProperty)) {
61
+ context.report({
62
+ node: methodProperty,
63
+ messageId: 'methodNotLiteral',
64
+ data: {
65
+ className,
66
+ actualType: getValueTypeDescription(methodProperty),
67
+ },
68
+ })
69
+ } else {
70
+ const methodValue = getLiteralValue(methodProperty)
71
+ if (!VALID_HTTP_METHODS.includes(methodValue)) {
72
+ context.report({
73
+ node: methodProperty,
74
+ messageId: 'invalidHttpMethod',
75
+ data: {
76
+ className,
77
+ value: methodValue,
78
+ },
79
+ })
80
+ }
81
+ }
82
+ },
83
+ }
84
+ },
85
+ }
@@ -0,0 +1,6 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils'
2
+
3
+ type MessageIds = 'missingRoute' | 'missingMethod' | 'routeNotLiteral' | 'methodNotLiteral' | 'invalidHttpMethod'
4
+
5
+ declare const rule: TSESLint.RuleModule<MessageIds>
6
+ export default rule
@@ -0,0 +1,47 @@
1
+ const {
2
+ hasDecorator,
3
+ findInstanceProperty,
4
+ hasStringLiteralArrayValue,
5
+ getValueTypeDescription,
6
+ } = require('./interface-ast-predicates.cjs')
7
+
8
+ module.exports = {
9
+ meta: {
10
+ type: 'problem',
11
+ docs: { description: 'Require @EventHandlerContainer classes to have subscribedEvents array with literal values' },
12
+ schema: [],
13
+ messages: {
14
+ missingSubscribedEvents: "Class '{{className}}' has @EventHandlerContainer but is missing 'subscribedEvents' property",
15
+ subscribedEventsNotLiteralArray: "Class '{{className}}' has 'subscribedEvents' property but value must be an array of string literals, not {{actualType}}",
16
+ },
17
+ },
18
+ create(context) {
19
+ return {
20
+ ClassDeclaration(node) {
21
+ /* v8 ignore next -- ESLint ClassDeclaration visitor guarantees node.id exists per estree spec; structurally unreachable */
22
+ if (!node.id) return
23
+ if (!hasDecorator(node, 'EventHandlerContainer')) return
24
+
25
+ const className = node.id.name
26
+ const subscribedEventsProperty = findInstanceProperty(node, 'subscribedEvents')
27
+
28
+ if (!subscribedEventsProperty) {
29
+ context.report({
30
+ node: node.id,
31
+ messageId: 'missingSubscribedEvents',
32
+ data: { className },
33
+ })
34
+ } else if (!hasStringLiteralArrayValue(subscribedEventsProperty)) {
35
+ context.report({
36
+ node: subscribedEventsProperty,
37
+ messageId: 'subscribedEventsNotLiteralArray',
38
+ data: {
39
+ className,
40
+ actualType: getValueTypeDescription(subscribedEventsProperty),
41
+ },
42
+ })
43
+ }
44
+ },
45
+ }
46
+ },
47
+ }
@@ -0,0 +1,6 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils'
2
+
3
+ type MessageIds = 'missingSubscribedEvents' | 'subscribedEventsNotLiteralArray'
4
+
5
+ declare const rule: TSESLint.RuleModule<MessageIds>
6
+ export default rule
@@ -0,0 +1,179 @@
1
+ const { hasDecorator } = require('./interface-ast-predicates.cjs')
2
+
3
+ let getParserServices = null
4
+ try {
5
+ getParserServices = require('@typescript-eslint/utils').ESLintUtils.getParserServices
6
+ /* v8 ignore next -- soft-require: package always present in this repo */
7
+ } catch { /* type checking unavailable */ }
8
+
9
+ function getTypeReferenceName(typeAnnotation) {
10
+ const typeName = typeAnnotation.typeName
11
+ if (typeName.type === 'TSQualifiedName') {
12
+ return typeName.right.name
13
+ }
14
+ return typeName.name
15
+ }
16
+
17
+ function checkEventTypeShape(checker, services, param, typeAnnotation) {
18
+ if (!checker || !services) return null
19
+
20
+ const tsParam = services.esTreeNodeToTSNodeMap.get(param)
21
+ const paramType = checker.getTypeAtLocation(tsParam)
22
+ const typeProperty = paramType.getProperty('type')
23
+ const referenceName = getTypeReferenceName(typeAnnotation)
24
+
25
+ if (!typeProperty) {
26
+ return referenceName
27
+ }
28
+
29
+ const typePropertyType = checker.getTypeOfSymbol(typeProperty)
30
+ const stringType = checker.getStringType()
31
+ if (!checker.isTypeAssignableTo(typePropertyType, stringType)) {
32
+ return referenceName
33
+ }
34
+
35
+ return null
36
+ }
37
+
38
+ function checkMethod(context, member, className, checker, services) {
39
+ const methodName = member.key.name
40
+
41
+ if (member.accessibility === 'private' || member.accessibility === 'protected') {
42
+ context.report({
43
+ node: member.key,
44
+ messageId: 'nonPublicMethod',
45
+ data: {
46
+ methodName,
47
+ className,
48
+ accessibility: member.accessibility,
49
+ },
50
+ })
51
+ return
52
+ }
53
+
54
+ const params = member.value.params
55
+
56
+ if (params.length === 0) {
57
+ context.report({
58
+ node: member.key,
59
+ messageId: 'missingParameter',
60
+ data: {
61
+ methodName,
62
+ className,
63
+ },
64
+ })
65
+ return
66
+ }
67
+
68
+ if (params.length > 1) {
69
+ context.report({
70
+ node: member.key,
71
+ messageId: 'tooManyParameters',
72
+ data: {
73
+ methodName,
74
+ className,
75
+ },
76
+ })
77
+ return
78
+ }
79
+
80
+ const param = params[0]
81
+ const typeAnnotation = param.typeAnnotation && param.typeAnnotation.typeAnnotation
82
+
83
+ if (!typeAnnotation) {
84
+ context.report({
85
+ node: member.key,
86
+ messageId: 'missingTypeAnnotation',
87
+ data: {
88
+ methodName,
89
+ className,
90
+ },
91
+ })
92
+ return
93
+ }
94
+
95
+ if (typeAnnotation.type !== 'TSTypeReference') {
96
+ context.report({
97
+ node: member.key,
98
+ messageId: 'notTypeReference',
99
+ data: {
100
+ methodName,
101
+ className,
102
+ },
103
+ })
104
+ return
105
+ }
106
+
107
+ const failedTypeName = checkEventTypeShape(checker, services, param, typeAnnotation)
108
+ if (failedTypeName) {
109
+ context.report({
110
+ node: member.key,
111
+ messageId: 'invalidEventType',
112
+ data: {
113
+ typeName: failedTypeName,
114
+ methodName,
115
+ className,
116
+ },
117
+ })
118
+ }
119
+ }
120
+
121
+ function isMethodToCheck(member) {
122
+ return (
123
+ member.type === 'MethodDefinition' &&
124
+ member.kind !== 'constructor' &&
125
+ member.kind !== 'get' &&
126
+ member.kind !== 'set'
127
+ )
128
+ }
129
+
130
+ module.exports = {
131
+ meta: {
132
+ type: 'problem',
133
+ docs: {
134
+ description: 'Require @EventPublisherContainer methods to have exactly one typed event parameter',
135
+ requiresTypeChecking: true,
136
+ examples: {
137
+ valid: "@EventPublisherContainer class OrderPublisher { publish(event: OrderPlacedEvent): void {} }",
138
+ invalid: "@EventPublisherContainer class OrderPublisher { publish(): void {} }",
139
+ },
140
+ },
141
+ schema: [],
142
+ messages: {
143
+ missingParameter: "Method '{{methodName}}' on @EventPublisherContainer class '{{className}}' must have exactly one parameter",
144
+ tooManyParameters: "Method '{{methodName}}' on @EventPublisherContainer class '{{className}}' must have exactly one parameter",
145
+ missingTypeAnnotation: "Parameter of method '{{methodName}}' on @EventPublisherContainer class '{{className}}' must have a type annotation",
146
+ notTypeReference: "Parameter of method '{{methodName}}' on @EventPublisherContainer class '{{className}}' must have a type reference annotation",
147
+ invalidEventType: "Parameter type '{{typeName}}' of method '{{methodName}}' on @EventPublisherContainer class '{{className}}' must have a 'type: string' property",
148
+ nonPublicMethod: "Method '{{methodName}}' on @EventPublisherContainer class '{{className}}' must be public, but is {{accessibility}}",
149
+ },
150
+ },
151
+ create(context) {
152
+ let services = null
153
+ let checker = null
154
+ /* v8 ignore start -- graceful degradation: getParserServices always available in this repo */
155
+ if (getParserServices) {
156
+ try {
157
+ services = getParserServices(context, true)
158
+ checker = services.program?.getTypeChecker() ?? null
159
+ } catch { /* no type info */ }
160
+ }
161
+ /* v8 ignore stop */
162
+
163
+ return {
164
+ ClassDeclaration(node) {
165
+ /* v8 ignore next -- ClassDeclaration always has id (name) */
166
+ if (!node.id) return
167
+ if (!hasDecorator(node, 'EventPublisherContainer')) return
168
+
169
+ const className = node.id.name
170
+
171
+ for (const member of node.body.body) {
172
+ if (isMethodToCheck(member)) {
173
+ checkMethod(context, member, className, checker, services)
174
+ }
175
+ }
176
+ },
177
+ }
178
+ },
179
+ }
@@ -0,0 +1,6 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils'
2
+
3
+ type MessageIds = 'missingParameter' | 'tooManyParameters' | 'missingTypeAnnotation' | 'notTypeReference' | 'invalidEventType' | 'nonPublicMethod'
4
+
5
+ declare const rule: TSESLint.RuleModule<MessageIds>
6
+ export default rule
@@ -0,0 +1,47 @@
1
+ const {
2
+ hasDecorator,
3
+ findInstanceProperty,
4
+ hasStringLiteralValue,
5
+ getValueTypeDescription,
6
+ } = require('./interface-ast-predicates.cjs')
7
+
8
+ module.exports = {
9
+ meta: {
10
+ type: 'problem',
11
+ docs: { description: 'Require @Event classes to have type property with literal value' },
12
+ schema: [],
13
+ messages: {
14
+ missingType: "Class '{{className}}' decorated with @Event but is missing 'type' property",
15
+ typeNotLiteral: "Class '{{className}}' has 'type' property but value must be a string literal, not {{actualType}}",
16
+ },
17
+ },
18
+ create(context) {
19
+ return {
20
+ ClassDeclaration(node) {
21
+ /* v8 ignore next -- ClassDeclaration always has id (name) */
22
+ if (!node.id) return
23
+ if (!hasDecorator(node, 'Event')) return
24
+
25
+ const className = node.id.name
26
+ const typeProperty = findInstanceProperty(node, 'type')
27
+
28
+ if (!typeProperty) {
29
+ context.report({
30
+ node: node.id,
31
+ messageId: 'missingType',
32
+ data: { className },
33
+ })
34
+ } else if (!hasStringLiteralValue(typeProperty)) {
35
+ context.report({
36
+ node: typeProperty,
37
+ messageId: 'typeNotLiteral',
38
+ data: {
39
+ className,
40
+ actualType: getValueTypeDescription(typeProperty),
41
+ },
42
+ })
43
+ }
44
+ },
45
+ }
46
+ },
47
+ }
@@ -0,0 +1,6 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils'
2
+
3
+ type MessageIds = 'missingType' | 'typeNotLiteral'
4
+
5
+ declare const rule: TSESLint.RuleModule<MessageIds>
6
+ export default rule
@@ -0,0 +1,68 @@
1
+ function hasDecoratorNamed(decorators, decoratorName) {
2
+ /* v8 ignore next 3 -- typescript-eslint provides an array for decorators on class and method nodes; undefined is structurally unreachable in current parser output */
3
+ if (!decorators) {
4
+ return false
5
+ }
6
+ return decorators.some((decorator) => {
7
+ const expression = decorator.expression
8
+ if (expression.type === 'Identifier') {
9
+ return expression.name === decoratorName
10
+ }
11
+ return (
12
+ expression.type === 'CallExpression' &&
13
+ expression.callee.type === 'Identifier' &&
14
+ expression.callee.name === decoratorName
15
+ )
16
+ })
17
+ }
18
+
19
+ function isMethodWithHttpCallDecorator(member) {
20
+ if (member.type !== 'MethodDefinition') {
21
+ return false
22
+ }
23
+ if (member.key.type !== 'Identifier') {
24
+ return false
25
+ }
26
+ return hasDecoratorNamed(member.decorators, 'HttpCall')
27
+ }
28
+
29
+ module.exports = {
30
+ meta: {
31
+ type: 'problem',
32
+ docs: { description: 'Require HttpCall methods to be inside HttpClient classes' },
33
+ schema: [],
34
+ messages: {
35
+ missingHttpClientContainer:
36
+ "Method '{{methodName}}' uses @HttpCall but its class '{{className}}' is missing @HttpClient",
37
+ },
38
+ },
39
+ create(context) {
40
+ return {
41
+ ClassDeclaration(node) {
42
+ if (!node.id) {
43
+ return
44
+ }
45
+
46
+ const hasHttpClientDecorator = hasDecoratorNamed(node.decorators, 'HttpClient')
47
+ if (hasHttpClientDecorator) {
48
+ return
49
+ }
50
+
51
+ for (const member of node.body.body) {
52
+ if (!isMethodWithHttpCallDecorator(member)) {
53
+ continue
54
+ }
55
+
56
+ context.report({
57
+ node: member.key,
58
+ messageId: 'missingHttpClientContainer',
59
+ data: {
60
+ className: node.id.name,
61
+ methodName: member.key.name,
62
+ },
63
+ })
64
+ }
65
+ },
66
+ }
67
+ },
68
+ }
@@ -0,0 +1,5 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils'
2
+
3
+ declare const rule: TSESLint.RuleModule<string, readonly unknown[]>
4
+
5
+ export = rule