@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
|
@@ -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
|