@spine-event-engine/proto-tools 2.0.0-snapshot.10
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/LICENSE +201 -0
- package/README.md +103 -0
- package/REFERENCE.md +90 -0
- package/bin/spine-proto.mjs +2 -0
- package/dist/src/cli/spine-proto-bootstrap.d.ts +4 -0
- package/dist/src/cli/spine-proto-bootstrap.d.ts.map +1 -0
- package/dist/src/cli/spine-proto-bootstrap.js +52 -0
- package/dist/src/cli/spine-proto-bootstrap.js.map +1 -0
- package/dist/src/cli/spine-proto.d.ts +3 -0
- package/dist/src/cli/spine-proto.d.ts.map +1 -0
- package/dist/src/cli/spine-proto.js +32 -0
- package/dist/src/cli/spine-proto.js.map +1 -0
- package/dist/src/generation/authored-interface-provider.d.ts +28 -0
- package/dist/src/generation/authored-interface-provider.d.ts.map +1 -0
- package/dist/src/generation/authored-interface-provider.js +244 -0
- package/dist/src/generation/authored-interface-provider.js.map +1 -0
- package/dist/src/generation/build-time-handler-analyzer.d.ts +195 -0
- package/dist/src/generation/build-time-handler-analyzer.d.ts.map +1 -0
- package/dist/src/generation/build-time-handler-analyzer.js +1308 -0
- package/dist/src/generation/build-time-handler-analyzer.js.map +1 -0
- package/dist/src/generation/generated-registry-writer.d.ts +58 -0
- package/dist/src/generation/generated-registry-writer.d.ts.map +1 -0
- package/dist/src/generation/generated-registry-writer.js +465 -0
- package/dist/src/generation/generated-registry-writer.js.map +1 -0
- package/dist/src/generation/generated-source-policy.d.ts +21 -0
- package/dist/src/generation/generated-source-policy.d.ts.map +1 -0
- package/dist/src/generation/generated-source-policy.js +126 -0
- package/dist/src/generation/generated-source-policy.js.map +1 -0
- package/dist/src/generation/generation-reuse.mjs +106 -0
- package/dist/src/generation/generator.d.ts +195 -0
- package/dist/src/generation/generator.d.ts.map +1 -0
- package/dist/src/generation/generator.js +566 -0
- package/dist/src/generation/generator.js.map +1 -0
- package/dist/src/generation/handler-codegen.d.ts +48 -0
- package/dist/src/generation/handler-codegen.d.ts.map +1 -0
- package/dist/src/generation/handler-codegen.js +214 -0
- package/dist/src/generation/handler-codegen.js.map +1 -0
- package/dist/src/generation/handler-generator.d.ts +7 -0
- package/dist/src/generation/handler-generator.d.ts.map +1 -0
- package/dist/src/generation/handler-generator.js +55 -0
- package/dist/src/generation/handler-generator.js.map +1 -0
- package/dist/src/generation/interface-generator.d.ts +21 -0
- package/dist/src/generation/interface-generator.d.ts.map +1 -0
- package/dist/src/generation/interface-generator.js +277 -0
- package/dist/src/generation/interface-generator.js.map +1 -0
- package/dist/src/generation/interface-provider.d.ts +30 -0
- package/dist/src/generation/interface-provider.d.ts.map +1 -0
- package/dist/src/generation/interface-provider.js +15 -0
- package/dist/src/generation/interface-provider.js.map +1 -0
- package/dist/src/generation/rejection-generator.d.ts +9 -0
- package/dist/src/generation/rejection-generator.d.ts.map +1 -0
- package/dist/src/generation/rejection-generator.js +62 -0
- package/dist/src/generation/rejection-generator.js.map +1 -0
- package/dist/src/generation/source-view.d.ts +103 -0
- package/dist/src/generation/source-view.d.ts.map +1 -0
- package/dist/src/generation/source-view.js +298 -0
- package/dist/src/generation/source-view.js.map +1 -0
- package/dist/src/index.d.ts +139 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +546 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/io/atomic-manifest.d.ts +32 -0
- package/dist/src/io/atomic-manifest.d.ts.map +1 -0
- package/dist/src/io/atomic-manifest.js +56 -0
- package/dist/src/io/atomic-manifest.js.map +1 -0
- package/dist/src/io/manifest-reader.d.ts +8 -0
- package/dist/src/io/manifest-reader.d.ts.map +1 -0
- package/dist/src/io/manifest-reader.js +19 -0
- package/dist/src/io/manifest-reader.js.map +1 -0
- package/dist/src/model/model-graph.d.ts +68 -0
- package/dist/src/model/model-graph.d.ts.map +1 -0
- package/dist/src/model/model-graph.js +220 -0
- package/dist/src/model/model-graph.js.map +1 -0
- package/dist/src/model/npm-package-name.d.ts +7 -0
- package/dist/src/model/npm-package-name.d.ts.map +1 -0
- package/dist/src/model/npm-package-name.js +33 -0
- package/dist/src/model/npm-package-name.js.map +1 -0
- package/dist/src/model/registry-dependency.d.ts +10 -0
- package/dist/src/model/registry-dependency.d.ts.map +1 -0
- package/dist/src/model/registry-dependency.js +66 -0
- package/dist/src/model/registry-dependency.js.map +1 -0
- package/dist/src/testing/index.d.ts +8 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +20 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +54 -0
- package/scripts/clean-dist.mjs +7 -0
- package/scripts/copy-runtime-modules.mjs +23 -0
|
@@ -0,0 +1,1308 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, CodeMatters. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
|
5
|
+
* in compliance with the License. You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
|
10
|
+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
11
|
+
* or implied. See the License for the specific language governing permissions and limitations under
|
|
12
|
+
* the License.
|
|
13
|
+
*/
|
|
14
|
+
import { Buffer } from "node:buffer";
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
import { createRequire } from "node:module";
|
|
17
|
+
import { dirname, join, resolve } from "node:path";
|
|
18
|
+
import ts from "typescript";
|
|
19
|
+
/**
|
|
20
|
+
* Provides build-time analysis for bare Spine handler decorators.
|
|
21
|
+
*/
|
|
22
|
+
export const BuildHandlerAnalyzer = Object.freeze({
|
|
23
|
+
// prettier-ignore
|
|
24
|
+
/**
|
|
25
|
+
* Inspects source files and returns receiver handler records with deterministic diagnostics.
|
|
26
|
+
*
|
|
27
|
+
* @param program TypeScript program that owns the source files and diagnostics.
|
|
28
|
+
* @param sourceFiles Application source files to inspect; defaults to program files.
|
|
29
|
+
* @returns Receiver handler records and deterministic diagnostics.
|
|
30
|
+
*/
|
|
31
|
+
analyze(program, sourceFiles = HandlerSources.appSourceFiles(program)) {
|
|
32
|
+
PackageDependencies.load(program);
|
|
33
|
+
const receivers = [];
|
|
34
|
+
const diagnostics = [];
|
|
35
|
+
for (const source of sourceFiles) {
|
|
36
|
+
const syntaxDiagnostics = program.getSyntacticDiagnostics(source);
|
|
37
|
+
if (syntaxDiagnostics.length > 0) {
|
|
38
|
+
diagnostics.push(...syntaxDiagnostics.map((diagnostic) => HandlerTypes.syntaxDiagnostic(source, diagnostic)));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const scope = {
|
|
42
|
+
program,
|
|
43
|
+
source,
|
|
44
|
+
imports: HandlerSources.buildImportState(source, program),
|
|
45
|
+
diagnostics,
|
|
46
|
+
};
|
|
47
|
+
const sourceReceivers = HandlerSources.analyzeSource(scope);
|
|
48
|
+
receivers.push(...sourceReceivers);
|
|
49
|
+
}
|
|
50
|
+
return { receivers, diagnostics };
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const handlerDecorators = new Set(["Assign", "Command", "React", "Subscribe"]);
|
|
54
|
+
const entityBaseNames = new Set(["Aggregate", "Projection", "ProcessManager"]);
|
|
55
|
+
const standaloneBaseNames = new Set([
|
|
56
|
+
"AbstractAssignee",
|
|
57
|
+
"AbstractCommander",
|
|
58
|
+
"AbstractEventReactor",
|
|
59
|
+
"AbstractEventSubscriber",
|
|
60
|
+
]);
|
|
61
|
+
const maxAliasDepth = 50;
|
|
62
|
+
// `spine.options.entity` in the frozen `spine/options.proto` contract.
|
|
63
|
+
const entityOptionFieldNumber = 73903;
|
|
64
|
+
let packageDependencies;
|
|
65
|
+
const HandlerSources = Object.freeze({
|
|
66
|
+
appSourceFiles(program) {
|
|
67
|
+
return program.getSourceFiles().filter((source) => !source.isDeclarationFile);
|
|
68
|
+
},
|
|
69
|
+
analyzeSource(scope) {
|
|
70
|
+
const receivers = [];
|
|
71
|
+
for (const statement of scope.source.statements) {
|
|
72
|
+
if (!ts.isClassDeclaration(statement) ||
|
|
73
|
+
(statement.name === undefined &&
|
|
74
|
+
!HandlerTypes.hasModifier(statement, ts.SyntaxKind.DefaultKeyword))) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const receiver = HandlerSources.analyzeClass(statement, scope);
|
|
78
|
+
if (receiver !== undefined) {
|
|
79
|
+
receivers.push(receiver);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return receivers;
|
|
83
|
+
},
|
|
84
|
+
analyzeClass(node, scope) {
|
|
85
|
+
const className = node.name?.text ?? "DefaultReceiver";
|
|
86
|
+
const exportIssue = HandlerSources.receiverExportIssue(node, scope.source);
|
|
87
|
+
if (HandlerSources.hasDecoratedMethod(node, scope.imports) && exportIssue !== undefined) {
|
|
88
|
+
HandlerTypes.pushDiagnostic(scope, exportIssue.code, node.name ?? node, exportIssue.message, className);
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
const lineage = HandlerSources.receiverLineage(node, scope);
|
|
92
|
+
const entityBase = lineage?.base;
|
|
93
|
+
const stateSchema = lineage?.stateSchema;
|
|
94
|
+
const handlers = [];
|
|
95
|
+
for (const member of node.members) {
|
|
96
|
+
if (!ts.isMethodDeclaration(member)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const handler = HandlerSources.analyzeMethod(member, className, entityBase, stateSchema, lineage?.receiverKind, scope);
|
|
100
|
+
if (handler !== undefined) {
|
|
101
|
+
handlers.push(handler);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (lineage === undefined || handlers.length === 0) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return lineage.receiverKind === "entity"
|
|
108
|
+
? stateSchema === undefined
|
|
109
|
+
? undefined
|
|
110
|
+
: {
|
|
111
|
+
receiverKind: "entity",
|
|
112
|
+
className,
|
|
113
|
+
...(HandlerTypes.hasModifier(node, ts.SyntaxKind.DefaultKeyword)
|
|
114
|
+
? { defaultExport: true }
|
|
115
|
+
: {}),
|
|
116
|
+
sourceFile: scope.source.fileName,
|
|
117
|
+
stateSchema,
|
|
118
|
+
handlers,
|
|
119
|
+
}
|
|
120
|
+
: {
|
|
121
|
+
receiverKind: "standalone",
|
|
122
|
+
className,
|
|
123
|
+
...(HandlerTypes.hasModifier(node, ts.SyntaxKind.DefaultKeyword)
|
|
124
|
+
? { defaultExport: true }
|
|
125
|
+
: {}),
|
|
126
|
+
sourceFile: scope.source.fileName,
|
|
127
|
+
handlers,
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
analyzeMethod(node, className, entityBase, stateSchema, receiverKind, scope) {
|
|
131
|
+
const decorators = HandlerSources.methodDecorators(node, scope.imports);
|
|
132
|
+
const apply = decorators.find((decorator) => decorator.name === "Apply");
|
|
133
|
+
const handler = decorators.find(HandlerSources.isHandlerUse);
|
|
134
|
+
const whereUses = decorators.filter((decorator) => decorator.name === "Where");
|
|
135
|
+
if (apply !== undefined) {
|
|
136
|
+
HandlerTypes.pushDiagnostic(scope, "APPLY_DECORATOR", apply.node, "Generated registries do not support @Apply.", className, HandlerTypes.methodName(node));
|
|
137
|
+
}
|
|
138
|
+
if (handler === undefined) {
|
|
139
|
+
if (whereUses.length > 0) {
|
|
140
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_WHERE", whereUses[0]?.node ?? node, "@Where requires an Event-consuming @Subscribe, @React, or @Command handler.", className, HandlerTypes.methodName(node));
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
const method = HandlerTypes.methodName(node);
|
|
145
|
+
if ((entityBase === "Aggregate" || entityBase === "Projection") && handler.name === "Command") {
|
|
146
|
+
HandlerTypes.pushDiagnostic(scope, "UNSUPPORTED_COMMAND_HANDLER", handler.node, "Only Process Managers support @Command handlers.", className, method);
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
if (receiverKind === "standalone" &&
|
|
150
|
+
!HandlerSources.allowsStandaloneDecorator(entityBase, handler.name)) {
|
|
151
|
+
HandlerTypes.pushDiagnostic(scope, "UNSUPPORTED_COMMAND_HANDLER", handler.node, `@${handler.name} is not legal for standalone ${entityBase ?? "receiver"}.`, className, method);
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const invalid = HandlerSources.validateHandlerNode(node, handler.name, receiverKind === "entity" ? stateSchema : undefined, receiverKind !== "standalone", scope, className, method);
|
|
155
|
+
if (handler.hasArguments) {
|
|
156
|
+
HandlerTypes.pushDiagnostic(scope, "SCHEMA_BEARING_DECORATOR", handler.node, `@${handler.name}(...) is not supported in analyzed app source.`, className, method);
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
if (invalid) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const origin = HandlerSources.externalOrigin(node.parameters, scope, className, method);
|
|
163
|
+
if (origin === undefined)
|
|
164
|
+
return undefined;
|
|
165
|
+
const signal = HandlerSources.schemaUseFromType(origin.type, scope.imports);
|
|
166
|
+
const emittedSchemas = HandlerSources.emittedSchemaUses(node.type === undefined ? undefined : HandlerSources.unwrapOuterPromise(node.type, scope), handler.name, scope.imports)?.map((schema) => schema.reference);
|
|
167
|
+
if (signal === undefined || emittedSchemas === undefined || method === undefined) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
if (!HandlerSources.validContextParameter(node.parameters, signal.kind, scope, className, method)) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
const where = HandlerSources.whereDeclaration(whereUses, handler, signal.kind, scope, className, method);
|
|
174
|
+
if (whereUses.length > 0 && where === undefined) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
kind: HandlerSources.handlerKind(handler.name, signal.kind),
|
|
179
|
+
methodName: method,
|
|
180
|
+
signalSchema: signal.reference,
|
|
181
|
+
emittedSchemas,
|
|
182
|
+
parameterCount: node.parameters.length,
|
|
183
|
+
origin: origin.value,
|
|
184
|
+
...(where === undefined ? {} : { where }),
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
whereDeclaration(uses, handler, signalKind, scope, className, methodName) {
|
|
188
|
+
if (uses.length === 0)
|
|
189
|
+
return undefined;
|
|
190
|
+
const use = uses[0];
|
|
191
|
+
if (uses.length !== 1 ||
|
|
192
|
+
(handler.name !== "Subscribe" && handler.name !== "React" && handler.name !== "Command") ||
|
|
193
|
+
(signalKind !== "event" && signalKind !== "rejection") ||
|
|
194
|
+
use === undefined) {
|
|
195
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_WHERE", use?.node ?? handler.node, "@Where is allowed once on an Event-consuming @Subscribe, @React, or @Command handler.", className, methodName);
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
const expression = use.node.expression;
|
|
199
|
+
if (!ts.isCallExpression(expression) || expression.arguments.length !== 1) {
|
|
200
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_WHERE", use.node, "@Where requires one object literal.", className, methodName);
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
const options = expression.arguments[0];
|
|
204
|
+
if (options === undefined || !ts.isObjectLiteralExpression(options)) {
|
|
205
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_WHERE", expression, "@Where options must be an object literal.", className, methodName);
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
const values = new Map();
|
|
209
|
+
for (const property of options.properties) {
|
|
210
|
+
if (!ts.isPropertyAssignment(property) ||
|
|
211
|
+
(property.name.kind !== ts.SyntaxKind.Identifier &&
|
|
212
|
+
!ts.isStringLiteralLike(property.name)) ||
|
|
213
|
+
!ts.isStringLiteralLike(property.initializer)) {
|
|
214
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_WHERE", property, "@Where accepts only eventField and equals string literals.", className, methodName);
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
const name = property.name.text;
|
|
218
|
+
if ((name !== "eventField" && name !== "equals") || values.has(name)) {
|
|
219
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_WHERE", property, "@Where accepts each of eventField and equals exactly once.", className, methodName);
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
values.set(name, property.initializer.text);
|
|
223
|
+
}
|
|
224
|
+
const eventField = values.get("eventField");
|
|
225
|
+
const equals = values.get("equals");
|
|
226
|
+
if (eventField === undefined || eventField.trim().length === 0 || equals === undefined) {
|
|
227
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_WHERE", options, "@Where requires non-empty eventField and string equals.", className, methodName);
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
return Object.freeze({ eventField, equals });
|
|
231
|
+
},
|
|
232
|
+
hasDecoratedMethod(node, imports) {
|
|
233
|
+
return node.members.some((member) => ts.isMethodDeclaration(member) &&
|
|
234
|
+
HandlerSources.methodDecorators(member, imports).length > 0);
|
|
235
|
+
},
|
|
236
|
+
receiverExportIssue(node, source) {
|
|
237
|
+
if (HandlerTypes.hasModifier(node, ts.SyntaxKind.DefaultKeyword))
|
|
238
|
+
return undefined;
|
|
239
|
+
if (HandlerTypes.hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
if (node.name !== undefined && HandlerSources.hasNamedClassExport(node.name.text, source)) {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
code: "NON_EXPORTED_RECEIVER_CLASS",
|
|
247
|
+
message: "Decorated receiver classes must be exported for generated registry imports.",
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
hasNamedClassExport(className, source) {
|
|
251
|
+
return source.statements.some((statement) => ts.isExportDeclaration(statement) &&
|
|
252
|
+
statement.moduleSpecifier === undefined &&
|
|
253
|
+
statement.exportClause !== undefined &&
|
|
254
|
+
HandlerSources.namedExportIncludes(statement.exportClause, className));
|
|
255
|
+
},
|
|
256
|
+
namedExportIncludes(clause, className) {
|
|
257
|
+
return (ts.isNamedExports(clause) &&
|
|
258
|
+
clause.elements.some((element) => element.name.text === className &&
|
|
259
|
+
(element.propertyName === undefined || element.propertyName.text === className)));
|
|
260
|
+
},
|
|
261
|
+
validateHandlerNode(node, decorator, stateSchema, requiresEntityState, scope, className, method) {
|
|
262
|
+
return [
|
|
263
|
+
HandlerSources.validateEntityState(node, stateSchema, requiresEntityState, scope, className, method),
|
|
264
|
+
HandlerSources.validateName(node, scope, className),
|
|
265
|
+
HandlerSources.validateVisibility(node, scope, className, method),
|
|
266
|
+
HandlerSources.validateParameters(node, decorator, scope, className, method),
|
|
267
|
+
HandlerSources.validateReturn(node, decorator, scope, className, method),
|
|
268
|
+
].some(Boolean);
|
|
269
|
+
},
|
|
270
|
+
validateEntityState(node, stateSchema, requiresEntityState, scope, className, method) {
|
|
271
|
+
if (!requiresEntityState || stateSchema !== undefined) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
HandlerTypes.pushDiagnostic(scope, "MISSING_ENTITY_STATE_SCHEMA", node, "Decorated handlers must be declared on an entity class with an inferred state schema.", className, method);
|
|
275
|
+
return true;
|
|
276
|
+
},
|
|
277
|
+
validateName(node, scope, className) {
|
|
278
|
+
if (HandlerTypes.methodName(node) !== undefined) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_HANDLER_NAME", node.name, "Decorated handlers must use a string method name.", className);
|
|
282
|
+
return true;
|
|
283
|
+
},
|
|
284
|
+
validateVisibility(node, scope, className, method) {
|
|
285
|
+
if (!HandlerTypes.hasModifier(node, ts.SyntaxKind.StaticKeyword) &&
|
|
286
|
+
!HandlerTypes.hasModifier(node, ts.SyntaxKind.PrivateKeyword) &&
|
|
287
|
+
!HandlerTypes.hasModifier(node, ts.SyntaxKind.ProtectedKeyword)) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_HANDLER_VISIBILITY", node.name, "Decorated handlers must be public instance methods.", className, method);
|
|
291
|
+
return true;
|
|
292
|
+
},
|
|
293
|
+
validateParameters(node, decorator, scope, className, method) {
|
|
294
|
+
if (node.parameters.length !== 1 && node.parameters.length !== 2) {
|
|
295
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_PARAMETER_COUNT", node, "Decorated handlers must declare one or two public parameters.", className, method);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
if (node.parameters[0]?.type === undefined) {
|
|
299
|
+
HandlerTypes.pushDiagnostic(scope, "MISSING_SIGNAL_TYPE", node, `@${decorator} handlers require an explicit first parameter type.`, className, method);
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
const origin = HandlerSources.externalOrigin(node.parameters, scope, className, method);
|
|
303
|
+
if (origin === undefined)
|
|
304
|
+
return true;
|
|
305
|
+
const signal = HandlerSources.schemaUseFromType(origin.type, scope.imports);
|
|
306
|
+
if (signal !== undefined && HandlerSources.acceptsSignalKind(decorator, signal.kind)) {
|
|
307
|
+
if (origin.value === "external" && signal.kind === "command") {
|
|
308
|
+
HandlerTypes.pushDiagnostic(scope, "EXTERNAL_COMMAND_RECEIVER", node.parameters[0].type, "Command receivers cannot declare External<Command>.", className, method);
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_SIGNAL_TYPE", node.parameters[0].type, `@${decorator} first parameter must be ${HandlerSources.signalMessage(decorator)}.`, className, method);
|
|
314
|
+
return true;
|
|
315
|
+
},
|
|
316
|
+
validateReturn(node, decorator, scope, className, method) {
|
|
317
|
+
if (decorator === "Subscribe") {
|
|
318
|
+
return HandlerSources.validateSubscribeReturn(node, scope, className, method);
|
|
319
|
+
}
|
|
320
|
+
if (node.type === undefined) {
|
|
321
|
+
HandlerTypes.pushDiagnostic(scope, "MISSING_RETURN_TYPE", node, `@${decorator} handlers require an explicit return type.`, className, method);
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
const returnType = HandlerSources.unwrapOuterPromise(node.type, scope);
|
|
325
|
+
const envelope = HandlerSources.frameworkEnvelope(returnType, scope.imports);
|
|
326
|
+
if (envelope !== undefined) {
|
|
327
|
+
HandlerTypes.pushDiagnostic(scope, "FRAMEWORK_ENVELOPE_RETURN", node.type, `Handler return type must not be framework ${envelope}.`, className, method);
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
return HandlerSources.validateEmittedReturn(node, decorator, scope, className, method, returnType);
|
|
331
|
+
},
|
|
332
|
+
validateSubscribeReturn(node, scope, className, method) {
|
|
333
|
+
if (node.type !== undefined &&
|
|
334
|
+
HandlerSources.isExplicitVoidType(HandlerSources.unwrapOuterPromise(node.type, scope))) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_SUBSCRIBE_RETURN", node, "@Subscribe handlers must return explicit void.", className, method);
|
|
338
|
+
return true;
|
|
339
|
+
},
|
|
340
|
+
validateEmittedReturn(node, decorator, scope, className, method, returnType = node.type) {
|
|
341
|
+
const schemas = HandlerSources.emittedSchemaUses(returnType, decorator, scope.imports);
|
|
342
|
+
if (schemas === undefined) {
|
|
343
|
+
HandlerTypes.pushDiagnostic(scope, "UNSUPPORTED_RETURN_TYPE", returnType ?? node, `@${decorator} return type must resolve to generated schema references.`, className, method);
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
const expected = HandlerSources.emittedSignalKind(decorator);
|
|
347
|
+
if (expected !== undefined && schemas.some((schema) => schema.kind !== expected)) {
|
|
348
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_EMITTED_SCHEMA", returnType ?? node, `@${decorator} return type must emit generated ${expected} schemas.`, className, method);
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
if ((decorator === "Assign" || decorator === "Command") && schemas.length === 0) {
|
|
352
|
+
HandlerTypes.pushDiagnostic(scope, "MISSING_EMITTED_SCHEMAS", returnType ?? node, `@${decorator} handlers must emit at least one schema.`, className, method);
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
if (decorator === "React" &&
|
|
356
|
+
schemas.length === 0 &&
|
|
357
|
+
!HandlerSources.isExplicitVoidType(returnType)) {
|
|
358
|
+
HandlerTypes.pushDiagnostic(scope, "MISSING_EMITTED_SCHEMAS", returnType ?? node, "@React handlers must emit at least one schema unless they return explicit void.", className, method);
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
},
|
|
363
|
+
methodDecorators(node, imports) {
|
|
364
|
+
return (ts.getDecorators(node) ?? []).flatMap((decorator) => {
|
|
365
|
+
const expression = ts.isCallExpression(decorator.expression)
|
|
366
|
+
? decorator.expression.expression
|
|
367
|
+
: decorator.expression;
|
|
368
|
+
const name = HandlerSources.serverDecoratorName(expression, imports);
|
|
369
|
+
return name === undefined
|
|
370
|
+
? []
|
|
371
|
+
: [{ hasArguments: ts.isCallExpression(decorator.expression), name, node: decorator }];
|
|
372
|
+
});
|
|
373
|
+
},
|
|
374
|
+
serverDecoratorName(expression, imports) {
|
|
375
|
+
if (ts.isIdentifier(expression)) {
|
|
376
|
+
return HandlerTypes.serverDecorator(imports.serverSymbols.get(expression.text));
|
|
377
|
+
}
|
|
378
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
379
|
+
const namespace = HandlerTypes.expressionName(expression.expression);
|
|
380
|
+
if (namespace !== undefined && imports.serverNamespaces.has(namespace)) {
|
|
381
|
+
return HandlerTypes.serverDecorator(expression.name.text);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return undefined;
|
|
385
|
+
},
|
|
386
|
+
isHandlerUse(decorator) {
|
|
387
|
+
return handlerDecorators.has(decorator.name);
|
|
388
|
+
},
|
|
389
|
+
allowsStandaloneDecorator(base, decorator) {
|
|
390
|
+
return ((base === "AbstractAssignee" && decorator === "Assign") ||
|
|
391
|
+
(base === "AbstractCommander" && decorator === "Command") ||
|
|
392
|
+
(base === "AbstractEventReactor" && decorator === "React") ||
|
|
393
|
+
(base === "AbstractEventSubscriber" && decorator === "Subscribe"));
|
|
394
|
+
},
|
|
395
|
+
receiverLineage(node, scope, seen = new Set()) {
|
|
396
|
+
if (seen.has(node))
|
|
397
|
+
return undefined;
|
|
398
|
+
const nextSeen = new Set(seen);
|
|
399
|
+
nextSeen.add(node);
|
|
400
|
+
for (const clause of node.heritageClauses ?? []) {
|
|
401
|
+
for (const type of clause.types) {
|
|
402
|
+
const direct = HandlerSources.directReceiverLineage(type, scope);
|
|
403
|
+
if (direct !== undefined)
|
|
404
|
+
return direct;
|
|
405
|
+
const inherited = HandlerSources.inheritedReceiverLineage(type.expression, scope, nextSeen);
|
|
406
|
+
if (inherited !== undefined)
|
|
407
|
+
return inherited;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return undefined;
|
|
411
|
+
},
|
|
412
|
+
directReceiverLineage(type, scope) {
|
|
413
|
+
const base = HandlerSources.directEntityBaseName(type.expression, scope.imports);
|
|
414
|
+
if (base !== undefined) {
|
|
415
|
+
const stateType = type.typeArguments?.[1];
|
|
416
|
+
const stateSchema = stateType === undefined
|
|
417
|
+
? undefined
|
|
418
|
+
: HandlerSources.schemaFromTypeQuery(stateType, scope.imports, HandlerSources.newTypeWalk());
|
|
419
|
+
return { receiverKind: "entity", base, stateSchema };
|
|
420
|
+
}
|
|
421
|
+
const standalone = HandlerSources.directStandaloneBaseName(type.expression, scope.imports);
|
|
422
|
+
if (standalone !== undefined)
|
|
423
|
+
return { receiverKind: "standalone", base: standalone, stateSchema: undefined };
|
|
424
|
+
},
|
|
425
|
+
inheritedReceiverLineage(expression, scope, seen) {
|
|
426
|
+
const parent = HandlerSources.classDeclarationFor(expression, scope);
|
|
427
|
+
if (parent === undefined)
|
|
428
|
+
return undefined;
|
|
429
|
+
const source = parent.getSourceFile();
|
|
430
|
+
return HandlerSources.receiverLineage(parent, { ...scope, source, imports: HandlerSources.buildImportState(source, scope.program) }, seen);
|
|
431
|
+
},
|
|
432
|
+
directEntityBaseName(expression, imports) {
|
|
433
|
+
if (ts.isIdentifier(expression)) {
|
|
434
|
+
const base = imports.serverSymbols.get(expression.text);
|
|
435
|
+
return base !== undefined && entityBaseNames.has(base) ? base : undefined;
|
|
436
|
+
}
|
|
437
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
438
|
+
const namespace = HandlerTypes.expressionName(expression.expression);
|
|
439
|
+
return namespace !== undefined &&
|
|
440
|
+
imports.serverNamespaces.has(namespace) &&
|
|
441
|
+
entityBaseNames.has(expression.name.text)
|
|
442
|
+
? expression.name.text
|
|
443
|
+
: undefined;
|
|
444
|
+
}
|
|
445
|
+
return undefined;
|
|
446
|
+
},
|
|
447
|
+
directStandaloneBaseName(expression, imports) {
|
|
448
|
+
if (ts.isIdentifier(expression)) {
|
|
449
|
+
const base = imports.serverSymbols.get(expression.text);
|
|
450
|
+
return base !== undefined && standaloneBaseNames.has(base) ? base : undefined;
|
|
451
|
+
}
|
|
452
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
453
|
+
const namespace = HandlerTypes.expressionName(expression.expression);
|
|
454
|
+
return namespace !== undefined &&
|
|
455
|
+
imports.serverNamespaces.has(namespace) &&
|
|
456
|
+
standaloneBaseNames.has(expression.name.text)
|
|
457
|
+
? expression.name.text
|
|
458
|
+
: undefined;
|
|
459
|
+
}
|
|
460
|
+
return undefined;
|
|
461
|
+
},
|
|
462
|
+
classDeclarationFor(expression, scope) {
|
|
463
|
+
const checker = scope.program.getTypeChecker();
|
|
464
|
+
const location = ts.isPropertyAccessExpression(expression) ? expression.name : expression;
|
|
465
|
+
let symbol = checker.getSymbolAtLocation(location);
|
|
466
|
+
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
|
|
467
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
468
|
+
}
|
|
469
|
+
const declaration = symbol?.declarations?.find(ts.isClassDeclaration);
|
|
470
|
+
if (declaration !== undefined)
|
|
471
|
+
return declaration;
|
|
472
|
+
const declarations = checker.getTypeAtLocation(expression).getSymbol()?.declarations;
|
|
473
|
+
return declarations === undefined ? undefined : declarations.find(ts.isClassDeclaration);
|
|
474
|
+
},
|
|
475
|
+
isEntityBase(expression, imports) {
|
|
476
|
+
return HandlerSources.directEntityBaseName(expression, imports) !== undefined;
|
|
477
|
+
},
|
|
478
|
+
schemaFromTypeQuery(typeNode, imports, walk) {
|
|
479
|
+
if (!HandlerSources.consumeTypeWalk(walk)) {
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
if (ts.isParenthesizedTypeNode(typeNode)) {
|
|
483
|
+
return HandlerSources.schemaFromTypeQuery(typeNode.type, imports, walk);
|
|
484
|
+
}
|
|
485
|
+
if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
|
|
486
|
+
const alias = imports.localTypeAliases.get(typeNode.typeName.text);
|
|
487
|
+
return alias === undefined
|
|
488
|
+
? undefined
|
|
489
|
+
: HandlerSources.resolveAlias(typeNode.typeName.text, alias, walk, (resolved) => HandlerSources.schemaFromTypeQuery(resolved, imports, walk));
|
|
490
|
+
}
|
|
491
|
+
if (!ts.isTypeQueryNode(typeNode)) {
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
return HandlerSources.schemaFromEntityName(typeNode.exprName, imports);
|
|
495
|
+
},
|
|
496
|
+
emittedSchemaUses(typeNode, decorator, imports) {
|
|
497
|
+
if (decorator === "Subscribe") {
|
|
498
|
+
return typeNode?.kind === ts.SyntaxKind.VoidKeyword ? [] : undefined;
|
|
499
|
+
}
|
|
500
|
+
if (typeNode?.kind === ts.SyntaxKind.VoidKeyword) {
|
|
501
|
+
return [];
|
|
502
|
+
}
|
|
503
|
+
if (typeNode === undefined) {
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
return HandlerSources.schemaListFromType(typeNode, imports, HandlerSources.newTypeWalk());
|
|
507
|
+
},
|
|
508
|
+
schemaListFromType(typeNode, imports, walk) {
|
|
509
|
+
if (!HandlerSources.consumeTypeWalk(walk)) {
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
const unwrapped = HandlerSources.unwrapReadonly(typeNode);
|
|
513
|
+
if (ts.isArrayTypeNode(unwrapped)) {
|
|
514
|
+
const item = HandlerSources.schemaUseFromType(unwrapped.elementType, imports, walk);
|
|
515
|
+
return item === undefined ? undefined : [item];
|
|
516
|
+
}
|
|
517
|
+
if (ts.isTupleTypeNode(unwrapped)) {
|
|
518
|
+
return HandlerSources.schemaListFromTuple(unwrapped, imports, walk);
|
|
519
|
+
}
|
|
520
|
+
if (ts.isTypeReferenceNode(unwrapped) && HandlerSources.isArrayReferenceType(unwrapped)) {
|
|
521
|
+
const item = HandlerSources.schemaUseFromType(unwrapped.typeArguments?.[0], imports, walk);
|
|
522
|
+
return item === undefined ? undefined : [item];
|
|
523
|
+
}
|
|
524
|
+
const schema = HandlerSources.schemaUseFromType(unwrapped, imports, walk);
|
|
525
|
+
return schema === undefined ? undefined : [schema];
|
|
526
|
+
},
|
|
527
|
+
schemaListFromTuple(typeNode, imports, walk) {
|
|
528
|
+
const schemas = [];
|
|
529
|
+
for (const element of typeNode.elements) {
|
|
530
|
+
const schema = HandlerSources.schemaFromTupleElement(element, imports, walk);
|
|
531
|
+
if (schema === undefined) {
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
schemas.push(schema);
|
|
535
|
+
}
|
|
536
|
+
return schemas;
|
|
537
|
+
},
|
|
538
|
+
schemaFromTupleElement(typeNode, imports, walk) {
|
|
539
|
+
if (ts.isNamedTupleMember(typeNode)) {
|
|
540
|
+
return typeNode.questionToken === undefined && typeNode.dotDotDotToken === undefined
|
|
541
|
+
? HandlerSources.schemaUseFromType(typeNode.type, imports, walk)
|
|
542
|
+
: undefined;
|
|
543
|
+
}
|
|
544
|
+
if (ts.isRestTypeNode(typeNode) || ts.isOptionalTypeNode(typeNode)) {
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
return HandlerSources.schemaUseFromType(typeNode, imports, walk);
|
|
548
|
+
},
|
|
549
|
+
schemaUseFromType(typeNode, imports, walk = HandlerSources.newTypeWalk()) {
|
|
550
|
+
if (typeNode === undefined) {
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
if (!HandlerSources.consumeTypeWalk(walk)) {
|
|
554
|
+
return undefined;
|
|
555
|
+
}
|
|
556
|
+
const unwrapped = HandlerSources.unwrapReadonly(typeNode);
|
|
557
|
+
if (!ts.isTypeReferenceNode(unwrapped)) {
|
|
558
|
+
return undefined;
|
|
559
|
+
}
|
|
560
|
+
if (ts.isIdentifier(unwrapped.typeName)) {
|
|
561
|
+
const alias = imports.localTypeAliases.get(unwrapped.typeName.text);
|
|
562
|
+
if (alias !== undefined) {
|
|
563
|
+
return HandlerSources.resolveAlias(unwrapped.typeName.text, alias, walk, (resolved) => HandlerSources.schemaUseFromType(resolved, imports, walk));
|
|
564
|
+
}
|
|
565
|
+
return HandlerSources.schemaUseFromSymbol(unwrapped.typeName.text, imports);
|
|
566
|
+
}
|
|
567
|
+
if (ts.isQualifiedName(unwrapped.typeName)) {
|
|
568
|
+
return HandlerSources.schemaUseFromName(unwrapped.typeName, imports);
|
|
569
|
+
}
|
|
570
|
+
return undefined;
|
|
571
|
+
},
|
|
572
|
+
schemaUseFromSymbol(name, imports) {
|
|
573
|
+
const symbol = imports.generatedSymbols.get(name);
|
|
574
|
+
if (symbol?.schemaExportName === undefined) {
|
|
575
|
+
return undefined;
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
kind: symbol.kind,
|
|
579
|
+
reference: { moduleSpecifier: symbol.moduleSpecifier, exportName: symbol.schemaExportName },
|
|
580
|
+
};
|
|
581
|
+
},
|
|
582
|
+
schemaUseFromName(name, imports) {
|
|
583
|
+
if (!ts.isIdentifier(name.left)) {
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
|
586
|
+
const namespace = imports.generatedNamespaces.get(name.left.text);
|
|
587
|
+
const schemaName = `${name.right.text}Schema`;
|
|
588
|
+
return namespace === undefined ||
|
|
589
|
+
!HandlerTypes.hasGeneratedType(namespace.exports, name.right.text) ||
|
|
590
|
+
!namespace.exports.values.has(schemaName)
|
|
591
|
+
? undefined
|
|
592
|
+
: {
|
|
593
|
+
kind: namespace.exports.schemaRoles.get(schemaName),
|
|
594
|
+
reference: { moduleSpecifier: namespace.moduleSpecifier, exportName: schemaName },
|
|
595
|
+
};
|
|
596
|
+
},
|
|
597
|
+
schemaFromEntityName(name, imports) {
|
|
598
|
+
if (ts.isIdentifier(name)) {
|
|
599
|
+
const symbol = imports.generatedSymbols.get(name.text);
|
|
600
|
+
return symbol?.schemaValue !== true
|
|
601
|
+
? undefined
|
|
602
|
+
: { moduleSpecifier: symbol.moduleSpecifier, exportName: symbol.exportName };
|
|
603
|
+
}
|
|
604
|
+
if (ts.isQualifiedName(name) && ts.isIdentifier(name.left)) {
|
|
605
|
+
const namespace = imports.generatedNamespaces.get(name.left.text);
|
|
606
|
+
return namespace?.exports.values.has(name.right.text) !== true
|
|
607
|
+
? undefined
|
|
608
|
+
: { moduleSpecifier: namespace.moduleSpecifier, exportName: name.right.text };
|
|
609
|
+
}
|
|
610
|
+
return undefined;
|
|
611
|
+
},
|
|
612
|
+
frameworkEnvelope(typeNode, imports, walk = HandlerSources.newTypeWalk()) {
|
|
613
|
+
if (!HandlerSources.consumeTypeWalk(walk)) {
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
const unwrapped = HandlerSources.unwrapReadonly(typeNode);
|
|
617
|
+
if (!ts.isTypeReferenceNode(unwrapped)) {
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
620
|
+
if (ts.isIdentifier(unwrapped.typeName)) {
|
|
621
|
+
const alias = imports.localTypeAliases.get(unwrapped.typeName.text);
|
|
622
|
+
if (alias !== undefined) {
|
|
623
|
+
return HandlerSources.resolveAlias(unwrapped.typeName.text, alias, walk, (resolved) => HandlerSources.frameworkEnvelope(resolved, imports, walk));
|
|
624
|
+
}
|
|
625
|
+
return imports.protoSymbols.has(unwrapped.typeName.text)
|
|
626
|
+
? unwrapped.typeName.text
|
|
627
|
+
: undefined;
|
|
628
|
+
}
|
|
629
|
+
if (ts.isQualifiedName(unwrapped.typeName) && ts.isIdentifier(unwrapped.typeName.left)) {
|
|
630
|
+
const namespace = unwrapped.typeName.left.text;
|
|
631
|
+
const name = unwrapped.typeName.right.text;
|
|
632
|
+
return imports.protoNamespaces.has(namespace) && (name === "Event" || name === "Command")
|
|
633
|
+
? `${namespace}.${name}`
|
|
634
|
+
: undefined;
|
|
635
|
+
}
|
|
636
|
+
return undefined;
|
|
637
|
+
},
|
|
638
|
+
unwrapReadonly(typeNode) {
|
|
639
|
+
if (ts.isParenthesizedTypeNode(typeNode)) {
|
|
640
|
+
return HandlerSources.unwrapReadonly(typeNode.type);
|
|
641
|
+
}
|
|
642
|
+
if (ts.isTypeOperatorNode(typeNode) && typeNode.operator === ts.SyntaxKind.ReadonlyKeyword) {
|
|
643
|
+
return HandlerSources.unwrapReadonly(typeNode.type);
|
|
644
|
+
}
|
|
645
|
+
return typeNode;
|
|
646
|
+
},
|
|
647
|
+
unwrapOuterPromise(typeNode, scope, walk = HandlerSources.newTypeWalk()) {
|
|
648
|
+
if (!HandlerSources.consumeTypeWalk(walk)) {
|
|
649
|
+
return typeNode;
|
|
650
|
+
}
|
|
651
|
+
const unwrapped = HandlerSources.unwrapReadonly(typeNode);
|
|
652
|
+
if (!ts.isTypeReferenceNode(unwrapped) || !ts.isIdentifier(unwrapped.typeName)) {
|
|
653
|
+
return typeNode;
|
|
654
|
+
}
|
|
655
|
+
const alias = scope.imports.localTypeAliases.get(unwrapped.typeName.text);
|
|
656
|
+
if (alias !== undefined) {
|
|
657
|
+
return (HandlerSources.resolveAlias(unwrapped.typeName.text, alias, walk, (resolved) => HandlerSources.unwrapOuterPromise(resolved, scope, walk)) ?? typeNode);
|
|
658
|
+
}
|
|
659
|
+
if (unwrapped.typeName.text !== "Promise" ||
|
|
660
|
+
unwrapped.typeArguments?.length !== 1 ||
|
|
661
|
+
!HandlerSources.isBuiltInPromise(unwrapped, scope.program)) {
|
|
662
|
+
return typeNode;
|
|
663
|
+
}
|
|
664
|
+
return unwrapped.typeArguments[0] ?? typeNode;
|
|
665
|
+
},
|
|
666
|
+
isBuiltInPromise(typeNode, program) {
|
|
667
|
+
const symbol = program.getTypeChecker().getTypeFromTypeNode(typeNode).getSymbol();
|
|
668
|
+
return (symbol?.getName() === "Promise" &&
|
|
669
|
+
symbol.declarations?.some((declaration) => program.isSourceFileDefaultLibrary(declaration.getSourceFile())) === true);
|
|
670
|
+
},
|
|
671
|
+
isExplicitVoidType(typeNode) {
|
|
672
|
+
if (typeNode === undefined) {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
if (ts.isParenthesizedTypeNode(typeNode)) {
|
|
676
|
+
return HandlerSources.isExplicitVoidType(typeNode.type);
|
|
677
|
+
}
|
|
678
|
+
return typeNode.kind === ts.SyntaxKind.VoidKeyword;
|
|
679
|
+
},
|
|
680
|
+
isArrayReferenceType(typeNode) {
|
|
681
|
+
return (ts.isIdentifier(typeNode.typeName) &&
|
|
682
|
+
(typeNode.typeName.text === "Array" || typeNode.typeName.text === "ReadonlyArray"));
|
|
683
|
+
},
|
|
684
|
+
handlerKind(decorator, signalKind) {
|
|
685
|
+
switch (decorator) {
|
|
686
|
+
case "Assign":
|
|
687
|
+
return "command-assignment";
|
|
688
|
+
case "Command":
|
|
689
|
+
return signalKind === "command" ? "command-substitution" : "command-reaction";
|
|
690
|
+
case "React":
|
|
691
|
+
return "event-reaction";
|
|
692
|
+
case "Subscribe":
|
|
693
|
+
return signalKind === "state" ? "state-subscription" : "event-subscription";
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
acceptsSignalKind(decorator, kind) {
|
|
697
|
+
if (decorator === "Command") {
|
|
698
|
+
return kind === "command" || kind === "event" || kind === "rejection";
|
|
699
|
+
}
|
|
700
|
+
if (decorator === "Assign") {
|
|
701
|
+
return kind === "command";
|
|
702
|
+
}
|
|
703
|
+
return (kind === "event" || kind === "rejection" || (decorator === "Subscribe" && kind === "state"));
|
|
704
|
+
},
|
|
705
|
+
signalMessage(decorator) {
|
|
706
|
+
if (decorator === "Command") {
|
|
707
|
+
return "a generated command, event, or rejection type";
|
|
708
|
+
}
|
|
709
|
+
if (decorator === "Assign") {
|
|
710
|
+
return "a generated command type";
|
|
711
|
+
}
|
|
712
|
+
return decorator === "Subscribe"
|
|
713
|
+
? "a generated event, rejection, or Entity state type"
|
|
714
|
+
: "a generated event or rejection type";
|
|
715
|
+
},
|
|
716
|
+
emittedSignalKind(decorator) {
|
|
717
|
+
if (decorator === "Command") {
|
|
718
|
+
return "command";
|
|
719
|
+
}
|
|
720
|
+
if (decorator === "Assign" || decorator === "React") {
|
|
721
|
+
return "event";
|
|
722
|
+
}
|
|
723
|
+
return undefined;
|
|
724
|
+
},
|
|
725
|
+
externalOrigin(parameters, scope, className, method) {
|
|
726
|
+
const first = parameters[0]?.type;
|
|
727
|
+
if (first === undefined)
|
|
728
|
+
return undefined;
|
|
729
|
+
const marker = HandlerSources.externalMarker(first, scope);
|
|
730
|
+
const laterMarker = parameters
|
|
731
|
+
.slice(1)
|
|
732
|
+
.some((parameter) => parameter.type === undefined
|
|
733
|
+
? false
|
|
734
|
+
: HandlerSources.containsExternalMarker(parameter.type, scope));
|
|
735
|
+
const nestedMarker = marker === undefined && HandlerSources.containsExternalMarker(first, scope);
|
|
736
|
+
if (laterMarker || nestedMarker || (marker !== undefined && !marker.direct)) {
|
|
737
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_EXTERNAL_ORIGIN", laterMarker ? (parameters[1]?.type ?? first) : first, "External<T> is valid only as the direct first receptor parameter type.", className, method);
|
|
738
|
+
return undefined;
|
|
739
|
+
}
|
|
740
|
+
if (marker?.direct === true && marker.type !== undefined) {
|
|
741
|
+
return { value: "external", type: marker.type };
|
|
742
|
+
}
|
|
743
|
+
return { value: "domestic", type: first };
|
|
744
|
+
},
|
|
745
|
+
validContextParameter(parameters, signalKind, scope, className, method) {
|
|
746
|
+
if (parameters.length !== 2)
|
|
747
|
+
return true;
|
|
748
|
+
const context = parameters[1]?.type;
|
|
749
|
+
const expected = signalKind === "command" ? "CommandContext" : "EventContext";
|
|
750
|
+
if (context !== undefined && HandlerSources.isCanonicalContext(context, expected, scope)) {
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_HANDLER_CONTEXT", context ?? parameters[1] ?? scope.source, `Two-argument handlers receiving ${signalKind === "command" ? "Commands" : "Events, rejections, or Entity states"} must declare ${expected} as their second parameter.`, className, method);
|
|
754
|
+
return false;
|
|
755
|
+
},
|
|
756
|
+
isCanonicalContext(type, expected, scope) {
|
|
757
|
+
if (!ts.isTypeReferenceNode(type))
|
|
758
|
+
return false;
|
|
759
|
+
if (ts.isIdentifier(type.typeName)) {
|
|
760
|
+
return scope.imports.protoContextSymbols.get(type.typeName.text) === expected;
|
|
761
|
+
}
|
|
762
|
+
return (ts.isIdentifier(type.typeName.left) &&
|
|
763
|
+
scope.imports.protoNamespaces.has(type.typeName.left.text) &&
|
|
764
|
+
type.typeName.right.text === expected);
|
|
765
|
+
},
|
|
766
|
+
externalMarker(type, scope) {
|
|
767
|
+
if (!ts.isTypeReferenceNode(type))
|
|
768
|
+
return undefined;
|
|
769
|
+
const canonical = (ts.isIdentifier(type.typeName) &&
|
|
770
|
+
scope.imports.serverSymbols.get(type.typeName.text) === "External") ||
|
|
771
|
+
(ts.isQualifiedName(type.typeName) &&
|
|
772
|
+
ts.isIdentifier(type.typeName.left) &&
|
|
773
|
+
type.typeName.right.text === "External" &&
|
|
774
|
+
scope.imports.serverNamespaces.has(type.typeName.left.text));
|
|
775
|
+
if (!canonical || !HandlerSources.isCanonicalExternal(type.typeName, scope.program))
|
|
776
|
+
return undefined;
|
|
777
|
+
const argument = type.typeArguments?.[0];
|
|
778
|
+
return argument === undefined
|
|
779
|
+
? { direct: false }
|
|
780
|
+
: { direct: type.typeArguments?.length === 1, type: argument };
|
|
781
|
+
},
|
|
782
|
+
containsExternalMarker(type, scope, seen = new Set()) {
|
|
783
|
+
if (HandlerSources.externalMarker(type, scope) !== undefined)
|
|
784
|
+
return true;
|
|
785
|
+
if (ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)) {
|
|
786
|
+
const alias = scope.imports.localTypeAliases.get(type.typeName.text);
|
|
787
|
+
if (alias !== undefined && !seen.has(type.typeName.text)) {
|
|
788
|
+
const next = new Set(seen);
|
|
789
|
+
next.add(type.typeName.text);
|
|
790
|
+
if (HandlerSources.containsExternalMarker(alias, scope, next))
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
let found = false;
|
|
795
|
+
const visit = (node) => {
|
|
796
|
+
if (found)
|
|
797
|
+
return;
|
|
798
|
+
if (ts.isTypeNode(node) && HandlerSources.externalMarker(node, scope) !== undefined) {
|
|
799
|
+
found = true;
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
ts.forEachChild(node, visit);
|
|
803
|
+
};
|
|
804
|
+
ts.forEachChild(type, visit);
|
|
805
|
+
return found;
|
|
806
|
+
},
|
|
807
|
+
isCanonicalExternal(name, program) {
|
|
808
|
+
const checker = program.getTypeChecker();
|
|
809
|
+
const location = ts.isQualifiedName(name) ? name.right : name;
|
|
810
|
+
let symbol = checker.getSymbolAtLocation(location);
|
|
811
|
+
if (symbol === undefined)
|
|
812
|
+
return false;
|
|
813
|
+
if ((symbol.flags & ts.SymbolFlags.Alias) !== 0) {
|
|
814
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
815
|
+
}
|
|
816
|
+
return (symbol.declarations ?? []).some((declaration) => ts.isTypeAliasDeclaration(declaration) &&
|
|
817
|
+
declaration.name.text === "External" &&
|
|
818
|
+
PackageIdentity.nameFor(declaration.getSourceFile().fileName) ===
|
|
819
|
+
"@spine-event-engine/server" &&
|
|
820
|
+
/^external(?:\.d)?\.ts$/u.test(declaration.getSourceFile().fileName.split(/[\\/]/u).at(-1) ?? ""));
|
|
821
|
+
},
|
|
822
|
+
newTypeWalk() {
|
|
823
|
+
return { remaining: maxAliasDepth, seen: new Set() };
|
|
824
|
+
},
|
|
825
|
+
consumeTypeWalk(walk) {
|
|
826
|
+
walk.remaining -= 1;
|
|
827
|
+
return walk.remaining >= 0;
|
|
828
|
+
},
|
|
829
|
+
resolveAlias(name, typeNode, walk, resolveType) {
|
|
830
|
+
if (walk.seen.has(name)) {
|
|
831
|
+
return undefined;
|
|
832
|
+
}
|
|
833
|
+
walk.seen.add(name);
|
|
834
|
+
const resolved = resolveType(typeNode);
|
|
835
|
+
walk.seen.delete(name);
|
|
836
|
+
return resolved;
|
|
837
|
+
},
|
|
838
|
+
buildImportState(source, program) {
|
|
839
|
+
const state = {
|
|
840
|
+
generatedNamespaces: new Map(),
|
|
841
|
+
generatedSymbols: new Map(),
|
|
842
|
+
localTypeAliases: new Map(),
|
|
843
|
+
serverNamespaces: new Set(),
|
|
844
|
+
serverSymbols: new Map(),
|
|
845
|
+
protoNamespaces: new Set(),
|
|
846
|
+
protoSymbols: new Set(),
|
|
847
|
+
protoContextSymbols: new Map(),
|
|
848
|
+
};
|
|
849
|
+
for (const statement of source.statements) {
|
|
850
|
+
HandlerSources.recordImportStatement(statement, source, program, state);
|
|
851
|
+
}
|
|
852
|
+
return state;
|
|
853
|
+
},
|
|
854
|
+
recordImportStatement(statement, source, program, state) {
|
|
855
|
+
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
856
|
+
HandlerSources.recordImportDeclaration(statement, statement.moduleSpecifier.text, source, program, state);
|
|
857
|
+
}
|
|
858
|
+
if (ts.isTypeAliasDeclaration(statement)) {
|
|
859
|
+
state.localTypeAliases.set(statement.name.text, statement.type);
|
|
860
|
+
}
|
|
861
|
+
},
|
|
862
|
+
recordImportDeclaration(statement, moduleSpecifier, source, program, state) {
|
|
863
|
+
const bindings = statement.importClause?.namedBindings;
|
|
864
|
+
if (bindings === undefined) {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (moduleSpecifier === "@spine-event-engine/server") {
|
|
868
|
+
HandlerSources.recordServerImport(bindings, state);
|
|
869
|
+
}
|
|
870
|
+
if (moduleSpecifier === "@spine-event-engine/proto") {
|
|
871
|
+
HandlerSources.recordProtoImport(bindings, state);
|
|
872
|
+
}
|
|
873
|
+
if (HandlerSources.isGeneratedModule(moduleSpecifier)) {
|
|
874
|
+
HandlerSources.recordGeneratedImport(bindings, moduleSpecifier, statement.importClause?.phaseModifier !== ts.SyntaxKind.TypeKeyword, source, program, state);
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
recordServerImport(bindings, state) {
|
|
878
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
879
|
+
state.serverNamespaces.add(bindings.name.text);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
for (const element of bindings.elements) {
|
|
883
|
+
state.serverSymbols.set(element.name.text, element.propertyName?.text ?? element.name.text);
|
|
884
|
+
}
|
|
885
|
+
},
|
|
886
|
+
recordProtoImport(bindings, state) {
|
|
887
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
888
|
+
state.protoNamespaces.add(bindings.name.text);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
for (const element of bindings.elements) {
|
|
892
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
893
|
+
if (imported === "Event" || imported === "Command") {
|
|
894
|
+
state.protoSymbols.add(element.name.text);
|
|
895
|
+
}
|
|
896
|
+
if (imported === "CommandContext" || imported === "EventContext") {
|
|
897
|
+
state.protoContextSymbols.set(element.name.text, imported);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
recordGeneratedImport(bindings, moduleSpecifier, valueImport, source, program, state) {
|
|
902
|
+
const exported = HandlerSources.generatedModuleExports(source, moduleSpecifier, program);
|
|
903
|
+
if (exported === undefined) {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
907
|
+
state.generatedNamespaces.set(bindings.name.text, { exports: exported, moduleSpecifier });
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
for (const element of bindings.elements) {
|
|
911
|
+
const exportName = element.propertyName?.text ?? element.name.text;
|
|
912
|
+
if (!HandlerTypes.hasGeneratedType(exported, exportName)) {
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
const schemaExportName = `${exportName}Schema`;
|
|
916
|
+
state.generatedSymbols.set(element.name.text, {
|
|
917
|
+
exports: exported,
|
|
918
|
+
moduleSpecifier,
|
|
919
|
+
exportName,
|
|
920
|
+
kind: exported.schemaRoles.get(schemaExportName),
|
|
921
|
+
schemaExportName: exported.values.has(schemaExportName) ? schemaExportName : undefined,
|
|
922
|
+
schemaValue: valueImport && !element.isTypeOnly && exported.values.has(exportName),
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
},
|
|
926
|
+
isGeneratedModule(moduleSpecifier) {
|
|
927
|
+
return /(^|\/)generated\/.+_pb(\.js)?$/.test(moduleSpecifier);
|
|
928
|
+
},
|
|
929
|
+
generatedModuleExports(source, moduleSpecifier, program) {
|
|
930
|
+
const module = HandlerSources.generatedModuleSource(source, moduleSpecifier, program);
|
|
931
|
+
if (module === undefined)
|
|
932
|
+
return undefined;
|
|
933
|
+
const declarations = HandlerSources.exportedNames(module);
|
|
934
|
+
const runtime = HandlerSources.pairedRuntimeExports(module);
|
|
935
|
+
if (runtime === undefined)
|
|
936
|
+
return declarations;
|
|
937
|
+
return {
|
|
938
|
+
types: declarations.types,
|
|
939
|
+
values: runtime.values,
|
|
940
|
+
schemaRoles: runtime.schemaRoles,
|
|
941
|
+
};
|
|
942
|
+
},
|
|
943
|
+
pairedRuntimeExports(declarations) {
|
|
944
|
+
if (!declarations.isDeclarationFile)
|
|
945
|
+
return undefined;
|
|
946
|
+
const runtimePath = declarations.fileName.replace(/\.d\.ts$/u, ".js");
|
|
947
|
+
try {
|
|
948
|
+
return HandlerSources.exportedNames(ts.createSourceFile(runtimePath, readFileSync(runtimePath, "utf8"), ts.ScriptTarget.Latest));
|
|
949
|
+
}
|
|
950
|
+
catch {
|
|
951
|
+
return undefined;
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
generatedModuleSource(source, moduleSpecifier, program) {
|
|
955
|
+
const imported = HandlerSources.importedModuleSource(source, moduleSpecifier, program);
|
|
956
|
+
if (imported !== undefined) {
|
|
957
|
+
return imported;
|
|
958
|
+
}
|
|
959
|
+
const resolved = ts.resolveModuleName(moduleSpecifier, source.fileName, program.getCompilerOptions(), ts.sys).resolvedModule?.resolvedFileName;
|
|
960
|
+
const base = resolve(dirname(source.fileName), moduleSpecifier);
|
|
961
|
+
const candidates = HandlerSources.uniqueStrings([
|
|
962
|
+
...(resolved === undefined ? [] : [resolved]),
|
|
963
|
+
base.replace(/\.js$/, ".ts"),
|
|
964
|
+
`${base}.ts`,
|
|
965
|
+
base,
|
|
966
|
+
base.replace(/\.js$/, ".d.ts"),
|
|
967
|
+
`${base}.d.ts`,
|
|
968
|
+
]);
|
|
969
|
+
const sourceFiles = new Map(program.getSourceFiles().map((candidate) => [resolve(candidate.fileName), candidate]));
|
|
970
|
+
for (const candidate of candidates) {
|
|
971
|
+
const sourceFile = sourceFiles.get(candidate);
|
|
972
|
+
if (sourceFile !== undefined) {
|
|
973
|
+
return sourceFile;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return undefined;
|
|
977
|
+
},
|
|
978
|
+
importedModuleSource(source, moduleSpecifier, program) {
|
|
979
|
+
const declaration = source.statements.find((statement) => ts.isImportDeclaration(statement) &&
|
|
980
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
981
|
+
statement.moduleSpecifier.text === moduleSpecifier);
|
|
982
|
+
if (declaration === undefined)
|
|
983
|
+
return undefined;
|
|
984
|
+
const symbol = program.getTypeChecker().getSymbolAtLocation(declaration.moduleSpecifier);
|
|
985
|
+
const imported = symbol?.declarations
|
|
986
|
+
?.map((candidate) => candidate.getSourceFile())
|
|
987
|
+
.find((candidate) => candidate !== source);
|
|
988
|
+
return imported;
|
|
989
|
+
},
|
|
990
|
+
uniqueStrings(values) {
|
|
991
|
+
return [...new Set(values)];
|
|
992
|
+
},
|
|
993
|
+
exportedNames(source) {
|
|
994
|
+
const files = HandlerSources.generatedFiles(source);
|
|
995
|
+
const schemaRoles = new Map();
|
|
996
|
+
const exports = { types: new Set(), values: new Set(), schemaRoles };
|
|
997
|
+
for (const statement of source.statements) {
|
|
998
|
+
HandlerSources.recordExportedNames(statement, exports, files);
|
|
999
|
+
}
|
|
1000
|
+
return exports;
|
|
1001
|
+
},
|
|
1002
|
+
generatedFiles(source) {
|
|
1003
|
+
const files = new Map();
|
|
1004
|
+
for (const statement of source.statements) {
|
|
1005
|
+
if (!ts.isVariableStatement(statement)) {
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1009
|
+
if (!ts.isIdentifier(declaration.name)) {
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
const descriptor = HandlerSources.fileDescriptor(declaration.initializer);
|
|
1013
|
+
if (descriptor !== undefined) {
|
|
1014
|
+
files.set(declaration.name.text, descriptor);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return files;
|
|
1019
|
+
},
|
|
1020
|
+
recordExportedNames(statement, exports, files) {
|
|
1021
|
+
if (ts.isVariableStatement(statement) &&
|
|
1022
|
+
HandlerTypes.hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
|
|
1023
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1024
|
+
if (ts.isIdentifier(declaration.name)) {
|
|
1025
|
+
exports.values.add(declaration.name.text);
|
|
1026
|
+
const schemaRole = HandlerSources.schemaRoleFromInitializer(declaration.name.text, declaration.initializer, files);
|
|
1027
|
+
if (schemaRole.found) {
|
|
1028
|
+
exports.schemaRoles.set(declaration.name.text, schemaRole.kind);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (HandlerTypes.hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
|
|
1034
|
+
HandlerTypes.recordNamedExport(statement, exports);
|
|
1035
|
+
}
|
|
1036
|
+
},
|
|
1037
|
+
schemaRoleFromInitializer(schemaExportName, initializer, files) {
|
|
1038
|
+
const call = HandlerSources.callExpression(initializer, "messageDesc");
|
|
1039
|
+
if (call === undefined) {
|
|
1040
|
+
return { found: false, kind: undefined };
|
|
1041
|
+
}
|
|
1042
|
+
const fileName = call.arguments[0];
|
|
1043
|
+
if (fileName === undefined || !ts.isIdentifier(fileName)) {
|
|
1044
|
+
return { found: false, kind: undefined };
|
|
1045
|
+
}
|
|
1046
|
+
const file = files.get(fileName.text);
|
|
1047
|
+
if (file === undefined) {
|
|
1048
|
+
return { found: true, kind: undefined };
|
|
1049
|
+
}
|
|
1050
|
+
const indexes = HandlerSources.messageDescIndexes(call);
|
|
1051
|
+
const message = HandlerSources.descriptorMessageAt(file, indexes);
|
|
1052
|
+
const messageName = message?.exportName;
|
|
1053
|
+
const expectedName = schemaExportName.replace(/Schema$/, "");
|
|
1054
|
+
if (messageName === undefined || messageName !== expectedName) {
|
|
1055
|
+
return { found: true, kind: undefined };
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
found: true,
|
|
1059
|
+
kind: message?.descriptor.isEntityState
|
|
1060
|
+
? "state"
|
|
1061
|
+
: HandlerSources.signalKindFromProto(file.sourceFile, indexes),
|
|
1062
|
+
};
|
|
1063
|
+
},
|
|
1064
|
+
messageDescIndexes(call) {
|
|
1065
|
+
const indexes = [];
|
|
1066
|
+
for (const argument of call.arguments.slice(1)) {
|
|
1067
|
+
if (!ts.isNumericLiteral(argument)) {
|
|
1068
|
+
return undefined;
|
|
1069
|
+
}
|
|
1070
|
+
const index = Number(argument.text);
|
|
1071
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
1072
|
+
return undefined;
|
|
1073
|
+
}
|
|
1074
|
+
indexes.push(index);
|
|
1075
|
+
}
|
|
1076
|
+
return indexes.length === 0 ? undefined : indexes;
|
|
1077
|
+
},
|
|
1078
|
+
descriptorMessageAt(file, indexes) {
|
|
1079
|
+
if (indexes === undefined) {
|
|
1080
|
+
return undefined;
|
|
1081
|
+
}
|
|
1082
|
+
let messages = file.messages;
|
|
1083
|
+
let message;
|
|
1084
|
+
const path = [];
|
|
1085
|
+
for (const index of indexes) {
|
|
1086
|
+
message = messages[index];
|
|
1087
|
+
if (message === undefined) {
|
|
1088
|
+
return undefined;
|
|
1089
|
+
}
|
|
1090
|
+
path.push(message.name);
|
|
1091
|
+
messages = message.nested;
|
|
1092
|
+
}
|
|
1093
|
+
return message === undefined ? undefined : { descriptor: message, exportName: path.join("_") };
|
|
1094
|
+
},
|
|
1095
|
+
fileDescriptor(initializer) {
|
|
1096
|
+
const call = HandlerSources.callExpression(initializer, "fileDesc");
|
|
1097
|
+
const descriptor = call?.arguments[0];
|
|
1098
|
+
if (descriptor === undefined || !ts.isStringLiteralLike(descriptor)) {
|
|
1099
|
+
return undefined;
|
|
1100
|
+
}
|
|
1101
|
+
try {
|
|
1102
|
+
const { protobuf, protobufWkt } = PackageDependencies.current();
|
|
1103
|
+
const file = protobuf.fromBinary(protobufWkt.FileDescriptorProtoSchema, Buffer.from(descriptor.text, "base64"));
|
|
1104
|
+
return file.name === ""
|
|
1105
|
+
? undefined
|
|
1106
|
+
: {
|
|
1107
|
+
sourceFile: file.name,
|
|
1108
|
+
messages: file.messageType.map(HandlerSources.descriptorMessage),
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
catch {
|
|
1112
|
+
return undefined;
|
|
1113
|
+
}
|
|
1114
|
+
},
|
|
1115
|
+
callExpression(expression, functionName) {
|
|
1116
|
+
if (expression === undefined) {
|
|
1117
|
+
return undefined;
|
|
1118
|
+
}
|
|
1119
|
+
const unwrapped = HandlerSources.unwrapExpression(expression);
|
|
1120
|
+
if (!ts.isCallExpression(unwrapped) || !ts.isIdentifier(unwrapped.expression)) {
|
|
1121
|
+
return undefined;
|
|
1122
|
+
}
|
|
1123
|
+
return unwrapped.expression.text === functionName ? unwrapped : undefined;
|
|
1124
|
+
},
|
|
1125
|
+
unwrapExpression(expression) {
|
|
1126
|
+
if (ts.isParenthesizedExpression(expression)) {
|
|
1127
|
+
return HandlerSources.unwrapExpression(expression.expression);
|
|
1128
|
+
}
|
|
1129
|
+
if (ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression)) {
|
|
1130
|
+
return HandlerSources.unwrapExpression(expression.expression);
|
|
1131
|
+
}
|
|
1132
|
+
return expression;
|
|
1133
|
+
},
|
|
1134
|
+
signalKindFromProto(sourceFile, messageIndexes) {
|
|
1135
|
+
const sourceName = sourceFile.split(/[\\/]/u).at(-1);
|
|
1136
|
+
if (sourceName === "rejections.proto" || sourceName?.endsWith("_rejections.proto") === true) {
|
|
1137
|
+
return messageIndexes?.length === 1 ? "rejection" : undefined;
|
|
1138
|
+
}
|
|
1139
|
+
if (sourceName === "commands.proto" || sourceName?.endsWith("_commands.proto") === true) {
|
|
1140
|
+
return "command";
|
|
1141
|
+
}
|
|
1142
|
+
if (sourceName === "events.proto" || sourceName?.endsWith("_events.proto") === true) {
|
|
1143
|
+
return "event";
|
|
1144
|
+
}
|
|
1145
|
+
return undefined;
|
|
1146
|
+
},
|
|
1147
|
+
descriptorMessage(message) {
|
|
1148
|
+
return {
|
|
1149
|
+
name: message.name,
|
|
1150
|
+
isEntityState: message.options?.$unknown?.some((field) => field.no === entityOptionFieldNumber) === true,
|
|
1151
|
+
nested: message.nestedType.map(HandlerSources.descriptorMessage),
|
|
1152
|
+
};
|
|
1153
|
+
},
|
|
1154
|
+
});
|
|
1155
|
+
/**
|
|
1156
|
+
* Finds the package identity that owns an analyzed source file.
|
|
1157
|
+
*/
|
|
1158
|
+
export const PackageIdentity = Object.freeze({
|
|
1159
|
+
nameFor(sourceFile) {
|
|
1160
|
+
let directory = resolve(dirname(sourceFile));
|
|
1161
|
+
let manifest = join(directory, "package.json");
|
|
1162
|
+
while (!existsSync(manifest)) {
|
|
1163
|
+
const parent = dirname(directory);
|
|
1164
|
+
if (parent === directory)
|
|
1165
|
+
return undefined;
|
|
1166
|
+
directory = parent;
|
|
1167
|
+
manifest = join(directory, "package.json");
|
|
1168
|
+
}
|
|
1169
|
+
try {
|
|
1170
|
+
const { name } = JSON.parse(readFileSync(manifest, "utf8"));
|
|
1171
|
+
return typeof name === "string" ? name : undefined;
|
|
1172
|
+
}
|
|
1173
|
+
catch {
|
|
1174
|
+
return undefined;
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
});
|
|
1178
|
+
const PackageDependencies = Object.freeze({
|
|
1179
|
+
load(program) {
|
|
1180
|
+
packageDependencies ??= {
|
|
1181
|
+
protobuf: PackageDependencies.require("@bufbuild/protobuf", program),
|
|
1182
|
+
protobufWkt: PackageDependencies.require("@bufbuild/protobuf/wkt", program),
|
|
1183
|
+
};
|
|
1184
|
+
},
|
|
1185
|
+
current() {
|
|
1186
|
+
if (packageDependencies === undefined)
|
|
1187
|
+
throw new Error("Build handler analyzer dependencies are not loaded.");
|
|
1188
|
+
return packageDependencies;
|
|
1189
|
+
},
|
|
1190
|
+
require(specifier, program) {
|
|
1191
|
+
try {
|
|
1192
|
+
return createRequire(import.meta.url)(specifier);
|
|
1193
|
+
}
|
|
1194
|
+
catch (directError) {
|
|
1195
|
+
const appRoot = program.getRootFileNames()[0];
|
|
1196
|
+
if (appRoot === undefined)
|
|
1197
|
+
throw directError;
|
|
1198
|
+
const applicationRequire = createRequire(appRoot);
|
|
1199
|
+
try {
|
|
1200
|
+
return applicationRequire(specifier);
|
|
1201
|
+
}
|
|
1202
|
+
catch {
|
|
1203
|
+
// The in-repository generator transpiles this module into a temporary
|
|
1204
|
+
// cache directory. A fresh workspace install has the descriptor
|
|
1205
|
+
// package at the application's resolution boundary, while the
|
|
1206
|
+
// temporary module has no package boundary of its own.
|
|
1207
|
+
}
|
|
1208
|
+
try {
|
|
1209
|
+
const toolingEntry = applicationRequire.resolve("@spine-event-engine/proto-tools");
|
|
1210
|
+
return createRequire(join(PackageDependencies.rootFor(toolingEntry), "package.json"))(specifier);
|
|
1211
|
+
}
|
|
1212
|
+
catch {
|
|
1213
|
+
throw directError;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
},
|
|
1217
|
+
rootFor(entry) {
|
|
1218
|
+
let directory = dirname(entry);
|
|
1219
|
+
while (!existsSync(join(directory, "package.json"))) {
|
|
1220
|
+
const parent = dirname(directory);
|
|
1221
|
+
if (parent === directory)
|
|
1222
|
+
throw new Error("Cannot locate proto-tools package manifest.");
|
|
1223
|
+
directory = parent;
|
|
1224
|
+
}
|
|
1225
|
+
return directory;
|
|
1226
|
+
},
|
|
1227
|
+
});
|
|
1228
|
+
const HandlerTypes = Object.freeze({
|
|
1229
|
+
recordNamedExport(statement, exports) {
|
|
1230
|
+
if (ts.isClassDeclaration(statement) || ts.isEnumDeclaration(statement)) {
|
|
1231
|
+
const name = statement.name;
|
|
1232
|
+
if (name !== undefined) {
|
|
1233
|
+
exports.types.add(name.text);
|
|
1234
|
+
exports.values.add(name.text);
|
|
1235
|
+
}
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (ts.isFunctionDeclaration(statement)) {
|
|
1239
|
+
const name = statement.name;
|
|
1240
|
+
if (name !== undefined) {
|
|
1241
|
+
exports.values.add(name.text);
|
|
1242
|
+
}
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
if (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) {
|
|
1246
|
+
exports.types.add(statement.name.text);
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1249
|
+
hasGeneratedType(exports, name) {
|
|
1250
|
+
return exports.types.has(name) || exports.values.has(name);
|
|
1251
|
+
},
|
|
1252
|
+
serverDecorator(name) {
|
|
1253
|
+
return name === "Assign" ||
|
|
1254
|
+
name === "Command" ||
|
|
1255
|
+
name === "React" ||
|
|
1256
|
+
name === "Subscribe" ||
|
|
1257
|
+
name === "Apply" ||
|
|
1258
|
+
name === "Where"
|
|
1259
|
+
? name
|
|
1260
|
+
: undefined;
|
|
1261
|
+
},
|
|
1262
|
+
methodName(node) {
|
|
1263
|
+
if (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) {
|
|
1264
|
+
return node.name.text;
|
|
1265
|
+
}
|
|
1266
|
+
return undefined;
|
|
1267
|
+
},
|
|
1268
|
+
hasModifier(node, kind) {
|
|
1269
|
+
return (ts.canHaveModifiers(node) &&
|
|
1270
|
+
(ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === kind));
|
|
1271
|
+
},
|
|
1272
|
+
expressionName(expression) {
|
|
1273
|
+
if (ts.isIdentifier(expression)) {
|
|
1274
|
+
return expression.text;
|
|
1275
|
+
}
|
|
1276
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
1277
|
+
const prefix = HandlerTypes.expressionName(expression.expression);
|
|
1278
|
+
return prefix === undefined ? undefined : `${prefix}.${expression.name.text}`;
|
|
1279
|
+
}
|
|
1280
|
+
return undefined;
|
|
1281
|
+
},
|
|
1282
|
+
pushDiagnostic(scope, code, node, message, className, methodNameValue) {
|
|
1283
|
+
const location = scope.source.getLineAndCharacterOfPosition(node.getStart(scope.source));
|
|
1284
|
+
const base = {
|
|
1285
|
+
code,
|
|
1286
|
+
sourceFile: scope.source.fileName,
|
|
1287
|
+
line: location.line + 1,
|
|
1288
|
+
column: location.character + 1,
|
|
1289
|
+
message,
|
|
1290
|
+
};
|
|
1291
|
+
scope.diagnostics.push({
|
|
1292
|
+
...base,
|
|
1293
|
+
...(className === undefined ? {} : { className }),
|
|
1294
|
+
...(methodNameValue === undefined ? {} : { methodName: methodNameValue }),
|
|
1295
|
+
});
|
|
1296
|
+
},
|
|
1297
|
+
syntaxDiagnostic(source, diagnostic) {
|
|
1298
|
+
const location = source.getLineAndCharacterOfPosition(diagnostic.start);
|
|
1299
|
+
return {
|
|
1300
|
+
code: "TYPESCRIPT_SYNTAX_ERROR",
|
|
1301
|
+
sourceFile: source.fileName,
|
|
1302
|
+
line: location.line + 1,
|
|
1303
|
+
column: location.character + 1,
|
|
1304
|
+
message: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
|
|
1305
|
+
};
|
|
1306
|
+
},
|
|
1307
|
+
});
|
|
1308
|
+
//# sourceMappingURL=build-time-handler-analyzer.js.map
|