@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
@@ -0,0 +1,662 @@
1
+ /**
2
+ * Webda object discovery on the TypeScript 7 API.
3
+ *
4
+ * The first half of `@webda/compiler`'s module generator: find every model,
5
+ * modda, bean and deployer in a project, work out which section it belongs to,
6
+ * and compute the `Import` string recorded in `webda.module.json`.
7
+ *
8
+ * Schema generation is deliberately not here. It runs through `@webda/schema`,
9
+ * which still builds its own language service, and that is a separate port. The
10
+ * point of splitting is that everything in this file can be checked against the
11
+ * committed `webda.module.json` byte for byte, which is stronger evidence than
12
+ * a passing test.
13
+ *
14
+ * Classification follows `module.ts`: the base-type chain is walked and each
15
+ * link is attributed to the npm package that declares it, so `Model` from
16
+ * `@webda/models` is distinguished from any local class of the same name.
17
+ */
18
+ import { existsSync, readFileSync } from "node:fs";
19
+ import { dirname, join, relative } from "node:path";
20
+ import * as is from "typescript/unstable/ast/is";
21
+ import { classPropertiesInTs6Order } from "./property-order.js";
22
+ /** Cache of directory to owning package name. */
23
+ const packageNameCache = new Map();
24
+ /**
25
+ * Name of the npm package that owns a file.
26
+ *
27
+ * Walks up to the nearest `package.json`, matching `getPackageFromType`.
28
+ * @param fileName - file to attribute
29
+ * @returns the package name, when one is found
30
+ */
31
+ export function packageOf(fileName) {
32
+ let folder = dirname(fileName);
33
+ while (folder.length > 2) {
34
+ if (packageNameCache.has(folder))
35
+ return packageNameCache.get(folder);
36
+ const manifest = join(folder, "package.json");
37
+ if (existsSync(manifest)) {
38
+ let name;
39
+ try {
40
+ name = JSON.parse(readFileSync(manifest, "utf8")).name;
41
+ }
42
+ catch {
43
+ name = undefined;
44
+ }
45
+ packageNameCache.set(folder, name);
46
+ return name;
47
+ }
48
+ const parent = dirname(folder);
49
+ if (parent === folder)
50
+ break;
51
+ folder = parent;
52
+ }
53
+ return undefined;
54
+ }
55
+ /**
56
+ * The declared type of a class, and every type it derives from.
57
+ *
58
+ * Each base is re-resolved through its symbol: `getBaseTypes()` returns
59
+ * instantiated references whose own bases are empty, so a generic link
60
+ * otherwise terminates the walk.
61
+ * @param ctx - analysis context
62
+ * @param cls - the class declaration
63
+ * @returns the type chain, nearest first
64
+ */
65
+ export function classTree(ctx, cls) {
66
+ const symbol = cls.name ? ctx.checker.getSymbolAtLocation(cls.name) : undefined;
67
+ const declared = symbol ? ctx.checker.getDeclaredTypeOfSymbol(symbol) : undefined;
68
+ if (!declared)
69
+ return [];
70
+ const chain = [];
71
+ const seen = new Set();
72
+ let current = declared;
73
+ while (current && chain.length < 64) {
74
+ chain.push(current);
75
+ const currentSymbol = current.getSymbol?.() ?? current.symbol;
76
+ const name = currentSymbol?.name;
77
+ if (name) {
78
+ // Keyed by declaration, not name: `class User extends User` (a local
79
+ // model extending `@webda/core`'s) is ordinary, and a name-only guard
80
+ // stops the walk before it reaches Model.
81
+ const declaration = currentSymbol.valueDeclaration ?? currentSymbol.declarations?.[0];
82
+ const owner = declaration?.resolve(ctx.project);
83
+ const file = owner?.getSourceFile?.()?.fileName ?? owner?._sourceFile?.fileName ?? "";
84
+ const key = `${name}@${file}`;
85
+ if (seen.has(key))
86
+ break;
87
+ seen.add(key);
88
+ }
89
+ const resolved = currentSymbol ? (ctx.checker.getDeclaredTypeOfSymbol(currentSymbol) ?? current) : current;
90
+ current = (resolved.getBaseTypes?.() ?? current.getBaseTypes?.() ?? [])[0];
91
+ }
92
+ return chain;
93
+ }
94
+ /**
95
+ * Whether a type chain contains a given class from a given package.
96
+ * @param ctx - analysis context
97
+ * @param chain - the type chain
98
+ * @param packageName - owning npm package
99
+ * @param symbolName - class name
100
+ * @returns true on a match
101
+ */
102
+ export function chainExtends(ctx, chain, packageName, symbolName) {
103
+ for (const type of chain) {
104
+ const symbol = type.getSymbol?.() ?? type.symbol;
105
+ if (symbol?.name !== symbolName)
106
+ continue;
107
+ const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
108
+ const node = declaration?.resolve(ctx.project);
109
+ const owner = node?.getSourceFile?.() ?? node?._sourceFile;
110
+ if (owner?.fileName && packageOf(owner.fileName) === packageName)
111
+ return true;
112
+ }
113
+ return false;
114
+ }
115
+ /**
116
+ * JSDoc tags on a node, by name.
117
+ * @param ctx - analysis context
118
+ * @param sf - containing file
119
+ * @param node - the node
120
+ * @returns tag name to value
121
+ */
122
+ function tagsOf(ctx, sf, node) {
123
+ const tags = {};
124
+ for (const match of ctx.triviaOf(sf, node).matchAll(/@(Webda[A-Za-z]*)\s*([^\s*]*)/g)) {
125
+ tags[match[1]] = match[2] ?? "";
126
+ }
127
+ return tags;
128
+ }
129
+ /**
130
+ * Output path for a source file, without extension.
131
+ *
132
+ * Mirrors `ts.getOutputFileNames` for the common `rootDir`/`outDir` layout.
133
+ * @param fileName - the source file
134
+ * @param options - discovery options
135
+ * @returns path relative to the application root
136
+ */
137
+ export function outputTarget(fileName, options) {
138
+ const fromRoot = relative(options.rootDir, fileName);
139
+ return relative(options.appPath, join(options.outDir, fromRoot)).replace(/\.tsx?$/, "");
140
+ }
141
+ /**
142
+ * The name a class is exported under, if any.
143
+ *
144
+ * Mirrors `getExportedName`: a `default` modifier records `"default"` rather
145
+ * than the class name, and a class with no export modifier may still be
146
+ * re-exported by a later `export { X }` — `webda.module.json` records the alias
147
+ * in both cases, so the `Import` string is wrong without this.
148
+ * @param ctx - analysis context
149
+ * @param sf - containing file
150
+ * @param cls - the class declaration
151
+ * @returns the exported name, or undefined when not exported
152
+ */
153
+ export function exportedName(ctx, sf, cls) {
154
+ const className = cls.name?.text;
155
+ if (!className)
156
+ return undefined;
157
+ const modifiers = cls.modifiers ?? [];
158
+ // 95 === ExportKeyword, 90 === DefaultKeyword
159
+ const hasExport = modifiers.some((modifier) => /^export$/.test(ctx.textOf(sf, modifier)));
160
+ if (hasExport) {
161
+ return modifiers.some((modifier) => /^default$/.test(ctx.textOf(sf, modifier))) ? "default" : className;
162
+ }
163
+ for (const statement of sf.statements) {
164
+ if (!is.isExportDeclaration(statement))
165
+ continue;
166
+ for (const element of statement.exportClause?.elements ?? []) {
167
+ const local = element.propertyName?.text ?? element.name?.text;
168
+ if (local === className)
169
+ return element.name?.text ?? className;
170
+ }
171
+ }
172
+ return undefined;
173
+ }
174
+ /**
175
+ * Discover every Webda object in the project.
176
+ * @param ctx - analysis context
177
+ * @param options - discovery options
178
+ * @returns discovered objects, ordered by section then name
179
+ */
180
+ export function discoverWebdaObjects(ctx, options) {
181
+ const found = [];
182
+ for (const sf of ctx.sourceFiles) {
183
+ if (sf.fileName.endsWith(".spec.ts"))
184
+ continue;
185
+ for (const statement of sf.statements) {
186
+ if (!is.isClassDeclaration(statement))
187
+ continue;
188
+ const cls = statement;
189
+ if (!cls.name)
190
+ continue;
191
+ const chain = classTree(ctx, cls);
192
+ if (!chain.length)
193
+ continue;
194
+ const tags = tagsOf(ctx, sf, cls);
195
+ // Opt-out marker used by test fixtures and deliberately unregistered
196
+ // classes; without it they surface as extra module entries.
197
+ if (tags.WebdaIgnore !== undefined)
198
+ continue;
199
+ let section;
200
+ if (chainExtends(ctx, chain, "@webda/models", "Model")) {
201
+ section = "models";
202
+ }
203
+ else if (tags.WebdaModda !== undefined) {
204
+ if (!chainExtends(ctx, chain, "@webda/core", "Service"))
205
+ continue;
206
+ section = "moddas";
207
+ }
208
+ else if (tags.WebdaDeployer !== undefined) {
209
+ if (!chainExtends(ctx, chain, "@webda/core", "AbstractDeployer"))
210
+ continue;
211
+ section = "deployers";
212
+ }
213
+ else if (chainExtends(ctx, chain, "@webda/core", "Service")) {
214
+ // Beans are Services carrying the @Bean decorator.
215
+ const decorated = cls.modifiers?.some((modifier) => modifier.kind === 171 && /(^|\W)Bean(\W|$)/.test(ctx.textOf(sf, modifier)));
216
+ if (!decorated)
217
+ continue;
218
+ section = "beans";
219
+ }
220
+ else {
221
+ continue;
222
+ }
223
+ const exportName = exportedName(ctx, sf, cls);
224
+ if (!exportName)
225
+ continue;
226
+ const tagName = tags[`Webda${section.slice(0, 1).toUpperCase()}${section.slice(1, section.length - 1)}`] || undefined;
227
+ const bare = tagName && tagName.length ? tagName : cls.name.text;
228
+ const name = bare.includes("/") ? bare : options.namespace ? `${options.namespace}/${bare}` : bare;
229
+ found.push({
230
+ name,
231
+ exportName,
232
+ section,
233
+ importTarget: `${outputTarget(sf.fileName, options)}:${exportName}`,
234
+ fileName: sf.fileName,
235
+ className: cls.name.text,
236
+ baseNames: chain
237
+ .slice(1)
238
+ .map((type) => (type.getSymbol?.() ?? type.symbol)?.name)
239
+ .filter(Boolean),
240
+ primaryKey: primaryKeyOfChain(ctx, sf, cls, chain),
241
+ primaryKeySeparator: primaryKeySeparatorOfChain(ctx, sf, cls, chain)
242
+ });
243
+ }
244
+ }
245
+ found.sort((a, b) => a.section.localeCompare(b.section) || a.name.localeCompare(b.name));
246
+ if (options.keepDuplicates)
247
+ return found;
248
+ // Two classes can resolve to the same namespaced name; `generateWebdaModule`
249
+ // reports it as an error. Keep the first in sorted order so the result does
250
+ // not depend on program file ordering.
251
+ const unique = [];
252
+ const claimed = new Set();
253
+ for (const object of found) {
254
+ const key = `${object.section}:${object.name}`;
255
+ if (claimed.has(key))
256
+ continue;
257
+ claimed.add(key);
258
+ unique.push(object);
259
+ }
260
+ return unique;
261
+ }
262
+ /**
263
+ * Primary key attributes declared on a model.
264
+ *
265
+ * Recognises the two forms `@webda/core` uses: a `readonly ["uuid"]` type
266
+ * annotation and an `= ["uuid"] as const` initialiser, both keyed by the
267
+ * `WEBDA_PRIMARY_KEY` symbol.
268
+ * @param ctx - analysis context
269
+ * @param sf - containing file
270
+ * @param cls - the class declaration
271
+ * @returns key attribute names
272
+ */
273
+ function primaryKeyOf(ctx, sf, cls) {
274
+ for (const member of cls.members ?? []) {
275
+ const name = member.name;
276
+ if (!name)
277
+ continue;
278
+ const nameText = ctx.textOf(sf, name);
279
+ if (!/WEBDA_PRIMARY_KEY/.test(nameText) || /WEBDA_PRIMARY_KEY_SEPARATOR/.test(nameText))
280
+ continue;
281
+ const text = ctx.textOf(sf, member);
282
+ const keys = [...text.matchAll(/["']([A-Za-z_$][\w$]*)["']/g)].map(match => match[1]);
283
+ if (keys.length)
284
+ return keys;
285
+ }
286
+ return [];
287
+ }
288
+ /**
289
+ * Primary key separator declared on the class itself
290
+ * @param ctx - analysis context
291
+ * @param sf - file declaring the class
292
+ * @param cls - the class declaration
293
+ * @returns the separator, or undefined when the class declares none
294
+ */
295
+ function primaryKeySeparatorOf(ctx, sf, cls) {
296
+ for (const member of cls.members ?? []) {
297
+ const name = member.name;
298
+ if (!name || !/WEBDA_PRIMARY_KEY_SEPARATOR/.test(ctx.textOf(sf, name)))
299
+ continue;
300
+ const match = /=\s*["']([^"']+)["']/.exec(ctx.textOf(sf, member));
301
+ if (match)
302
+ return match[1];
303
+ }
304
+ return undefined;
305
+ }
306
+ /**
307
+ * Walk a class and its ancestors, nearest first, returning the first value the extractor finds.
308
+ * @param ctx - analysis context
309
+ * @param sf - file declaring the class
310
+ * @param cls - the class declaration
311
+ * @param chain - the class's type chain, nearest first
312
+ * @param extract - reads a value off one class declaration
313
+ * @param empty - result when no class in the chain has a value
314
+ * @returns the first value found, or `empty`
315
+ */
316
+ function inheritedFromChain(ctx, sf, cls, chain, extract, empty) {
317
+ const own = extract(ctx, sf, cls);
318
+ if (own !== undefined && own.length !== 0)
319
+ return own;
320
+ for (const type of chain.slice(1)) {
321
+ const symbol = type.getSymbol?.() ?? type.symbol;
322
+ const declaration = symbol?.valueDeclaration ?? symbol?.declarations?.[0];
323
+ const node = declaration?.resolve(ctx.project);
324
+ if (!node)
325
+ continue;
326
+ const owner = node.getSourceFile?.() ?? node._sourceFile;
327
+ if (!owner)
328
+ continue;
329
+ const inherited = extract(ctx, owner, node);
330
+ if (inherited !== undefined && inherited.length !== 0)
331
+ return inherited;
332
+ }
333
+ return empty;
334
+ }
335
+ /**
336
+ * Primary key for a class, inherited when it declares none.
337
+ *
338
+ * `UuidModel` declares `["uuid"]` and every subclass inherits it, including
339
+ * across package boundaries — a sample-app model inherits from `@webda/models`.
340
+ * Looking only at the class's own members reports an empty key for all of them.
341
+ * @param ctx - analysis context
342
+ * @param sf - file declaring the class
343
+ * @param cls - the class declaration
344
+ * @param chain - the class's type chain, nearest first
345
+ * @returns key attribute names
346
+ */
347
+ function primaryKeyOfChain(ctx, sf, cls, chain) {
348
+ return inheritedFromChain(ctx, sf, cls, chain, primaryKeyOf, []);
349
+ }
350
+ /**
351
+ * Primary key separator for a class, inherited when it declares none.
352
+ * @param ctx - analysis context
353
+ * @param sf - file declaring the class
354
+ * @param cls - the class declaration
355
+ * @param chain - the class's type chain, nearest first
356
+ * @returns the separator, or undefined when no class in the chain declares one
357
+ */
358
+ function primaryKeySeparatorOfChain(ctx, sf, cls, chain) {
359
+ return inheritedFromChain(ctx, sf, cls, chain, primaryKeySeparatorOf, undefined);
360
+ }
361
+ const packageRootCache = new Map();
362
+ /**
363
+ * Directory of the npm package that owns a file: the nearest folder with a
364
+ * `package.json`.
365
+ * @param fileName - file to attribute
366
+ * @returns the package directory, when one is found
367
+ */
368
+ export function packageRootOf(fileName) {
369
+ let folder = dirname(fileName);
370
+ const visited = [];
371
+ let root;
372
+ while (folder.length > 2) {
373
+ if (packageRootCache.has(folder)) {
374
+ root = packageRootCache.get(folder);
375
+ break;
376
+ }
377
+ visited.push(folder);
378
+ if (existsSync(join(folder, "package.json"))) {
379
+ root = folder;
380
+ break;
381
+ }
382
+ const parent = dirname(folder);
383
+ if (parent === folder)
384
+ break;
385
+ folder = parent;
386
+ }
387
+ for (const folder of visited)
388
+ packageRootCache.set(folder, root);
389
+ return root;
390
+ }
391
+ /** `Import` target → model name, per dependency package directory. */
392
+ const dependencyModelsCache = new Map();
393
+ /**
394
+ * Models a dependency package declares in its `webda.module.json`, keyed by
395
+ * their `Import` target.
396
+ * @param root - the dependency's package directory
397
+ * @returns import target → namespaced model name
398
+ */
399
+ function dependencyModels(root) {
400
+ let models = dependencyModelsCache.get(root);
401
+ if (models)
402
+ return models;
403
+ models = new Map();
404
+ try {
405
+ const module = JSON.parse(readFileSync(join(root, "webda.module.json"), "utf8"));
406
+ for (const [name, entry] of Object.entries(module.models ?? {})) {
407
+ if (typeof entry?.Import === "string")
408
+ models.set(entry.Import, name);
409
+ }
410
+ }
411
+ catch {
412
+ // No module, or not readable: the package declares no models
413
+ }
414
+ dependencyModelsCache.set(root, models);
415
+ return models;
416
+ }
417
+ /**
418
+ * Name of a model declared by a dependency package, as its own
419
+ * `webda.module.json` records it.
420
+ *
421
+ * The class is matched on its `Import` target: the declaring file relative to
422
+ * the package, without extension, then `:` and the export name. A source file
423
+ * (`src/x.ts`) also matches its compiled counterpart (`lib/x`).
424
+ * @param fileName - file declaring the class
425
+ * @param className - exported name of the class
426
+ * @param appPath - the project being compiled, whose own module is skipped
427
+ * @returns the namespaced model name, when the dependency declares one
428
+ */
429
+ export function dependencyModelName(fileName, className, appPath) {
430
+ const root = packageRootOf(fileName);
431
+ if (!root || root === appPath)
432
+ return undefined;
433
+ const models = dependencyModels(root);
434
+ if (!models.size)
435
+ return undefined;
436
+ const path = relative(root, fileName)
437
+ .replace(/\\/g, "/")
438
+ .replace(/(\.d)?\.[cm]?[jt]sx?$/, "");
439
+ const paths = path.startsWith("src/") ? [path, `lib/${path.substring(4)}`] : [path];
440
+ for (const candidate of paths) {
441
+ const name = models.get(`${candidate}:${className}`);
442
+ if (name)
443
+ return name;
444
+ }
445
+ // A default export is recorded as `:default`; only trust it when it is the
446
+ // file's only one.
447
+ const defaults = paths.flatMap(candidate => {
448
+ const name = models.get(`${candidate}:default`);
449
+ return name ? [name] : [];
450
+ });
451
+ return defaults.length === 1 && defaults[0].split("/").pop() === className ? defaults[0] : undefined;
452
+ }
453
+ /**
454
+ * Build the structural half of each model's `webda.module.json` entry.
455
+ *
456
+ * Ancestors and subclasses are resolved among the discovered models, so they
457
+ * carry namespaced names rather than raw class names.
458
+ * @param objects - discovered objects
459
+ * @param plural - pluralisation function, supplied by the host
460
+ * @returns metadata per model name
461
+ */
462
+ export function buildModelMetadata(objects, plural) {
463
+ const models = objects.filter(object => object.section === "models");
464
+ const byClassName = new Map();
465
+ for (const model of models)
466
+ byClassName.set(model.className, model);
467
+ const result = {};
468
+ for (const model of models) {
469
+ const ancestors = [];
470
+ for (const base of model.baseNames) {
471
+ const ancestor = byClassName.get(base);
472
+ // Bases outside the discovered set — `Model` itself, or a class from a
473
+ // dependency — are not module entries and are not recorded.
474
+ if (ancestor && ancestor.name !== model.name)
475
+ ancestors.push(ancestor.name);
476
+ }
477
+ result[model.name] = {
478
+ Identifier: model.name,
479
+ Plural: plural(model.name.split("/").pop() ?? model.name),
480
+ Ancestors: ancestors,
481
+ Subclasses: [],
482
+ PrimaryKey: model.primaryKey,
483
+ ...(model.primaryKeySeparator ? { PrimaryKeySeparator: model.primaryKeySeparator } : {})
484
+ };
485
+ }
486
+ // Subclasses are the inverse of the immediate ancestor relation.
487
+ for (const model of models) {
488
+ const immediate = model.baseNames.map(base => byClassName.get(base)).find(Boolean);
489
+ if (immediate && immediate.name !== model.name) {
490
+ result[immediate.name]?.Subclasses.push(model.name);
491
+ }
492
+ }
493
+ for (const entry of Object.values(result))
494
+ entry.Subclasses.sort();
495
+ return result;
496
+ }
497
+ /**
498
+ * Reflect a model's attributes, as `webda.module.json` records them.
499
+ *
500
+ * The recorded type is the *written* text rather than a resolved type: an
501
+ * inline object literal appears verbatim, comments and all. Generic types are
502
+ * the exception — their arguments are resolved through the checker so a type
503
+ * parameter is reported as its constraint.
504
+ *
505
+ * Properties keyed by a symbol, carrying `@ignore`, or marked not-enumerable
506
+ * are excluded, matching the original.
507
+ * @param ctx - analysis context
508
+ * @param sf - file declaring the class
509
+ * @param cls - the class declaration
510
+ * @returns attribute name to reflected type
511
+ */
512
+ export function reflectAttributes(ctx, sf, cls) {
513
+ const reflection = {};
514
+ // Properties of the *type*, so inherited attributes are included — `uuid`
515
+ // comes from UuidModel and is recorded on every subclass. In TypeScript 6
516
+ // order, which is the key order of the committed artefact.
517
+ for (const property of classPropertiesInTs6Order(ctx, cls)) {
518
+ const name = property.name;
519
+ // Symbol-keyed slots surface as `__@WEBDA_STORAGE@123`. Ordinary
520
+ // double-underscore attributes such as `__password` are real and recorded.
521
+ if (typeof name !== "string" || name.startsWith("__@"))
522
+ continue;
523
+ const declaration = property.valueDeclaration ?? property.declarations?.[0];
524
+ const member = declaration?.resolve(ctx.project);
525
+ if (!member || !is.isPropertyDeclaration(member))
526
+ continue;
527
+ const owner = member.getSourceFile?.() ?? member._sourceFile ?? sf;
528
+ const nameNode = member.name;
529
+ if (nameNode && is.isComputedPropertyName(nameNode))
530
+ continue;
531
+ if (/@ignore\b/.test(ctx.triviaOf(owner, member)))
532
+ continue;
533
+ if (member.modifiers?.some((modifier) => /NotEnumerable/.test(ctx.textOf(owner, modifier))))
534
+ continue;
535
+ const typeNode = member.type;
536
+ if (!typeNode) {
537
+ reflection[name] = { type: "any" };
538
+ continue;
539
+ }
540
+ const typeArguments = typeNode.typeArguments;
541
+ if (is.isTypeReferenceNode(typeNode) && typeArguments?.length) {
542
+ reflection[name] = {
543
+ type: ctx.textOf(owner, typeNode.typeName),
544
+ typeParameters: typeArguments.map((argument) => {
545
+ const type = ctx.checker.getTypeAtLocation(argument);
546
+ return ctx.checker.typeToString(constraintOf(ctx, type) ?? type);
547
+ })
548
+ };
549
+ continue;
550
+ }
551
+ const text = ctx.textOf(owner, typeNode);
552
+ reflection[name] = text.endsWith("[]") ? { type: "Array", typeParameters: [text.slice(0, -2)] } : { type: text };
553
+ }
554
+ return reflection;
555
+ }
556
+ /**
557
+ * Constraint of a type parameter, or undefined for anything else.
558
+ *
559
+ * `getConstraintOfTypeParameter` throws when the type is not a parameter or has
560
+ * no constraint, rather than returning undefined, so it has to be guarded.
561
+ * @param ctx - analysis context
562
+ * @param type - the type to inspect
563
+ * @returns the constraint, when there is one
564
+ */
565
+ function constraintOf(ctx, type) {
566
+ if (!type?.isTypeParameter?.())
567
+ return undefined;
568
+ try {
569
+ return ctx.checker.getConstraintOfTypeParameter?.(type);
570
+ }
571
+ catch {
572
+ return undefined;
573
+ }
574
+ }
575
+ /** `ModelLink*` type names and the link kind each records. */
576
+ const LINK_KINDS = {
577
+ ModelLink: "LINK",
578
+ ModelLinksMap: "LINKS_MAP",
579
+ ModelLinksArray: "LINKS_ARRAY",
580
+ ModelLinksSimpleArray: "LINKS_SIMPLE_ARRAY"
581
+ };
582
+ /**
583
+ * Build a model's `Relations` entry.
584
+ *
585
+ * Relation kinds are recognised by the written type name — `ModelParent`,
586
+ * `ModelRelated`, the `ModelLink*` family — and the referenced model is
587
+ * resolved to its namespaced identifier so the graph is expressed in module
588
+ * terms rather than class names.
589
+ * @param ctx - analysis context
590
+ * @param sf - file declaring the class
591
+ * @param cls - the class declaration
592
+ * @param resolveModelId - maps a class name to its namespaced identifier
593
+ * @returns the relations, omitting empty groups
594
+ */
595
+ export function buildRelations(ctx, sf, cls, resolveModelId) {
596
+ const relations = {};
597
+ for (const property of classPropertiesInTs6Order(ctx, cls)) {
598
+ const attribute = property.name;
599
+ if (typeof attribute !== "string" || attribute.startsWith("__@"))
600
+ continue;
601
+ const declaration = property.valueDeclaration ?? property.declarations?.[0];
602
+ const member = declaration?.resolve(ctx.project);
603
+ if (!member || !is.isPropertyDeclaration(member))
604
+ continue;
605
+ const owner = member.getSourceFile?.() ?? member._sourceFile ?? sf;
606
+ const typeNode = member.type;
607
+ if (!typeNode || !is.isTypeReferenceNode(typeNode))
608
+ continue;
609
+ const typeName = ctx.textOf(owner, typeNode.typeName);
610
+ const typeArguments = typeNode.typeArguments ?? [];
611
+ if (!typeArguments.length)
612
+ continue;
613
+ const targetName = symbolNameOfTypeNode(ctx, typeArguments[0]);
614
+ const model = targetName ? resolveModelId(targetName) : undefined;
615
+ // `ModelLink<T>` on a generic base has no resolvable target. The relation
616
+ // still exists and is recorded, simply without a `model` — dropping it
617
+ // loses the link entirely.
618
+ if (typeName === "ModelParent") {
619
+ if (!model)
620
+ continue;
621
+ relations.parent = { attribute, model };
622
+ }
623
+ else if (typeName === "ModelRelated") {
624
+ if (!model)
625
+ continue;
626
+ relations.queries ??= [];
627
+ relations.queries.push({
628
+ attribute,
629
+ model,
630
+ targetAttribute: ctx.textOf(owner, typeArguments[1]).replace(/"/g, "")
631
+ });
632
+ }
633
+ else if (LINK_KINDS[typeName]) {
634
+ relations.links ??= [];
635
+ relations.links.push({
636
+ attribute,
637
+ ...(model ? { model } : {}),
638
+ type: LINK_KINDS[typeName]
639
+ });
640
+ }
641
+ }
642
+ return relations;
643
+ }
644
+ /**
645
+ * Class name a type node refers to.
646
+ * @param ctx - analysis context
647
+ * @param typeNode - the type node
648
+ * @returns the referenced symbol's name
649
+ */
650
+ function symbolNameOfTypeNode(ctx, typeNode) {
651
+ const type = ctx.checker.getTypeFromTypeNode(typeNode);
652
+ const symbol = type?.getSymbol?.() ?? type?.symbol;
653
+ const name = symbol?.name;
654
+ if (name !== "default")
655
+ return name;
656
+ // `export default class Contact` has the symbol name "default"; the class
657
+ // name is what the module graph is keyed by.
658
+ const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
659
+ const node = declaration?.resolve(ctx.project);
660
+ return node?.name?.text ?? name;
661
+ }
662
+ //# sourceMappingURL=module-discovery.js.map