@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.
Files changed (115) hide show
  1. package/LICENSE +166 -0
  2. package/lib/coercions.d.ts +22 -0
  3. package/lib/coercions.d.ts.map +1 -0
  4. package/lib/coercions.js +10 -0
  5. package/lib/coercions.js.map +1 -0
  6. package/lib/context.d.ts +56 -0
  7. package/lib/context.d.ts.map +1 -0
  8. package/lib/context.js +111 -0
  9. package/lib/context.js.map +1 -0
  10. package/lib/defaults.d.ts +31 -0
  11. package/lib/defaults.d.ts.map +1 -0
  12. package/lib/defaults.js +33 -0
  13. package/lib/defaults.js.map +1 -0
  14. package/lib/generators/accessors.d.ts +15 -0
  15. package/lib/generators/accessors.d.ts.map +1 -0
  16. package/lib/generators/accessors.js +567 -0
  17. package/lib/generators/accessors.js.map +1 -0
  18. package/lib/generators/behaviors.d.ts +21 -0
  19. package/lib/generators/behaviors.d.ts.map +1 -0
  20. package/lib/generators/behaviors.js +292 -0
  21. package/lib/generators/behaviors.js.map +1 -0
  22. package/lib/generators/imports.d.ts +86 -0
  23. package/lib/generators/imports.d.ts.map +1 -0
  24. package/lib/generators/imports.js +205 -0
  25. package/lib/generators/imports.js.map +1 -0
  26. package/lib/generators/loadparameters.d.ts +7 -0
  27. package/lib/generators/loadparameters.d.ts.map +1 -0
  28. package/lib/generators/loadparameters.js +175 -0
  29. package/lib/generators/loadparameters.js.map +1 -0
  30. package/lib/generators/qlvalidator.d.ts +53 -0
  31. package/lib/generators/qlvalidator.d.ts.map +1 -0
  32. package/lib/generators/qlvalidator.js +238 -0
  33. package/lib/generators/qlvalidator.js.map +1 -0
  34. package/lib/index.d.ts +53 -0
  35. package/lib/index.d.ts.map +1 -0
  36. package/lib/index.js +36 -0
  37. package/lib/index.js.map +1 -0
  38. package/lib/module-discovery.d.ts +211 -0
  39. package/lib/module-discovery.d.ts.map +1 -0
  40. package/lib/module-discovery.js +662 -0
  41. package/lib/module-discovery.js.map +1 -0
  42. package/lib/module.d.ts +213 -0
  43. package/lib/module.d.ts.map +1 -0
  44. package/lib/module.js +1243 -0
  45. package/lib/module.js.map +1 -0
  46. package/lib/plan.d.ts +101 -0
  47. package/lib/plan.d.ts.map +1 -0
  48. package/lib/plan.js +61 -0
  49. package/lib/plan.js.map +1 -0
  50. package/lib/property-order.d.ts +37 -0
  51. package/lib/property-order.d.ts.map +1 -0
  52. package/lib/property-order.js +141 -0
  53. package/lib/property-order.js.map +1 -0
  54. package/lib/schema/action.d.ts +46 -0
  55. package/lib/schema/action.d.ts.map +1 -0
  56. package/lib/schema/action.js +190 -0
  57. package/lib/schema/action.js.map +1 -0
  58. package/lib/schema/annotations.d.ts +63 -0
  59. package/lib/schema/annotations.d.ts.map +1 -0
  60. package/lib/schema/annotations.js +116 -0
  61. package/lib/schema/annotations.js.map +1 -0
  62. package/lib/schema/converter.d.ts +363 -0
  63. package/lib/schema/converter.d.ts.map +1 -0
  64. package/lib/schema/converter.js +1377 -0
  65. package/lib/schema/converter.js.map +1 -0
  66. package/lib/schema/jsdoc.d.ts +34 -0
  67. package/lib/schema/jsdoc.d.ts.map +1 -0
  68. package/lib/schema/jsdoc.js +206 -0
  69. package/lib/schema/jsdoc.js.map +1 -0
  70. package/lib/schema/model.d.ts +27 -0
  71. package/lib/schema/model.d.ts.map +1 -0
  72. package/lib/schema/model.js +86 -0
  73. package/lib/schema/model.js.map +1 -0
  74. package/lib/schema/project.d.ts +34 -0
  75. package/lib/schema/project.d.ts.map +1 -0
  76. package/lib/schema/project.js +305 -0
  77. package/lib/schema/project.js.map +1 -0
  78. package/lib/schema/service.d.ts +62 -0
  79. package/lib/schema/service.d.ts.map +1 -0
  80. package/lib/schema/service.js +130 -0
  81. package/lib/schema/service.js.map +1 -0
  82. package/lib/schema/types.d.ts +61 -0
  83. package/lib/schema/types.d.ts.map +1 -0
  84. package/lib/schema/types.js +36 -0
  85. package/lib/schema/types.js.map +1 -0
  86. package/lib/schema/worker-cli.d.ts +3 -0
  87. package/lib/schema/worker-cli.d.ts.map +1 -0
  88. package/lib/schema/worker-cli.js +7 -0
  89. package/lib/schema/worker-cli.js.map +1 -0
  90. package/lib/schema/worker.d.ts +81 -0
  91. package/lib/schema/worker.d.ts.map +1 -0
  92. package/lib/schema/worker.js +186 -0
  93. package/lib/schema/worker.js.map +1 -0
  94. package/lib/server.d.ts +2 -0
  95. package/lib/server.d.ts.map +1 -0
  96. package/lib/server.js +133 -0
  97. package/lib/server.js.map +1 -0
  98. package/lib/session.d.ts +84 -0
  99. package/lib/session.d.ts.map +1 -0
  100. package/lib/session.js +141 -0
  101. package/lib/session.js.map +1 -0
  102. package/lib/spans.d.ts +46 -0
  103. package/lib/spans.d.ts.map +1 -0
  104. package/lib/spans.js +64 -0
  105. package/lib/spans.js.map +1 -0
  106. package/lib/twopass.d.ts +45 -0
  107. package/lib/twopass.d.ts.map +1 -0
  108. package/lib/twopass.js +87 -0
  109. package/lib/twopass.js.map +1 -0
  110. package/lib/vite.d.ts +20 -0
  111. package/lib/vite.d.ts.map +1 -0
  112. package/lib/vite.js +41 -0
  113. package/lib/vite.js.map +1 -0
  114. package/package.json +89 -3
  115. 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