@webda/content-mapper 0.0.0-stage → 4.0.0-beta.4
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 +166 -0
- package/lib/coercions.d.ts +22 -0
- package/lib/coercions.d.ts.map +1 -0
- package/lib/coercions.js +10 -0
- package/lib/coercions.js.map +1 -0
- package/lib/context.d.ts +56 -0
- package/lib/context.d.ts.map +1 -0
- package/lib/context.js +111 -0
- package/lib/context.js.map +1 -0
- package/lib/defaults.d.ts +31 -0
- package/lib/defaults.d.ts.map +1 -0
- package/lib/defaults.js +33 -0
- package/lib/defaults.js.map +1 -0
- package/lib/generators/accessors.d.ts +15 -0
- package/lib/generators/accessors.d.ts.map +1 -0
- package/lib/generators/accessors.js +567 -0
- package/lib/generators/accessors.js.map +1 -0
- package/lib/generators/behaviors.d.ts +21 -0
- package/lib/generators/behaviors.d.ts.map +1 -0
- package/lib/generators/behaviors.js +292 -0
- package/lib/generators/behaviors.js.map +1 -0
- package/lib/generators/imports.d.ts +86 -0
- package/lib/generators/imports.d.ts.map +1 -0
- package/lib/generators/imports.js +205 -0
- package/lib/generators/imports.js.map +1 -0
- package/lib/generators/loadparameters.d.ts +7 -0
- package/lib/generators/loadparameters.d.ts.map +1 -0
- package/lib/generators/loadparameters.js +175 -0
- package/lib/generators/loadparameters.js.map +1 -0
- package/lib/generators/qlvalidator.d.ts +53 -0
- package/lib/generators/qlvalidator.d.ts.map +1 -0
- package/lib/generators/qlvalidator.js +238 -0
- package/lib/generators/qlvalidator.js.map +1 -0
- package/lib/index.d.ts +53 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +36 -0
- package/lib/index.js.map +1 -0
- package/lib/module-discovery.d.ts +211 -0
- package/lib/module-discovery.d.ts.map +1 -0
- package/lib/module-discovery.js +662 -0
- package/lib/module-discovery.js.map +1 -0
- package/lib/module.d.ts +213 -0
- package/lib/module.d.ts.map +1 -0
- package/lib/module.js +1243 -0
- package/lib/module.js.map +1 -0
- package/lib/plan.d.ts +101 -0
- package/lib/plan.d.ts.map +1 -0
- package/lib/plan.js +61 -0
- package/lib/plan.js.map +1 -0
- package/lib/property-order.d.ts +37 -0
- package/lib/property-order.d.ts.map +1 -0
- package/lib/property-order.js +141 -0
- package/lib/property-order.js.map +1 -0
- package/lib/schema/action.d.ts +46 -0
- package/lib/schema/action.d.ts.map +1 -0
- package/lib/schema/action.js +190 -0
- package/lib/schema/action.js.map +1 -0
- package/lib/schema/annotations.d.ts +63 -0
- package/lib/schema/annotations.d.ts.map +1 -0
- package/lib/schema/annotations.js +116 -0
- package/lib/schema/annotations.js.map +1 -0
- package/lib/schema/converter.d.ts +363 -0
- package/lib/schema/converter.d.ts.map +1 -0
- package/lib/schema/converter.js +1377 -0
- package/lib/schema/converter.js.map +1 -0
- package/lib/schema/jsdoc.d.ts +34 -0
- package/lib/schema/jsdoc.d.ts.map +1 -0
- package/lib/schema/jsdoc.js +206 -0
- package/lib/schema/jsdoc.js.map +1 -0
- package/lib/schema/model.d.ts +27 -0
- package/lib/schema/model.d.ts.map +1 -0
- package/lib/schema/model.js +86 -0
- package/lib/schema/model.js.map +1 -0
- package/lib/schema/project.d.ts +34 -0
- package/lib/schema/project.d.ts.map +1 -0
- package/lib/schema/project.js +305 -0
- package/lib/schema/project.js.map +1 -0
- package/lib/schema/service.d.ts +62 -0
- package/lib/schema/service.d.ts.map +1 -0
- package/lib/schema/service.js +130 -0
- package/lib/schema/service.js.map +1 -0
- package/lib/schema/types.d.ts +61 -0
- package/lib/schema/types.d.ts.map +1 -0
- package/lib/schema/types.js +36 -0
- package/lib/schema/types.js.map +1 -0
- package/lib/schema/worker-cli.d.ts +3 -0
- package/lib/schema/worker-cli.d.ts.map +1 -0
- package/lib/schema/worker-cli.js +7 -0
- package/lib/schema/worker-cli.js.map +1 -0
- package/lib/schema/worker.d.ts +81 -0
- package/lib/schema/worker.d.ts.map +1 -0
- package/lib/schema/worker.js +186 -0
- package/lib/schema/worker.js.map +1 -0
- package/lib/server.d.ts +2 -0
- package/lib/server.d.ts.map +1 -0
- package/lib/server.js +133 -0
- package/lib/server.js.map +1 -0
- package/lib/session.d.ts +84 -0
- package/lib/session.d.ts.map +1 -0
- package/lib/session.js +141 -0
- package/lib/session.js.map +1 -0
- package/lib/spans.d.ts +46 -0
- package/lib/spans.d.ts.map +1 -0
- package/lib/spans.js +64 -0
- package/lib/spans.js.map +1 -0
- package/lib/twopass.d.ts +45 -0
- package/lib/twopass.d.ts.map +1 -0
- package/lib/twopass.js +87 -0
- package/lib/twopass.js.map +1 -0
- package/lib/vite.d.ts +20 -0
- package/lib/vite.d.ts.map +1 -0
- package/lib/vite.js +41 -0
- package/lib/vite.js.map +1 -0
- package/package.json +89 -3
- package/README.md +0 -3
package/lib/module.js
ADDED
|
@@ -0,0 +1,1243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The whole `webda.module.json`, generated on the TypeScript 7.1 API.
|
|
3
|
+
*
|
|
4
|
+
* A port of `@webda/compiler`'s `ModuleGenerator.generate()` and its metadata
|
|
5
|
+
* plugins (`metadata/*.ts`). The target is byte identity with the artefact the
|
|
6
|
+
* TypeScript 6 generator writes, so key order is reproduced as carefully as
|
|
7
|
+
* the values: `beans`, `deployers`, `moddas` and `models` are key-sorted with
|
|
8
|
+
* the default code-unit sort (`JSONUtils.sortObject`), every other map keeps
|
|
9
|
+
* insertion order, and insertion order follows the TypeScript 6 program walk.
|
|
10
|
+
*
|
|
11
|
+
* Discovery, reflection and the schemas are not reimplemented here — they
|
|
12
|
+
* come from `module-discovery.ts` and `schema/*.ts`. What this file adds is
|
|
13
|
+
* the remaining metadata (Relations, Ancestors, Subclasses, Actions, Events,
|
|
14
|
+
* PrimaryKey, Plural, behaviours, commands, capabilities, Configuration) and
|
|
15
|
+
* the assembly.
|
|
16
|
+
*
|
|
17
|
+
* The module holds no source digest: the compiler keeps it in `.webda/cache`
|
|
18
|
+
* so the committed file only changes when its content does.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
21
|
+
import { join, relative } from "node:path";
|
|
22
|
+
import { getJSDocTags, getTextOfJSDocComment, SyntaxKind } from "typescript/unstable/ast";
|
|
23
|
+
import * as is from "typescript/unstable/ast/is";
|
|
24
|
+
import { SymbolFlags } from "typescript/unstable/sync";
|
|
25
|
+
import { classTree, dependencyModelName, discoverWebdaObjects, exportedName, outputTarget, packageOf, reflectAttributes } from "./module-discovery.js";
|
|
26
|
+
import { classPropertiesInTs6Order, ts6PropertyOrder } from "./property-order.js";
|
|
27
|
+
import { generateModelSchemas } from "./schema/model.js";
|
|
28
|
+
import { generateTopLevelSchemas } from "./schema/project.js";
|
|
29
|
+
import { findParametersNode, generateServiceSchema } from "./schema/service.js";
|
|
30
|
+
/** Value of `$schema` in every generated module. */
|
|
31
|
+
export const WEBDA_MODULE_SCHEMA = "https://webda.io/schemas/webda.module.v4.json";
|
|
32
|
+
/** Suffixes the content mapper is registered for. */
|
|
33
|
+
const MAPPED_SUFFIXES = [".model.ts", ".service.ts"];
|
|
34
|
+
/** Suffix suggested for a section when a file has none of {@link MAPPED_SUFFIXES}. */
|
|
35
|
+
const SECTION_SUFFIX = {
|
|
36
|
+
models: ".model.ts",
|
|
37
|
+
moddas: ".service.ts",
|
|
38
|
+
beans: ".service.ts",
|
|
39
|
+
deployers: ".service.ts"
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Plural of an English noun, as `@webda/compiler`'s `getPlural`.
|
|
43
|
+
*
|
|
44
|
+
* https://www.teflcourse.net/english-grammar-corner/changing-nouns-from-singular-to-plural/
|
|
45
|
+
* @param name - the singular noun
|
|
46
|
+
* @returns the pluralized form
|
|
47
|
+
*/
|
|
48
|
+
export function getPlural(name) {
|
|
49
|
+
const isVowel = (char) => "aeiouy".includes(char.toLowerCase());
|
|
50
|
+
if (name.endsWith("y")) {
|
|
51
|
+
if (isVowel(name.charAt(name.length - 2)))
|
|
52
|
+
return name + "s";
|
|
53
|
+
return name.substring(0, name.length - 1) + "ies";
|
|
54
|
+
}
|
|
55
|
+
if (name.endsWith("fe"))
|
|
56
|
+
return name.substring(0, name.length - 2) + "ves";
|
|
57
|
+
if (name.endsWith("f"))
|
|
58
|
+
return name.substring(0, name.length - 1) + "ves";
|
|
59
|
+
if (name.endsWith("on"))
|
|
60
|
+
return name.substring(0, name.length - 2) + "a";
|
|
61
|
+
if (name.endsWith("is"))
|
|
62
|
+
return name.substring(0, name.length - 2) + "es";
|
|
63
|
+
if (name.endsWith("man"))
|
|
64
|
+
return name.substring(0, name.length - 3) + "men";
|
|
65
|
+
if (name.endsWith("s") ||
|
|
66
|
+
name.endsWith("ss") ||
|
|
67
|
+
name.endsWith("sh") ||
|
|
68
|
+
name.endsWith("ch") ||
|
|
69
|
+
name.endsWith("x") ||
|
|
70
|
+
name.endsWith("z") ||
|
|
71
|
+
name.endsWith("o")) {
|
|
72
|
+
return name + "es";
|
|
73
|
+
}
|
|
74
|
+
return name + "s";
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* `JSONUtils.sortObject`: keys in default (code unit) order, entries the
|
|
78
|
+
* transformer maps to a falsy value dropped.
|
|
79
|
+
* @param unordered - the map
|
|
80
|
+
* @param transformer - value mapper
|
|
81
|
+
* @returns the sorted map
|
|
82
|
+
*/
|
|
83
|
+
export function sortObject(unordered, transformer = v => v) {
|
|
84
|
+
const sorted = {};
|
|
85
|
+
for (const key of Object.keys(unordered).sort()) {
|
|
86
|
+
const value = transformer(unordered[key]);
|
|
87
|
+
if (!value)
|
|
88
|
+
continue;
|
|
89
|
+
sorted[key] = value;
|
|
90
|
+
}
|
|
91
|
+
return sorted;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Text of a node, as `Node.getText()`.
|
|
95
|
+
* @param ctx - analysis context
|
|
96
|
+
* @param node - the node
|
|
97
|
+
* @returns the source text
|
|
98
|
+
*/
|
|
99
|
+
function text(ctx, node) {
|
|
100
|
+
return ctx.textOf(sourceFileOf(node), node);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The source file a node belongs to.
|
|
104
|
+
* @param node - the node
|
|
105
|
+
* @returns its file
|
|
106
|
+
*/
|
|
107
|
+
function sourceFileOf(node) {
|
|
108
|
+
const direct = node.getSourceFile?.() ?? node._sourceFile;
|
|
109
|
+
if (direct)
|
|
110
|
+
return direct;
|
|
111
|
+
let current = node;
|
|
112
|
+
while (current && current.kind !== SyntaxKind.SourceFile)
|
|
113
|
+
current = current.parent;
|
|
114
|
+
return current;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Decorators on a node, from its `modifiers`.
|
|
118
|
+
* @param node - a class element or class
|
|
119
|
+
* @returns the decorator nodes
|
|
120
|
+
*/
|
|
121
|
+
function decoratorsOf(node) {
|
|
122
|
+
return (node.modifiers ?? []).filter((modifier) => modifier.kind === SyntaxKind.Decorator);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Decorator name, for both `@Name` and `@Name()`, as `getDecoratorName`.
|
|
126
|
+
* @param ctx - analysis context
|
|
127
|
+
* @param decorator - the decorator
|
|
128
|
+
* @returns the name, when the expression is an identifier or a call on one
|
|
129
|
+
*/
|
|
130
|
+
function decoratorName(ctx, decorator) {
|
|
131
|
+
const expression = decorator.expression;
|
|
132
|
+
if (is.isCallExpression(expression) && is.isIdentifier(expression.expression)) {
|
|
133
|
+
return text(ctx, expression.expression);
|
|
134
|
+
}
|
|
135
|
+
if (is.isIdentifier(expression))
|
|
136
|
+
return text(ctx, expression);
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Whether a method carries `@Action` or `@Operation`.
|
|
141
|
+
* @param ctx - analysis context
|
|
142
|
+
* @param method - the method
|
|
143
|
+
* @returns true when decorated
|
|
144
|
+
*/
|
|
145
|
+
function hasOperationDecorator(ctx, method) {
|
|
146
|
+
return decoratorsOf(method).some(decorator => {
|
|
147
|
+
const name = decoratorName(ctx, decorator);
|
|
148
|
+
return name === "Action" || name === "Operation";
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Whether a node carries the `static` keyword.
|
|
153
|
+
* @param node - the node
|
|
154
|
+
* @returns true when static
|
|
155
|
+
*/
|
|
156
|
+
function isStaticMember(node) {
|
|
157
|
+
return (node.modifiers ?? []).some((modifier) => modifier.kind === SyntaxKind.StaticKeyword);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The first `@tagName` JSDoc tag on a node.
|
|
161
|
+
* @param node - the declaration
|
|
162
|
+
* @param tagName - tag to find
|
|
163
|
+
* @returns the tag
|
|
164
|
+
*/
|
|
165
|
+
function findTag(node, tagName) {
|
|
166
|
+
try {
|
|
167
|
+
return getJSDocTags(node).find(tag => tag.tagName.text === tagName);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* `getTagsName`: every `Webda*` tag, valued by the first token of its payload
|
|
175
|
+
* or the class name.
|
|
176
|
+
* @param node - the declaration
|
|
177
|
+
* @returns tag name to value
|
|
178
|
+
*/
|
|
179
|
+
function webdaTags(node) {
|
|
180
|
+
const tags = {};
|
|
181
|
+
let found = [];
|
|
182
|
+
try {
|
|
183
|
+
found = getJSDocTags(node);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
found = [];
|
|
187
|
+
}
|
|
188
|
+
for (const tag of found) {
|
|
189
|
+
const name = tag.tagName.text;
|
|
190
|
+
if (name.startsWith("Webda")) {
|
|
191
|
+
tags[name] =
|
|
192
|
+
(getTextOfJSDocComment(tag.comment) ?? "").trim().replace("\n", " ").split(" ").shift() ||
|
|
193
|
+
node.name?.text;
|
|
194
|
+
}
|
|
195
|
+
else if (name.startsWith("Schema")) {
|
|
196
|
+
tags[name] = (getTextOfJSDocComment(tag.comment) ?? "").trim();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return tags;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Follow an alias to its target.
|
|
203
|
+
* @param ctx - analysis context
|
|
204
|
+
* @param symbol - the symbol
|
|
205
|
+
* @returns the resolved symbol
|
|
206
|
+
*/
|
|
207
|
+
function resolveAlias(ctx, symbol) {
|
|
208
|
+
if (!symbol)
|
|
209
|
+
return symbol;
|
|
210
|
+
if (!(symbol.flags & SymbolFlags.Alias))
|
|
211
|
+
return symbol;
|
|
212
|
+
try {
|
|
213
|
+
return ctx.checker.getAliasedSymbol(symbol);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return symbol;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Resolved declaration nodes of a symbol.
|
|
221
|
+
* @param ctx - analysis context
|
|
222
|
+
* @param symbol - the symbol
|
|
223
|
+
* @returns the declarations
|
|
224
|
+
*/
|
|
225
|
+
function declarationsOf(ctx, symbol) {
|
|
226
|
+
const nodes = [];
|
|
227
|
+
for (const handle of symbol?.declarations ?? []) {
|
|
228
|
+
const node = handle.resolve(ctx.project);
|
|
229
|
+
if (node)
|
|
230
|
+
nodes.push(node);
|
|
231
|
+
}
|
|
232
|
+
return nodes;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Resolved value declaration of a symbol.
|
|
236
|
+
* @param ctx - analysis context
|
|
237
|
+
* @param symbol - the symbol
|
|
238
|
+
* @returns the declaration
|
|
239
|
+
*/
|
|
240
|
+
function valueDeclarationOf(ctx, symbol) {
|
|
241
|
+
return symbol?.valueDeclaration?.resolve(ctx.project) ?? undefined;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* `propertyIsKeyedBySymbol`: whether a property is declared `[key]` with
|
|
245
|
+
* `key` the named `unique symbol` exported by `packageName`.
|
|
246
|
+
* @param ctx - analysis context
|
|
247
|
+
* @param property - the property symbol
|
|
248
|
+
* @param packageName - package declaring the key
|
|
249
|
+
* @param symbolName - name of the key
|
|
250
|
+
* @returns true on a match
|
|
251
|
+
*/
|
|
252
|
+
function propertyIsKeyedBySymbol(ctx, property, packageName, symbolName) {
|
|
253
|
+
// Cheap pre-filter: symbol-keyed members are named `__@<name>@<id>`.
|
|
254
|
+
if (!property.name.startsWith(`__@${symbolName}@`))
|
|
255
|
+
return false;
|
|
256
|
+
for (const declaration of declarationsOf(ctx, property)) {
|
|
257
|
+
const name = declaration.name;
|
|
258
|
+
if (!name || !is.isComputedPropertyName(name))
|
|
259
|
+
continue;
|
|
260
|
+
const key = resolveAlias(ctx, ctx.checker.getSymbolAtLocation(name.expression));
|
|
261
|
+
if (!key || key.name !== symbolName)
|
|
262
|
+
continue;
|
|
263
|
+
const keyDeclaration = valueDeclarationOf(ctx, key);
|
|
264
|
+
const file = keyDeclaration ? sourceFileOf(keyDeclaration)?.fileName : undefined;
|
|
265
|
+
if (file && packageOf(file) === packageName)
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Declared (instance) type of a class.
|
|
272
|
+
* @param ctx - analysis context
|
|
273
|
+
* @param cls - the class declaration
|
|
274
|
+
* @returns the type
|
|
275
|
+
*/
|
|
276
|
+
function declaredTypeOf(ctx, cls) {
|
|
277
|
+
const symbol = cls.name ? ctx.checker.getSymbolAtLocation(cls.name) : undefined;
|
|
278
|
+
return symbol ? ctx.checker.getDeclaredTypeOfSymbol(symbol) : undefined;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Properties of a class's instance type, inherited ones included.
|
|
282
|
+
* @param ctx - analysis context
|
|
283
|
+
* @param cls - the class declaration
|
|
284
|
+
* @returns the property symbols
|
|
285
|
+
*/
|
|
286
|
+
function propertiesOf(ctx, cls) {
|
|
287
|
+
return classPropertiesInTs6Order(ctx, cls);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Names of the properties of a symbol-keyed member's type — the `Events` of
|
|
291
|
+
* `[WEBDA_EVENTS]` or the declarative `Actions` of `[WEBDA_ACTIONS]`.
|
|
292
|
+
* @param ctx - analysis context
|
|
293
|
+
* @param property - the member
|
|
294
|
+
* @returns property names in declaration order
|
|
295
|
+
*/
|
|
296
|
+
function memberTypeKeys(ctx, property) {
|
|
297
|
+
const location = valueDeclarationOf(ctx, property);
|
|
298
|
+
const type = location
|
|
299
|
+
? ctx.checker.getTypeOfSymbolAtLocation(property, location)
|
|
300
|
+
: ctx.checker.getTypeOfSymbol(property);
|
|
301
|
+
const keys = {};
|
|
302
|
+
for (const member of ts6PropertyOrder(ctx, type, ctx.checker.getPropertiesOfType(type)))
|
|
303
|
+
keys[member.name] = true;
|
|
304
|
+
return Object.keys(keys);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Read the string-literal options of an `@Action`/`@Operation` decorator.
|
|
308
|
+
* @param ctx - analysis context
|
|
309
|
+
* @param method - the method
|
|
310
|
+
* @param onProperty - receives each `key: value` property assignment
|
|
311
|
+
*/
|
|
312
|
+
function forEachOperationOption(ctx, method, onProperty) {
|
|
313
|
+
const decorator = decoratorsOf(method).find(candidate => {
|
|
314
|
+
const name = decoratorName(ctx, candidate);
|
|
315
|
+
return name === "Action" || name === "Operation";
|
|
316
|
+
});
|
|
317
|
+
if (!decorator || !is.isCallExpression(decorator.expression))
|
|
318
|
+
return;
|
|
319
|
+
const argument = decorator.expression.arguments?.[0];
|
|
320
|
+
if (!argument || !is.isObjectLiteralExpression(argument))
|
|
321
|
+
return;
|
|
322
|
+
for (const property of argument.properties) {
|
|
323
|
+
if (!is.isPropertyAssignment(property) || !is.isIdentifier(property.name))
|
|
324
|
+
continue;
|
|
325
|
+
onProperty(property.name.text, property.initializer);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Decorated instance methods, via the type so inherited ones are included.
|
|
330
|
+
* @param ctx - analysis context
|
|
331
|
+
* @param cls - the class
|
|
332
|
+
* @returns the method declarations, in property order
|
|
333
|
+
*/
|
|
334
|
+
function decoratedInstanceMethods(ctx, cls) {
|
|
335
|
+
const methods = [];
|
|
336
|
+
for (const property of propertiesOf(ctx, cls)) {
|
|
337
|
+
const node = valueDeclarationOf(ctx, property);
|
|
338
|
+
if (!node || node.kind !== SyntaxKind.MethodDeclaration)
|
|
339
|
+
continue;
|
|
340
|
+
if (hasOperationDecorator(ctx, node))
|
|
341
|
+
methods.push(node);
|
|
342
|
+
}
|
|
343
|
+
return methods;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Decorated static methods declared on the class itself.
|
|
347
|
+
* @param ctx - analysis context
|
|
348
|
+
* @param cls - the class
|
|
349
|
+
* @returns the method declarations, in member order
|
|
350
|
+
*/
|
|
351
|
+
function decoratedStaticMethods(ctx, cls) {
|
|
352
|
+
return (cls.members ?? []).filter((member) => is.isMethodDeclaration(member) && isStaticMember(member) && hasOperationDecorator(ctx, member));
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* A model's `Actions`, as `ActionsMetadata`.
|
|
356
|
+
* @param ctx - analysis context
|
|
357
|
+
* @param cls - the model class
|
|
358
|
+
* @returns action name to metadata
|
|
359
|
+
*/
|
|
360
|
+
export function buildModelActions(ctx, cls) {
|
|
361
|
+
let actions = {};
|
|
362
|
+
// Source 1: the declarative `[WEBDA_ACTIONS]` member.
|
|
363
|
+
const declared = propertiesOf(ctx, cls).find(p => propertyIsKeyedBySymbol(ctx, p, "@webda/models", "WEBDA_ACTIONS"));
|
|
364
|
+
if (declared) {
|
|
365
|
+
actions = {};
|
|
366
|
+
for (const key of memberTypeKeys(ctx, declared))
|
|
367
|
+
actions[key] = {};
|
|
368
|
+
}
|
|
369
|
+
// Source 2: decorated methods, instance then static.
|
|
370
|
+
const processMethod = (method) => {
|
|
371
|
+
const meta = {};
|
|
372
|
+
if (isStaticMember(method))
|
|
373
|
+
meta.global = true;
|
|
374
|
+
const methodName = text(ctx, method.name);
|
|
375
|
+
let name = methodName;
|
|
376
|
+
forEachOperationOption(ctx, method, (key, initializer) => {
|
|
377
|
+
if ((key === "description" || key === "summary") && is.isStringLiteral(initializer)) {
|
|
378
|
+
meta[key] = initializer.text;
|
|
379
|
+
}
|
|
380
|
+
// `@Action({ name })` exposes the action under another name than the method
|
|
381
|
+
if (key === "name" && is.isStringLiteral(initializer) && initializer.text) {
|
|
382
|
+
name = initializer.text;
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
if (name !== methodName)
|
|
386
|
+
meta.handler = methodName;
|
|
387
|
+
actions[name] = meta;
|
|
388
|
+
};
|
|
389
|
+
decoratedInstanceMethods(ctx, cls).forEach(processMethod);
|
|
390
|
+
decoratedStaticMethods(ctx, cls).forEach(processMethod);
|
|
391
|
+
return actions;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* A model's `Events`, as `EventsMetadata`.
|
|
395
|
+
* @param ctx - analysis context
|
|
396
|
+
* @param cls - the model class
|
|
397
|
+
* @returns event names, or undefined when the model declares no events member
|
|
398
|
+
*/
|
|
399
|
+
export function buildModelEvents(ctx, cls) {
|
|
400
|
+
const events = propertiesOf(ctx, cls).find(p => propertyIsKeyedBySymbol(ctx, p, "@webda/models", "WEBDA_EVENTS"));
|
|
401
|
+
return events ? memberTypeKeys(ctx, events) : undefined;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* A model's `PrimaryKey`, as `PrimaryKeyMetadata`.
|
|
405
|
+
*
|
|
406
|
+
* Two forms: a `readonly ["a", "b"]` tuple annotation, and an
|
|
407
|
+
* `= ["a", "b"] as const` initialiser. A `readonly X[]` annotation — the
|
|
408
|
+
* abstract `keyof this` form — records nothing, even when an initialiser
|
|
409
|
+
* follows it.
|
|
410
|
+
* @param ctx - analysis context
|
|
411
|
+
* @param cls - the model class
|
|
412
|
+
* @returns key attributes, or undefined when the model has no key member
|
|
413
|
+
*/
|
|
414
|
+
export function buildModelPrimaryKey(ctx, cls) {
|
|
415
|
+
const symbol = propertiesOf(ctx, cls).find(p => propertyIsKeyedBySymbol(ctx, p, "@webda/models", "WEBDA_PRIMARY_KEY"));
|
|
416
|
+
if (!symbol)
|
|
417
|
+
return undefined;
|
|
418
|
+
const keys = [];
|
|
419
|
+
const declaration = valueDeclarationOf(ctx, symbol);
|
|
420
|
+
if (!declaration)
|
|
421
|
+
return keys;
|
|
422
|
+
const typeNode = declaration.type;
|
|
423
|
+
if (typeNode && is.isTypeOperatorNode(typeNode) && typeNode.operator === SyntaxKind.ReadonlyKeyword) {
|
|
424
|
+
const inner = typeNode.type;
|
|
425
|
+
if (is.isTupleTypeNode(inner)) {
|
|
426
|
+
for (const element of inner.elements) {
|
|
427
|
+
if (is.isLiteralTypeNode(element))
|
|
428
|
+
keys.push(text(ctx, element.literal).replace(/"/g, ""));
|
|
429
|
+
}
|
|
430
|
+
return keys;
|
|
431
|
+
}
|
|
432
|
+
if (is.isArrayTypeNode(inner))
|
|
433
|
+
return keys;
|
|
434
|
+
}
|
|
435
|
+
const initializer = declaration.initializer;
|
|
436
|
+
if (initializer && is.isAsExpression(initializer)) {
|
|
437
|
+
const array = initializer.expression;
|
|
438
|
+
if (is.isArrayLiteralExpression(array)) {
|
|
439
|
+
for (const element of array.elements) {
|
|
440
|
+
if (is.isStringLiteral(element))
|
|
441
|
+
keys.push(element.text);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return keys;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* A model's `PrimaryKeySeparator`, from a `[WEBDA_PRIMARY_KEY_SEPARATOR] = ":"` initialiser.
|
|
449
|
+
* @param ctx - analysis context
|
|
450
|
+
* @param cls - the model class
|
|
451
|
+
* @returns the separator, or undefined when the model declares none
|
|
452
|
+
*/
|
|
453
|
+
export function buildModelPrimaryKeySeparator(ctx, cls) {
|
|
454
|
+
const symbol = propertiesOf(ctx, cls).find(p => propertyIsKeyedBySymbol(ctx, p, "@webda/models", "WEBDA_PRIMARY_KEY_SEPARATOR"));
|
|
455
|
+
const initializer = symbol ? valueDeclarationOf(ctx, symbol)?.initializer : undefined;
|
|
456
|
+
return initializer && is.isStringLiteral(initializer) ? initializer.text : undefined;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Leading comments of a parameter, one string per comment.
|
|
460
|
+
* @param ctx - analysis context
|
|
461
|
+
* @param param - the parameter
|
|
462
|
+
* @returns comment texts
|
|
463
|
+
*/
|
|
464
|
+
function leadingComments(ctx, param) {
|
|
465
|
+
const trivia = ctx.triviaOf(sourceFileOf(param), param);
|
|
466
|
+
return trivia.match(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g) ?? [];
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* One `@Command` argument, as `extractParamDefinition`.
|
|
470
|
+
* @param ctx - analysis context
|
|
471
|
+
* @param param - the parameter declaration
|
|
472
|
+
* @returns the argument definition
|
|
473
|
+
*/
|
|
474
|
+
function commandArg(ctx, param) {
|
|
475
|
+
let type = "string";
|
|
476
|
+
if (param.type) {
|
|
477
|
+
const typeText = text(ctx, param.type).toLowerCase().trim();
|
|
478
|
+
type = typeText === "number" ? "number" : typeText === "boolean" ? "boolean" : "string";
|
|
479
|
+
}
|
|
480
|
+
const def = { type };
|
|
481
|
+
if (!param.initializer && !param.questionToken)
|
|
482
|
+
def.required = true;
|
|
483
|
+
const initializer = param.initializer;
|
|
484
|
+
if (initializer) {
|
|
485
|
+
if (is.isStringLiteral(initializer))
|
|
486
|
+
def.default = initializer.text;
|
|
487
|
+
else if (is.isNumericLiteral(initializer))
|
|
488
|
+
def.default = Number(initializer.text);
|
|
489
|
+
else if (initializer.kind === SyntaxKind.TrueKeyword)
|
|
490
|
+
def.default = true;
|
|
491
|
+
else if (initializer.kind === SyntaxKind.FalseKeyword)
|
|
492
|
+
def.default = false;
|
|
493
|
+
}
|
|
494
|
+
const docs = {};
|
|
495
|
+
for (const comment of leadingComments(ctx, param)) {
|
|
496
|
+
const alias = comment.match(/@alias\s+(\S+)/);
|
|
497
|
+
if (alias)
|
|
498
|
+
docs.alias = alias[1];
|
|
499
|
+
const description = comment.match(/@description\s+(.+?)(?:\n|$|\*\/)/);
|
|
500
|
+
if (description)
|
|
501
|
+
docs.description = description[1]
|
|
502
|
+
.trim()
|
|
503
|
+
.replace(/\s*\*\s*/g, " ")
|
|
504
|
+
.trim();
|
|
505
|
+
const deprecated = comment.match(/@deprecated\s*(.*?)(?:\n|$|\*\/)/);
|
|
506
|
+
if (deprecated)
|
|
507
|
+
docs.deprecated =
|
|
508
|
+
deprecated[1]
|
|
509
|
+
.trim()
|
|
510
|
+
.replace(/\s*\*\s*/g, " ")
|
|
511
|
+
.trim() || "";
|
|
512
|
+
}
|
|
513
|
+
if (docs.alias)
|
|
514
|
+
def.alias = docs.alias;
|
|
515
|
+
if (docs.description)
|
|
516
|
+
def.description = docs.description;
|
|
517
|
+
if (docs.deprecated !== undefined)
|
|
518
|
+
def.deprecated = docs.deprecated;
|
|
519
|
+
return def;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* A service's `commands`, as `CommandsMetadata`: every `@Command` or
|
|
523
|
+
* `@BuildCommand` method declared on the class itself.
|
|
524
|
+
* @param ctx - analysis context
|
|
525
|
+
* @param cls - the service class
|
|
526
|
+
* @returns command name to definition
|
|
527
|
+
*/
|
|
528
|
+
export function buildCommands(ctx, cls) {
|
|
529
|
+
const commands = {};
|
|
530
|
+
for (const member of cls.members ?? []) {
|
|
531
|
+
if (!is.isMethodDeclaration(member))
|
|
532
|
+
continue;
|
|
533
|
+
const decorator = decoratorsOf(member).find(candidate => {
|
|
534
|
+
const name = decoratorName(ctx, candidate);
|
|
535
|
+
return name === "Command" || name === "BuildCommand";
|
|
536
|
+
});
|
|
537
|
+
if (!decorator || !is.isCallExpression(decorator.expression))
|
|
538
|
+
continue;
|
|
539
|
+
const isBuild = decoratorName(ctx, decorator) === "BuildCommand";
|
|
540
|
+
const args = decorator.expression.arguments ?? [];
|
|
541
|
+
let commandName;
|
|
542
|
+
let options;
|
|
543
|
+
if (isBuild) {
|
|
544
|
+
commandName = "build";
|
|
545
|
+
options = args[0];
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
if (args.length === 0 || !is.isStringLiteral(args[0]))
|
|
549
|
+
continue;
|
|
550
|
+
commandName = args[0].text;
|
|
551
|
+
options = args[1];
|
|
552
|
+
}
|
|
553
|
+
let description = "";
|
|
554
|
+
let requires;
|
|
555
|
+
let phase = isBuild ? "resolved" : undefined;
|
|
556
|
+
if (options && is.isObjectLiteralExpression(options)) {
|
|
557
|
+
for (const property of options.properties) {
|
|
558
|
+
if (!is.isPropertyAssignment(property) || !is.isIdentifier(property.name))
|
|
559
|
+
continue;
|
|
560
|
+
const key = property.name.text;
|
|
561
|
+
const initializer = property.initializer;
|
|
562
|
+
if (key === "description" && is.isStringLiteral(initializer)) {
|
|
563
|
+
description = initializer.text;
|
|
564
|
+
}
|
|
565
|
+
else if (key === "requires" && is.isArrayLiteralExpression(initializer)) {
|
|
566
|
+
requires = initializer.elements.filter((e) => is.isStringLiteral(e)).map((e) => e.text);
|
|
567
|
+
}
|
|
568
|
+
else if (!isBuild && key === "phase" && is.isStringLiteral(initializer)) {
|
|
569
|
+
const value = initializer.text;
|
|
570
|
+
if (value === "resolved" || value === "initialized")
|
|
571
|
+
phase = value;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const methodArgs = {};
|
|
576
|
+
for (const param of member.parameters)
|
|
577
|
+
methodArgs[text(ctx, param.name)] = commandArg(ctx, param);
|
|
578
|
+
commands[commandName] = { description, method: text(ctx, member.name), args: methodArgs, requires, phase };
|
|
579
|
+
}
|
|
580
|
+
return commands;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* A service's `capabilities`, as `CapabilitiesMetadata`: the
|
|
584
|
+
* `@WebdaCapability` of every type named in the class's own heritage clauses.
|
|
585
|
+
* @param ctx - analysis context
|
|
586
|
+
* @param cls - the service class
|
|
587
|
+
* @returns sorted capability names
|
|
588
|
+
*/
|
|
589
|
+
export function buildCapabilities(ctx, cls) {
|
|
590
|
+
const capabilities = [];
|
|
591
|
+
const type = declaredTypeOf(ctx, cls);
|
|
592
|
+
const declarations = declarationsOf(ctx, type?.getSymbol());
|
|
593
|
+
for (const declaration of declarations) {
|
|
594
|
+
if (!is.isClassDeclaration(declaration))
|
|
595
|
+
continue;
|
|
596
|
+
for (const clause of declaration.heritageClauses ?? []) {
|
|
597
|
+
for (const expression of clause.types) {
|
|
598
|
+
const target = ctx.checker.getTypeAtLocation(expression);
|
|
599
|
+
for (const targetDeclaration of declarationsOf(ctx, target?.getSymbol())) {
|
|
600
|
+
const tag = findTag(targetDeclaration, "WebdaCapability");
|
|
601
|
+
if (!tag)
|
|
602
|
+
continue;
|
|
603
|
+
const name = (getTextOfJSDocComment(tag.comment) ?? "").trim().split(" ").shift() || undefined;
|
|
604
|
+
if (name && !capabilities.includes(name))
|
|
605
|
+
capabilities.push(name);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return capabilities.sort();
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* The Behavior identifier of a `@WebdaBehavior` class: the tag's first token,
|
|
614
|
+
* else the namespaced class name.
|
|
615
|
+
* @param cls - the class
|
|
616
|
+
* @param namespace - project namespace
|
|
617
|
+
* @returns the identifier, or undefined when the class is not a Behavior
|
|
618
|
+
*/
|
|
619
|
+
function behaviorIdentifier(cls, namespace) {
|
|
620
|
+
const tag = findTag(cls, "WebdaBehavior");
|
|
621
|
+
if (!tag)
|
|
622
|
+
return undefined;
|
|
623
|
+
const override = getTextOfJSDocComment(tag.comment)?.trim();
|
|
624
|
+
if (override) {
|
|
625
|
+
const first = override.split(/\s+/).shift();
|
|
626
|
+
if (first)
|
|
627
|
+
return first;
|
|
628
|
+
}
|
|
629
|
+
const className = cls.name?.text ?? "";
|
|
630
|
+
if (!className)
|
|
631
|
+
return undefined;
|
|
632
|
+
return completeNamespace(className, namespace);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* `WebdaProject.completeNamespace`.
|
|
636
|
+
* @param name - a bare or qualified name
|
|
637
|
+
* @param namespace - project namespace
|
|
638
|
+
* @returns the qualified name
|
|
639
|
+
*/
|
|
640
|
+
function completeNamespace(name, namespace) {
|
|
641
|
+
if (name.includes("/"))
|
|
642
|
+
return name;
|
|
643
|
+
return `${namespace ?? "Webda"}/${name}`;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* A Behavior's `Actions`, as `BehaviorsMetadata`.
|
|
647
|
+
* @param ctx - analysis context
|
|
648
|
+
* @param cls - the Behavior class
|
|
649
|
+
* @param identifier - its identifier, for error messages
|
|
650
|
+
* @returns action name to metadata
|
|
651
|
+
* @throws on a static action or `global: true`, both of which Behaviors forbid
|
|
652
|
+
*/
|
|
653
|
+
export function buildBehaviorActions(ctx, cls, identifier) {
|
|
654
|
+
const actions = {};
|
|
655
|
+
const processMethod = (method) => {
|
|
656
|
+
const methodName = text(ctx, method.name);
|
|
657
|
+
if (isStaticMember(method)) {
|
|
658
|
+
throw new Error(`Behavior ${identifier}: static @Action methods are not allowed (method "${methodName}").`);
|
|
659
|
+
}
|
|
660
|
+
const meta = {};
|
|
661
|
+
forEachOperationOption(ctx, method, (key, initializer) => {
|
|
662
|
+
if (key === "global") {
|
|
663
|
+
throw new Error(`Behavior ${identifier}: @Action({ global: true }) is not allowed (method "${methodName}").`);
|
|
664
|
+
}
|
|
665
|
+
if ((key === "description" || key === "summary") && is.isStringLiteral(initializer)) {
|
|
666
|
+
meta[key] = initializer.text;
|
|
667
|
+
}
|
|
668
|
+
if (key === "rest" && is.isObjectLiteralExpression(initializer)) {
|
|
669
|
+
const rest = {};
|
|
670
|
+
for (const restProperty of initializer.properties) {
|
|
671
|
+
if (is.isPropertyAssignment(restProperty) &&
|
|
672
|
+
is.isIdentifier(restProperty.name) &&
|
|
673
|
+
is.isStringLiteral(restProperty.initializer)) {
|
|
674
|
+
const restKey = restProperty.name.text;
|
|
675
|
+
if (restKey === "route" || restKey === "method")
|
|
676
|
+
rest[restKey] = restProperty.initializer.text;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (Object.keys(rest).length > 0)
|
|
680
|
+
meta.rest = rest;
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
actions[methodName] = meta;
|
|
684
|
+
};
|
|
685
|
+
decoratedInstanceMethods(ctx, cls).forEach(processMethod);
|
|
686
|
+
decoratedStaticMethods(ctx, cls).forEach(processMethod);
|
|
687
|
+
return actions;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* The Behavior identifier of an attribute's written type, when that type is a
|
|
691
|
+
* `@WebdaBehavior` class — `resolveBehaviorIdentifierFromType`.
|
|
692
|
+
* @param ctx - analysis context
|
|
693
|
+
* @param typeRef - the attribute's type reference
|
|
694
|
+
* @param namespace - project namespace
|
|
695
|
+
* @returns the identifier
|
|
696
|
+
*/
|
|
697
|
+
function behaviorOfTypeReference(ctx, typeRef, namespace) {
|
|
698
|
+
if (!typeRef?.typeName)
|
|
699
|
+
return undefined;
|
|
700
|
+
// `getTypeAtLocation(typeName)` answers with a symbol-less type on 7.1; the
|
|
701
|
+
// type of the reference itself carries the class symbol.
|
|
702
|
+
let symbol;
|
|
703
|
+
try {
|
|
704
|
+
symbol = ctx.checker.getTypeFromTypeNode(typeRef).getSymbol();
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
symbol = undefined;
|
|
708
|
+
}
|
|
709
|
+
symbol ??= resolveAlias(ctx, ctx.checker.getSymbolAtLocation(typeRef.typeName));
|
|
710
|
+
const declaration = declarationsOf(ctx, symbol).find(d => is.isClassDeclaration(d) || is.isClassExpression(d));
|
|
711
|
+
if (!declaration)
|
|
712
|
+
return undefined;
|
|
713
|
+
return behaviorIdentifier(declaration, namespace);
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Whether a property should be skipped entirely: `@NotEnumerable` (the bare
|
|
717
|
+
* decorator only) or an `@ignore` JSDoc tag.
|
|
718
|
+
* @param ctx - analysis context
|
|
719
|
+
* @param member - the property declaration
|
|
720
|
+
* @returns true to skip
|
|
721
|
+
*/
|
|
722
|
+
function isSkippedProperty(ctx, member) {
|
|
723
|
+
for (const decorator of decoratorsOf(member)) {
|
|
724
|
+
if (is.isIdentifier(decorator.expression) && decorator.expression.text === "NotEnumerable")
|
|
725
|
+
return true;
|
|
726
|
+
}
|
|
727
|
+
return findTag(member, "ignore") !== undefined;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* `processModels`: Relations, Ancestors and Subclasses for every local model,
|
|
731
|
+
* plus the fields the plugins fill in later.
|
|
732
|
+
* @param ctx - analysis context
|
|
733
|
+
* @param models - local models, in program-walk order
|
|
734
|
+
* @param namespace - project namespace
|
|
735
|
+
* @param errors - accumulator for conditions TypeScript 6 throws on
|
|
736
|
+
* @param appPath - project directory, to tell dependency models from local ones
|
|
737
|
+
* @returns model entries, in walk order (sorted by the caller)
|
|
738
|
+
*/
|
|
739
|
+
function processModels(ctx, models, namespace, errors, appPath) {
|
|
740
|
+
const byTypeId = new Map();
|
|
741
|
+
for (const model of models)
|
|
742
|
+
byTypeId.set(model.type.id, model.object.name);
|
|
743
|
+
const modelOf = (typeNode) => {
|
|
744
|
+
if (!typeNode)
|
|
745
|
+
return undefined;
|
|
746
|
+
try {
|
|
747
|
+
return byTypeId.get(ctx.checker.getTypeFromTypeNode(typeNode).id);
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
return undefined;
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
const entries = {};
|
|
754
|
+
for (const { object, declaration } of models) {
|
|
755
|
+
const name = object.name;
|
|
756
|
+
const sf = sourceFileOf(declaration);
|
|
757
|
+
const relations = {};
|
|
758
|
+
let hasProperty = false;
|
|
759
|
+
for (const property of propertiesOf(ctx, declaration)) {
|
|
760
|
+
let member = valueDeclarationOf(ctx, property);
|
|
761
|
+
if (!member)
|
|
762
|
+
continue;
|
|
763
|
+
if (is.isPropertyDeclaration(member)) {
|
|
764
|
+
hasProperty = true;
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
// A property inherited from a dependency's model is read from its
|
|
768
|
+
// declaration file, where relation properties are emitted as accessors.
|
|
769
|
+
member = declarationFileGetter(ctx, property, member);
|
|
770
|
+
if (!member)
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
if (isSkippedProperty(ctx, member))
|
|
774
|
+
continue;
|
|
775
|
+
const typeNode = member.type;
|
|
776
|
+
if (!typeNode || !is.isTypeReferenceNode(typeNode))
|
|
777
|
+
continue;
|
|
778
|
+
const attribute = property.name;
|
|
779
|
+
const typeArguments = typeNode.typeArguments ?? [];
|
|
780
|
+
const typeName = text(ctx, typeNode.typeName);
|
|
781
|
+
const addLink = (type) => {
|
|
782
|
+
relations.links ??= [];
|
|
783
|
+
relations.links.push({ attribute, model: modelOf(typeArguments[0]), type });
|
|
784
|
+
};
|
|
785
|
+
switch (typeName) {
|
|
786
|
+
case "ModelParent": {
|
|
787
|
+
const model = modelOf(typeArguments[0]);
|
|
788
|
+
if (!model)
|
|
789
|
+
errors.push(`Cannot find parent model ${text(ctx, typeArguments[0])} for ${name}.${attribute}`);
|
|
790
|
+
relations.parent = { attribute, model };
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
case "ModelRelated": {
|
|
794
|
+
relations.queries ??= [];
|
|
795
|
+
const model = modelOf(typeArguments[0]);
|
|
796
|
+
if (!model)
|
|
797
|
+
errors.push(`Cannot find query model ${text(ctx, typeArguments[0])} for ${name}.${attribute}`);
|
|
798
|
+
relations.queries.push({
|
|
799
|
+
attribute,
|
|
800
|
+
model,
|
|
801
|
+
targetAttribute: typeArguments[1] ? text(ctx, typeArguments[1]).replace(/"/g, "") : undefined
|
|
802
|
+
});
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
case "ModelsMapped": {
|
|
806
|
+
relations.maps ??= [];
|
|
807
|
+
const model = modelOf(typeArguments[0]);
|
|
808
|
+
if (!model)
|
|
809
|
+
errors.push(`Cannot find map model ${text(ctx, typeArguments[0])} for ${name}.${attribute}`);
|
|
810
|
+
const map = {
|
|
811
|
+
attribute,
|
|
812
|
+
cascadeDelete: findTag(member, "CascadeDelete") !== undefined,
|
|
813
|
+
model,
|
|
814
|
+
targetLink: text(ctx, typeArguments[1]).replace(/"/g, ""),
|
|
815
|
+
targetAttributes: text(ctx, typeArguments[2])
|
|
816
|
+
.replace(/"/g, "")
|
|
817
|
+
.split("|")
|
|
818
|
+
.map(t => t.trim())
|
|
819
|
+
};
|
|
820
|
+
if (!map.targetAttributes.includes("uuid"))
|
|
821
|
+
map.targetAttributes.push("uuid");
|
|
822
|
+
relations.maps.push(map);
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
case "ModelLink":
|
|
826
|
+
addLink("LINK");
|
|
827
|
+
break;
|
|
828
|
+
case "ModelLinksMap":
|
|
829
|
+
addLink("LINKS_MAP");
|
|
830
|
+
break;
|
|
831
|
+
case "ModelLinksArray":
|
|
832
|
+
addLink("LINKS_ARRAY");
|
|
833
|
+
break;
|
|
834
|
+
case "ModelLinksSimpleArray":
|
|
835
|
+
addLink("LINKS_SIMPLE_ARRAY");
|
|
836
|
+
break;
|
|
837
|
+
default: {
|
|
838
|
+
const behavior = behaviorOfTypeReference(ctx, typeNode, namespace);
|
|
839
|
+
if (behavior) {
|
|
840
|
+
relations.behaviors ??= [];
|
|
841
|
+
relations.behaviors.push({ attribute, behavior });
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
// TypeScript 6 records the entry from inside the property loop, so a model
|
|
847
|
+
// with no property declarations at all has no entry.
|
|
848
|
+
if (!hasProperty)
|
|
849
|
+
continue;
|
|
850
|
+
entries[name] = {
|
|
851
|
+
Plural: "",
|
|
852
|
+
Import: object.importTarget,
|
|
853
|
+
Relations: relations,
|
|
854
|
+
Ancestors: [],
|
|
855
|
+
Subclasses: [],
|
|
856
|
+
Reflection: reflectAttributes(ctx, sf, declaration),
|
|
857
|
+
Schemas: {},
|
|
858
|
+
Actions: {},
|
|
859
|
+
Events: [],
|
|
860
|
+
PrimaryKey: [],
|
|
861
|
+
Identifier: name
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
// Ancestors: the class tree mapped to local models, nearest first, without
|
|
865
|
+
// the model itself and — as in TypeScript 6 — without `Webda/CoreModel`.
|
|
866
|
+
for (const { object, declaration } of models) {
|
|
867
|
+
const entry = entries[object.name];
|
|
868
|
+
if (!entry)
|
|
869
|
+
continue;
|
|
870
|
+
entry.Ancestors = classTree(ctx, declaration)
|
|
871
|
+
.map((type) => byTypeId.get(declaredId(ctx, type)) ?? dependencyModelOf(ctx, type, appPath))
|
|
872
|
+
.filter((ancestor) => ancestor !== undefined && ancestor !== "Webda/CoreModel" && ancestor !== object.name);
|
|
873
|
+
}
|
|
874
|
+
for (const name of Object.keys(entries)) {
|
|
875
|
+
const ancestors = entries[name].Ancestors;
|
|
876
|
+
if (ancestors.length)
|
|
877
|
+
entries[ancestors[0]]?.Subclasses.push(name);
|
|
878
|
+
}
|
|
879
|
+
return entries;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Getter declaring a property in a declaration file.
|
|
883
|
+
*
|
|
884
|
+
* Declaration emit turns a relation property (`_user: ModelLink<T>`) into a
|
|
885
|
+
* `get`/`set` pair; the getter's return type is the property's type.
|
|
886
|
+
* @param ctx - analysis context
|
|
887
|
+
* @param property - the property symbol
|
|
888
|
+
* @param member - its value declaration
|
|
889
|
+
* @returns the getter, or undefined outside a declaration file
|
|
890
|
+
*/
|
|
891
|
+
function declarationFileGetter(ctx, property, member) {
|
|
892
|
+
if (!sourceFileOf(member)?.isDeclarationFile)
|
|
893
|
+
return undefined;
|
|
894
|
+
if (is.isGetAccessorDeclaration(member))
|
|
895
|
+
return member;
|
|
896
|
+
for (const declaration of property.declarations ?? []) {
|
|
897
|
+
const node = declaration?.resolve(ctx.project);
|
|
898
|
+
if (node && is.isGetAccessorDeclaration(node))
|
|
899
|
+
return node;
|
|
900
|
+
}
|
|
901
|
+
return undefined;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Framework base classes from `@webda/models`. Like `Webda/CoreModel`, they are
|
|
905
|
+
* the root every model derives from, not a domain ancestor, and are not
|
|
906
|
+
* recorded.
|
|
907
|
+
*/
|
|
908
|
+
const FRAMEWORK_BASE_MODELS = new Set(["Webda/Model", "Webda/UuidModel"]);
|
|
909
|
+
/**
|
|
910
|
+
* Namespaced name of a class declared as a model by a dependency package.
|
|
911
|
+
* @param ctx - analysis context
|
|
912
|
+
* @param type - a class in a model's class tree
|
|
913
|
+
* @param appPath - project directory
|
|
914
|
+
* @returns the model name, when the class is a dependency's domain model
|
|
915
|
+
*/
|
|
916
|
+
function dependencyModelOf(ctx, type, appPath) {
|
|
917
|
+
const symbol = type.getSymbol?.() ?? type.symbol;
|
|
918
|
+
const declaration = valueDeclarationOf(ctx, symbol);
|
|
919
|
+
const fileName = declaration ? sourceFileOf(declaration)?.fileName : undefined;
|
|
920
|
+
if (!fileName || !symbol?.name)
|
|
921
|
+
return undefined;
|
|
922
|
+
const name = dependencyModelName(fileName, symbol.name, appPath);
|
|
923
|
+
return name && !FRAMEWORK_BASE_MODELS.has(name) ? name : undefined;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Id of the declared type behind a (possibly instantiated) type — what
|
|
927
|
+
* TypeScript 6's `getClassTree` records, as it re-reads each base through
|
|
928
|
+
* `getTypeAtLocation(valueDeclaration)`.
|
|
929
|
+
* @param ctx - analysis context
|
|
930
|
+
* @param type - the type
|
|
931
|
+
* @returns the declared type's id
|
|
932
|
+
*/
|
|
933
|
+
function declaredId(ctx, type) {
|
|
934
|
+
const symbol = type.getSymbol?.();
|
|
935
|
+
if (!symbol)
|
|
936
|
+
return type.id;
|
|
937
|
+
try {
|
|
938
|
+
return ctx.checker.getDeclaredTypeOfSymbol(symbol).id;
|
|
939
|
+
}
|
|
940
|
+
catch {
|
|
941
|
+
return type.id;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Find a class declaration by file and written name.
|
|
946
|
+
* @param ctx - analysis context
|
|
947
|
+
* @param fileName - declaring file
|
|
948
|
+
* @param className - class name
|
|
949
|
+
* @returns the declaration
|
|
950
|
+
*/
|
|
951
|
+
function findClass(ctx, fileName, className) {
|
|
952
|
+
const sf = ctx.program.getSourceFile(fileName);
|
|
953
|
+
return sf?.statements.find((statement) => is.isClassDeclaration(statement) && statement.name?.text === className);
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* `getJSTargetFile` without extension: the output path for a project file, or
|
|
957
|
+
* `node_modules/<package>/<path>` for a dependency's declaration file.
|
|
958
|
+
* @param fileName - the source file
|
|
959
|
+
* @param options - discovery options
|
|
960
|
+
* @returns the import path
|
|
961
|
+
*/
|
|
962
|
+
function jsTarget(fileName, options) {
|
|
963
|
+
if (!fileName.startsWith(options.appPath)) {
|
|
964
|
+
let folder = fileName;
|
|
965
|
+
let libPath = "node_modules/";
|
|
966
|
+
for (;;) {
|
|
967
|
+
const parent = join(folder, "..");
|
|
968
|
+
if (parent === folder || parent.length <= 2)
|
|
969
|
+
break;
|
|
970
|
+
folder = parent;
|
|
971
|
+
const manifest = join(folder, "package.json");
|
|
972
|
+
if (!existsSync(manifest))
|
|
973
|
+
continue;
|
|
974
|
+
const name = JSON.parse(readFileSync(manifest, "utf8")).name;
|
|
975
|
+
if (name) {
|
|
976
|
+
libPath += name + "/";
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return (libPath + relative(folder, fileName)).replace(/\.d\.ts$/, ".js").replace(/\.js$/, "");
|
|
981
|
+
}
|
|
982
|
+
return outputTarget(fileName, options);
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* The `Configuration` of a service: where its parameters class is exported.
|
|
986
|
+
* @param ctx - analysis context
|
|
987
|
+
* @param declaration - the service class
|
|
988
|
+
* @param base - parameters base type
|
|
989
|
+
* @param options - discovery options
|
|
990
|
+
* @returns `path:export`, or undefined when there is no parameters type
|
|
991
|
+
* @throws when the parameters class is not exported
|
|
992
|
+
*/
|
|
993
|
+
function configurationOf(ctx, declaration, base, options) {
|
|
994
|
+
const node = findParametersNode(declaration, ctx.checker, ctx.project, base);
|
|
995
|
+
if (!node)
|
|
996
|
+
return undefined;
|
|
997
|
+
const type = ctx.checker.getTypeAtLocation(node);
|
|
998
|
+
const target = valueDeclarationOf(ctx, type.getSymbol());
|
|
999
|
+
if (!target)
|
|
1000
|
+
throw new Error(`Cannot find the declaration of ${text(ctx, node)}`);
|
|
1001
|
+
const sf = sourceFileOf(target);
|
|
1002
|
+
const exportName = exportedName(ctx, sf, target);
|
|
1003
|
+
if (!exportName) {
|
|
1004
|
+
throw new Error(`Cannot find exported name for ${target.name?.text}, check that the class is exported`);
|
|
1005
|
+
}
|
|
1006
|
+
return `${jsTarget(sf.fileName, options)}:${exportName}`;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Read the application's `package.json`.
|
|
1010
|
+
* @param appPath - application root
|
|
1011
|
+
* @returns the manifest, or an empty object
|
|
1012
|
+
*/
|
|
1013
|
+
function manifestOf(appPath) {
|
|
1014
|
+
try {
|
|
1015
|
+
return JSON.parse(readFileSync(join(appPath, "package.json"), "utf8"));
|
|
1016
|
+
}
|
|
1017
|
+
catch {
|
|
1018
|
+
return {};
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Generate the complete `webda.module.json` for a project.
|
|
1023
|
+
* @param ctx - analysis context over the project
|
|
1024
|
+
* @param options - roots, namespace and capabilities
|
|
1025
|
+
* @returns the module, naming violations and errors
|
|
1026
|
+
*/
|
|
1027
|
+
export function generateWebdaModule(ctx, options) {
|
|
1028
|
+
const compilerOptions = ctx.program.getCompilerOptions?.() ?? {};
|
|
1029
|
+
const discovery = {
|
|
1030
|
+
appPath: options.appPath,
|
|
1031
|
+
rootDir: options.rootDir ?? compilerOptions.rootDir ?? join(options.appPath, "src"),
|
|
1032
|
+
outDir: options.outDir ?? compilerOptions.outDir ?? join(options.appPath, "lib"),
|
|
1033
|
+
namespace: options.namespace
|
|
1034
|
+
};
|
|
1035
|
+
const namespace = options.namespace;
|
|
1036
|
+
const errors = [];
|
|
1037
|
+
// TypeScript 6 walks `program.getSourceFiles()` in program order; several
|
|
1038
|
+
// maps below keep that insertion order, so reproduce it.
|
|
1039
|
+
const fileOrder = new Map();
|
|
1040
|
+
ctx.program.getSourceFileNames().forEach((fileName, index) => fileOrder.set(fileName, index));
|
|
1041
|
+
const walkOrder = (a, b) => (fileOrder.get(a.fileName) ?? 0) - (fileOrder.get(b.fileName) ?? 0) || a.pos - b.pos;
|
|
1042
|
+
const classified = discoverWebdaObjects(ctx, { ...discovery, keepDuplicates: true })
|
|
1043
|
+
.map(object => ({ object, declaration: findClass(ctx, object.fileName, object.className) }))
|
|
1044
|
+
.filter((item) => !!item.declaration)
|
|
1045
|
+
// A `@WebdaSchema` class is a top-level schema and nothing else: the
|
|
1046
|
+
// TypeScript 6 walk returns before classifying it.
|
|
1047
|
+
.filter(item => webdaTags(item.declaration).WebdaSchema === undefined)
|
|
1048
|
+
.sort((a, b) => walkOrder({ fileName: a.object.fileName, pos: a.declaration.pos }, { fileName: b.object.fileName, pos: b.declaration.pos }));
|
|
1049
|
+
// Two classes can share a namespaced name. TypeScript 6 let the last class
|
|
1050
|
+
// in program order win silently, which registered one class while code
|
|
1051
|
+
// saved the other (`@webda/core` shipped two AuditEntry models). Keep that
|
|
1052
|
+
// choice so the module stays inspectable, but report it: the build fails.
|
|
1053
|
+
const slots = new Map();
|
|
1054
|
+
const discovered = [];
|
|
1055
|
+
for (const item of classified) {
|
|
1056
|
+
const key = `${item.object.section}:${item.object.name}`;
|
|
1057
|
+
const slot = slots.get(key);
|
|
1058
|
+
if (slot === undefined) {
|
|
1059
|
+
slots.set(key, discovered.length);
|
|
1060
|
+
discovered.push(item);
|
|
1061
|
+
}
|
|
1062
|
+
else {
|
|
1063
|
+
const previous = discovered[slot].object;
|
|
1064
|
+
errors.push(`${item.object.name} is declared twice: ${previous.className} in ${previous.fileName} and ` +
|
|
1065
|
+
`${item.object.className} in ${item.object.fileName}; rename one`);
|
|
1066
|
+
discovered[slot] = item;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
// Naming guard, in walk order, one report per file — every classified
|
|
1070
|
+
// class counts, including a duplicate that loses its slot.
|
|
1071
|
+
const namingViolations = [];
|
|
1072
|
+
for (const { object } of classified) {
|
|
1073
|
+
const expectedSuffix = SECTION_SUFFIX[object.section];
|
|
1074
|
+
if (!expectedSuffix)
|
|
1075
|
+
continue;
|
|
1076
|
+
if (MAPPED_SUFFIXES.some(suffix => object.fileName.endsWith(suffix)))
|
|
1077
|
+
continue;
|
|
1078
|
+
if (namingViolations.some(v => v.fileName === object.fileName))
|
|
1079
|
+
continue;
|
|
1080
|
+
namingViolations.push({
|
|
1081
|
+
fileName: object.fileName,
|
|
1082
|
+
className: object.className,
|
|
1083
|
+
section: object.section,
|
|
1084
|
+
expectedSuffix
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
const services = {
|
|
1088
|
+
beans: {},
|
|
1089
|
+
deployers: {},
|
|
1090
|
+
moddas: {}
|
|
1091
|
+
};
|
|
1092
|
+
const models = [];
|
|
1093
|
+
for (const { object, declaration } of discovered) {
|
|
1094
|
+
if (object.section === "models") {
|
|
1095
|
+
const type = declaredTypeOf(ctx, declaration);
|
|
1096
|
+
if (type)
|
|
1097
|
+
models.push({ object, declaration, type, tags: webdaTags(declaration) });
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
services[object.section][object.name] = { Import: object.importTarget, Schema: {} };
|
|
1101
|
+
}
|
|
1102
|
+
const module = {
|
|
1103
|
+
$schema: WEBDA_MODULE_SCHEMA,
|
|
1104
|
+
beans: sortObject(services.beans),
|
|
1105
|
+
deployers: sortObject(services.deployers),
|
|
1106
|
+
moddas: sortObject(services.moddas),
|
|
1107
|
+
models: sortObject(processModels(ctx, models, namespace, errors, options.appPath)),
|
|
1108
|
+
schemas: {},
|
|
1109
|
+
behaviors: {},
|
|
1110
|
+
capabilities: options.capabilities ?? manifestOf(options.appPath).webda?.capabilities
|
|
1111
|
+
};
|
|
1112
|
+
// Schemas and Configuration — `WebdaSchemaResults.generateSchemas`.
|
|
1113
|
+
for (const { object, declaration } of discovered) {
|
|
1114
|
+
if (object.section === "models") {
|
|
1115
|
+
const entry = module.models[object.name];
|
|
1116
|
+
if (!entry)
|
|
1117
|
+
continue;
|
|
1118
|
+
try {
|
|
1119
|
+
entry.Schemas = generateModelSchemas(declaration, { project: ctx.project, checker: ctx.checker });
|
|
1120
|
+
}
|
|
1121
|
+
catch (error) {
|
|
1122
|
+
errors.push(`${object.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1123
|
+
}
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
const entry = module[object.section][object.name];
|
|
1127
|
+
const base = object.section === "deployers" ? "DeployerResources" : "ServiceParameters";
|
|
1128
|
+
try {
|
|
1129
|
+
const schema = generateServiceSchema(declaration, {
|
|
1130
|
+
project: ctx.project,
|
|
1131
|
+
checker: ctx.checker,
|
|
1132
|
+
addOpenApi: object.section !== "deployers",
|
|
1133
|
+
parametersBase: object.section === "deployers" ? base : undefined,
|
|
1134
|
+
title: declaration.name?.text ?? object.className
|
|
1135
|
+
});
|
|
1136
|
+
if (schema)
|
|
1137
|
+
entry.Schema = schema;
|
|
1138
|
+
}
|
|
1139
|
+
catch (error) {
|
|
1140
|
+
errors.push(`${object.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1141
|
+
}
|
|
1142
|
+
try {
|
|
1143
|
+
const configuration = configurationOf(ctx, declaration, base, discovery);
|
|
1144
|
+
if (configuration)
|
|
1145
|
+
entry.Configuration = configuration;
|
|
1146
|
+
}
|
|
1147
|
+
catch (error) {
|
|
1148
|
+
errors.push(`${object.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
try {
|
|
1152
|
+
Object.assign(module.schemas, generateTopLevelSchemas(ctx, discovery));
|
|
1153
|
+
}
|
|
1154
|
+
catch (error) {
|
|
1155
|
+
errors.push(`schemas: ${error instanceof Error ? error.message : String(error)}`);
|
|
1156
|
+
}
|
|
1157
|
+
// Plugins, in the TypeScript 6 order: Actions, Behaviors, Capabilities,
|
|
1158
|
+
// Commands, Events, PrimaryKey, Plural.
|
|
1159
|
+
for (const { object, declaration } of models) {
|
|
1160
|
+
const entry = module.models[object.name];
|
|
1161
|
+
if (entry)
|
|
1162
|
+
entry.Actions = buildModelActions(ctx, declaration);
|
|
1163
|
+
}
|
|
1164
|
+
for (const { sf, declaration } of behaviorClasses(ctx)) {
|
|
1165
|
+
const identifier = behaviorIdentifier(declaration, namespace);
|
|
1166
|
+
const exportName = exportedName(ctx, sf, declaration);
|
|
1167
|
+
try {
|
|
1168
|
+
module.behaviors[identifier] = {
|
|
1169
|
+
Identifier: identifier,
|
|
1170
|
+
Import: `${outputTarget(sf.fileName, discovery)}:${exportName}`,
|
|
1171
|
+
Actions: buildBehaviorActions(ctx, declaration, identifier)
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
catch (error) {
|
|
1175
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
for (const { object, declaration } of discovered) {
|
|
1179
|
+
if (object.section !== "moddas" && object.section !== "beans")
|
|
1180
|
+
continue;
|
|
1181
|
+
const capabilities = buildCapabilities(ctx, declaration);
|
|
1182
|
+
if (capabilities.length > 0)
|
|
1183
|
+
module[object.section][object.name].capabilities = capabilities;
|
|
1184
|
+
}
|
|
1185
|
+
for (const { object, declaration } of discovered) {
|
|
1186
|
+
if (object.section !== "moddas" && object.section !== "beans")
|
|
1187
|
+
continue;
|
|
1188
|
+
const commands = buildCommands(ctx, declaration);
|
|
1189
|
+
if (Object.keys(commands).length > 0)
|
|
1190
|
+
module[object.section][object.name].commands = commands;
|
|
1191
|
+
}
|
|
1192
|
+
for (const { object, declaration, tags } of models) {
|
|
1193
|
+
const entry = module.models[object.name];
|
|
1194
|
+
if (!entry)
|
|
1195
|
+
continue;
|
|
1196
|
+
const events = buildModelEvents(ctx, declaration);
|
|
1197
|
+
if (events)
|
|
1198
|
+
entry.Events = events;
|
|
1199
|
+
const primaryKey = buildModelPrimaryKey(ctx, declaration);
|
|
1200
|
+
if (primaryKey)
|
|
1201
|
+
entry.PrimaryKey = primaryKey;
|
|
1202
|
+
const primaryKeySeparator = buildModelPrimaryKeySeparator(ctx, declaration);
|
|
1203
|
+
if (primaryKeySeparator)
|
|
1204
|
+
entry.PrimaryKeySeparator = primaryKeySeparator;
|
|
1205
|
+
entry.Plural = tags.WebdaPlural || getPlural(object.name.split("/").pop());
|
|
1206
|
+
}
|
|
1207
|
+
return { module, namingViolations, errors };
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Exported `@WebdaBehavior` classes of the project, in walk order.
|
|
1211
|
+
*
|
|
1212
|
+
* The same filter as `searchForWebdaObjects`' `allClasses` for root files:
|
|
1213
|
+
* not a spec, not `@WebdaSchema`, not `@WebdaIgnore`, exported.
|
|
1214
|
+
* @param ctx - analysis context
|
|
1215
|
+
* @returns the behaviour classes
|
|
1216
|
+
*/
|
|
1217
|
+
function behaviorClasses(ctx) {
|
|
1218
|
+
const found = [];
|
|
1219
|
+
const own = new Set(ctx.sourceFiles.map(sf => sf.fileName));
|
|
1220
|
+
for (const fileName of ctx.program.getSourceFileNames()) {
|
|
1221
|
+
if (!own.has(fileName) || fileName.endsWith(".spec.ts"))
|
|
1222
|
+
continue;
|
|
1223
|
+
const sf = ctx.program.getSourceFile(fileName);
|
|
1224
|
+
if (!sf)
|
|
1225
|
+
continue;
|
|
1226
|
+
for (const statement of sf.statements) {
|
|
1227
|
+
const tags = webdaTags(statement);
|
|
1228
|
+
if (tags.WebdaSchema !== undefined)
|
|
1229
|
+
continue;
|
|
1230
|
+
if (!is.isClassDeclaration(statement) || !statement.name)
|
|
1231
|
+
continue;
|
|
1232
|
+
if (tags.WebdaIgnore !== undefined)
|
|
1233
|
+
continue;
|
|
1234
|
+
if (!findTag(statement, "WebdaBehavior"))
|
|
1235
|
+
continue;
|
|
1236
|
+
if (!exportedName(ctx, sf, statement))
|
|
1237
|
+
continue;
|
|
1238
|
+
found.push({ sf, declaration: statement });
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
return found;
|
|
1242
|
+
}
|
|
1243
|
+
//# sourceMappingURL=module.js.map
|