@happyvertical/smrt-scanner 0.30.0
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/AGENTS.md +69 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +77 -0
- package/bin/smrt-scan.js +26 -0
- package/dist/chunks/scanner-3K_xuVXN.js +2117 -0
- package/dist/chunks/scanner-3K_xuVXN.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +191 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1408 -0
- package/dist/index.js +163 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +323 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,2117 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import fg from "fast-glob";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { parseSync } from "oxc-parser";
|
|
5
|
+
const FRAMEWORK_BASE_CLASSES = /* @__PURE__ */ new Set([
|
|
6
|
+
"SmrtObject",
|
|
7
|
+
"SmrtClass",
|
|
8
|
+
"SmrtCollection",
|
|
9
|
+
"SmrtJunction",
|
|
10
|
+
"SmrtHierarchical",
|
|
11
|
+
"SmrtPolymorphicAssociation"
|
|
12
|
+
]);
|
|
13
|
+
class InheritanceResolver {
|
|
14
|
+
/** Map of className -> RawClassDefinition */
|
|
15
|
+
classMap = /* @__PURE__ */ new Map();
|
|
16
|
+
/** External package manifests for cross-package resolution */
|
|
17
|
+
externalManifests = /* @__PURE__ */ new Map();
|
|
18
|
+
/** Known base classes (user-provided) */
|
|
19
|
+
knownBaseClasses;
|
|
20
|
+
/** Cache of resolved inheritance chains */
|
|
21
|
+
chainCache = /* @__PURE__ */ new Map();
|
|
22
|
+
/**
|
|
23
|
+
* Create a new `InheritanceResolver`.
|
|
24
|
+
*
|
|
25
|
+
* @param options.baseClasses - Additional class names to treat as known
|
|
26
|
+
* framework base classes (beyond the built-in `SmrtObject`, `SmrtClass`,
|
|
27
|
+
* and `SmrtCollection`).
|
|
28
|
+
* @param options.externalManifests - Pre-loaded external package manifests
|
|
29
|
+
* keyed by package name, used for cross-package parent class resolution.
|
|
30
|
+
*/
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.knownBaseClasses = /* @__PURE__ */ new Set([
|
|
33
|
+
...FRAMEWORK_BASE_CLASSES,
|
|
34
|
+
...options.baseClasses || []
|
|
35
|
+
]);
|
|
36
|
+
this.externalManifests = options.externalManifests || /* @__PURE__ */ new Map();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Register raw class definitions from a scan pass.
|
|
40
|
+
*
|
|
41
|
+
* Adds each class to the internal class map by `className`. Calling this
|
|
42
|
+
* clears the inheritance chain cache so subsequent calls to
|
|
43
|
+
* {@link resolveAll} or {@link resolveInheritanceChain} reflect the new
|
|
44
|
+
* classes.
|
|
45
|
+
*
|
|
46
|
+
* @param classes - Array of {@link RawClassDefinition} objects from
|
|
47
|
+
* {@link ScanResults.classes}.
|
|
48
|
+
*/
|
|
49
|
+
addClasses(classes) {
|
|
50
|
+
for (const classDef of classes) {
|
|
51
|
+
this.classMap.set(classDef.className, classDef);
|
|
52
|
+
}
|
|
53
|
+
this.chainCache.clear();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Register an external package manifest for cross-package base class resolution.
|
|
57
|
+
*
|
|
58
|
+
* Clears the chain cache after registration so re-resolution picks up the
|
|
59
|
+
* new definitions.
|
|
60
|
+
*
|
|
61
|
+
* @param manifest - External package manifest providing class definitions
|
|
62
|
+
* that may appear as base classes in the local project.
|
|
63
|
+
*
|
|
64
|
+
* @see {@link ExternalManifest}
|
|
65
|
+
*/
|
|
66
|
+
addExternalManifest(manifest) {
|
|
67
|
+
this.externalManifests.set(manifest.packageName, manifest);
|
|
68
|
+
this.chainCache.clear();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve all registered classes and return fully-resolved definitions.
|
|
72
|
+
*
|
|
73
|
+
* A class is included in the output if it either:
|
|
74
|
+
* 1. Has an `@smrt()` decorator, or
|
|
75
|
+
* 2. Directly or transitively extends a framework base class
|
|
76
|
+
* (`SmrtObject`, `SmrtClass`, `SmrtCollection`) — this captures
|
|
77
|
+
* collection classes such as `class MeetingCollection extends
|
|
78
|
+
* SmrtCollection<Meeting>` that do not carry `@smrt()` themselves.
|
|
79
|
+
*
|
|
80
|
+
* @returns An array of {@link ResolvedClassDefinition} — one entry per
|
|
81
|
+
* eligible class, with inheritance chain, STI metadata, and merged fields
|
|
82
|
+
* populated.
|
|
83
|
+
*
|
|
84
|
+
* @see {@link resolve} to resolve a single class definition.
|
|
85
|
+
*/
|
|
86
|
+
resolveAll() {
|
|
87
|
+
const resolved = [];
|
|
88
|
+
for (const classDef of this.classMap.values()) {
|
|
89
|
+
const extendsFrameworkBase = this.extendsFrameworkBase(classDef);
|
|
90
|
+
if (!classDef.hasSmartDecorator && !extendsFrameworkBase) continue;
|
|
91
|
+
const resolvedClass = this.resolve(classDef);
|
|
92
|
+
resolved.push(resolvedClass);
|
|
93
|
+
}
|
|
94
|
+
return resolved;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Check if a class extends a framework base class
|
|
98
|
+
* (SmrtObject, SmrtClass, or SmrtCollection)
|
|
99
|
+
*/
|
|
100
|
+
extendsFrameworkBase(classDef) {
|
|
101
|
+
if (classDef.extendsClause && this.knownBaseClasses.has(classDef.extendsClause)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
const chain = this.resolveInheritanceChain(classDef.className);
|
|
105
|
+
return chain.some((className) => this.knownBaseClasses.has(className));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Resolve a single raw class definition into a fully-resolved definition.
|
|
109
|
+
*
|
|
110
|
+
* Computes the inheritance chain, determines the effective table strategy,
|
|
111
|
+
* detects STI membership, and merges ancestor fields for STI classes.
|
|
112
|
+
*
|
|
113
|
+
* @param classDef - The raw class definition to resolve.
|
|
114
|
+
* @returns A {@link ResolvedClassDefinition} with all inherited metadata
|
|
115
|
+
* applied. The `packageName` field is left as `null` and must be set by
|
|
116
|
+
* the caller (e.g. {@link ManifestAdapter}).
|
|
117
|
+
*
|
|
118
|
+
* @see {@link resolveAll} to resolve every registered class at once.
|
|
119
|
+
*/
|
|
120
|
+
resolve(classDef) {
|
|
121
|
+
const inheritanceChain = this.resolveInheritanceChain(classDef.className);
|
|
122
|
+
const stiBase = this.findSTIBase(inheritanceChain);
|
|
123
|
+
const effectiveTableStrategy = this.determineTableStrategy(
|
|
124
|
+
classDef,
|
|
125
|
+
inheritanceChain
|
|
126
|
+
);
|
|
127
|
+
const isFrameworkBase = this.knownBaseClasses.has(classDef.className);
|
|
128
|
+
const isSTI = effectiveTableStrategy === "sti";
|
|
129
|
+
const allFields = isSTI ? this.mergeFieldsForSTI(inheritanceChain) : classDef.fields;
|
|
130
|
+
return {
|
|
131
|
+
...classDef,
|
|
132
|
+
inheritanceChain,
|
|
133
|
+
stiBase,
|
|
134
|
+
effectiveTableStrategy,
|
|
135
|
+
isSTI,
|
|
136
|
+
isFrameworkBase,
|
|
137
|
+
allFields,
|
|
138
|
+
packageName: null
|
|
139
|
+
// Will be set by manifest adapter
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Resolve the full inheritance chain for a named class, from the root base
|
|
144
|
+
* class down to the named class itself.
|
|
145
|
+
*
|
|
146
|
+
* Results are memoised in an internal cache that is cleared whenever
|
|
147
|
+
* {@link addClasses} or {@link addExternalManifest} is called.
|
|
148
|
+
*
|
|
149
|
+
* @param className - Name of the class to resolve.
|
|
150
|
+
* @returns An ordered array of class names starting from the furthest
|
|
151
|
+
* ancestor and ending with `className`.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```typescript
|
|
155
|
+
* // Given: class Article extends Content, class Content extends SmrtObject
|
|
156
|
+
* resolver.resolveInheritanceChain('Article');
|
|
157
|
+
* // => ['SmrtObject', 'Content', 'Article']
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
resolveInheritanceChain(className) {
|
|
161
|
+
const cached = this.chainCache.get(className);
|
|
162
|
+
if (cached) return cached;
|
|
163
|
+
const chain = [];
|
|
164
|
+
const visited = /* @__PURE__ */ new Set();
|
|
165
|
+
let current = className;
|
|
166
|
+
while (current && !visited.has(current)) {
|
|
167
|
+
visited.add(current);
|
|
168
|
+
chain.unshift(current);
|
|
169
|
+
if (this.knownBaseClasses.has(current)) {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
const classDef = this.findClassDefinition(current);
|
|
173
|
+
current = classDef?.extendsClause || null;
|
|
174
|
+
}
|
|
175
|
+
this.chainCache.set(className, chain);
|
|
176
|
+
return chain;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Look up a class definition by name, searching in priority order:
|
|
180
|
+
* 1. Local classes added via {@link addClasses}.
|
|
181
|
+
* 2. External package manifests added via {@link addExternalManifest}.
|
|
182
|
+
* 3. Built-in framework base classes (`SmrtObject`, `SmrtClass`,
|
|
183
|
+
* `SmrtCollection`) — returns a minimal stub definition so chain walking
|
|
184
|
+
* can terminate cleanly.
|
|
185
|
+
*
|
|
186
|
+
* @param className - Class name to look up.
|
|
187
|
+
* @returns The {@link RawClassDefinition} if found, or `null` if the class
|
|
188
|
+
* is unknown to the resolver.
|
|
189
|
+
*/
|
|
190
|
+
findClassDefinition(className) {
|
|
191
|
+
const local = this.classMap.get(className);
|
|
192
|
+
if (local) return local;
|
|
193
|
+
for (const manifest of this.externalManifests.values()) {
|
|
194
|
+
const external = manifest.classes.get(className);
|
|
195
|
+
if (external) return external;
|
|
196
|
+
}
|
|
197
|
+
if (this.knownBaseClasses.has(className)) {
|
|
198
|
+
return {
|
|
199
|
+
className,
|
|
200
|
+
filePath: "",
|
|
201
|
+
extendsClause: null,
|
|
202
|
+
extendsTypeArg: null,
|
|
203
|
+
decoratorConfig: null,
|
|
204
|
+
hasSmartDecorator: false,
|
|
205
|
+
fields: [],
|
|
206
|
+
methods: [],
|
|
207
|
+
startLine: 0,
|
|
208
|
+
endLine: 0
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Find the STI root class in a resolved inheritance chain.
|
|
215
|
+
*
|
|
216
|
+
* Walks the chain from base to leaf and returns the name of the first class
|
|
217
|
+
* whose `@smrt()` decorator explicitly declares `tableStrategy: 'sti'`.
|
|
218
|
+
*
|
|
219
|
+
* @param chain - Ordered inheritance chain (base → leaf) as returned by
|
|
220
|
+
* {@link resolveInheritanceChain}.
|
|
221
|
+
* @returns The class name of the STI root, or `null` if no class in the
|
|
222
|
+
* chain uses `tableStrategy: 'sti'`.
|
|
223
|
+
*/
|
|
224
|
+
findSTIBase(chain) {
|
|
225
|
+
for (const className of chain) {
|
|
226
|
+
const classDef = this.findClassDefinition(className);
|
|
227
|
+
if (classDef?.decoratorConfig?.tableStrategy === "sti") {
|
|
228
|
+
return className;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Determine the effective table strategy (`'sti'` or `'cti'`) for a class.
|
|
235
|
+
*
|
|
236
|
+
* Resolution order:
|
|
237
|
+
* 1. The class's own `@smrt({ tableStrategy })` declaration, if present.
|
|
238
|
+
* 2. The nearest ancestor that declares `tableStrategy: 'sti'` — STI is
|
|
239
|
+
* inherited automatically by all subclasses.
|
|
240
|
+
* 3. Defaults to `'cti'` if no STI ancestor is found.
|
|
241
|
+
*
|
|
242
|
+
* @param classDef - Raw class definition whose strategy is being determined.
|
|
243
|
+
* @param chain - Pre-resolved inheritance chain for `classDef` (base → leaf).
|
|
244
|
+
* @returns `'sti'` or `'cti'`.
|
|
245
|
+
*/
|
|
246
|
+
determineTableStrategy(classDef, chain) {
|
|
247
|
+
if (classDef.decoratorConfig?.tableStrategy) {
|
|
248
|
+
return classDef.decoratorConfig.tableStrategy;
|
|
249
|
+
}
|
|
250
|
+
for (const className of chain) {
|
|
251
|
+
if (className === classDef.className) continue;
|
|
252
|
+
const ancestorDef = this.findClassDefinition(className);
|
|
253
|
+
if (ancestorDef?.decoratorConfig?.tableStrategy === "sti") {
|
|
254
|
+
return "sti";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return "cti";
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Merge fields from all classes in an STI inheritance chain.
|
|
261
|
+
*
|
|
262
|
+
* Iterates from the root base class to the leaf class so that base class
|
|
263
|
+
* fields appear first in the returned array. If a field name is declared in
|
|
264
|
+
* both an ancestor and a descendant, the ancestor's definition takes
|
|
265
|
+
* precedence (first-seen wins), preserving the base-class column layout.
|
|
266
|
+
*
|
|
267
|
+
* @param chain - Ordered inheritance chain (base → leaf) as returned by
|
|
268
|
+
* {@link resolveInheritanceChain}.
|
|
269
|
+
* @returns A deduplicated, ordered array of {@link RawFieldDefinition}
|
|
270
|
+
* covering every field in the STI hierarchy.
|
|
271
|
+
*/
|
|
272
|
+
mergeFieldsForSTI(chain) {
|
|
273
|
+
const allFields = [];
|
|
274
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
275
|
+
for (const className of chain) {
|
|
276
|
+
const classDef = this.findClassDefinition(className);
|
|
277
|
+
if (!classDef) continue;
|
|
278
|
+
for (const field of classDef.fields) {
|
|
279
|
+
if (seenNames.has(field.name)) continue;
|
|
280
|
+
seenNames.add(field.name);
|
|
281
|
+
allFields.push(field);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return allFields;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Return all known descendants of a class.
|
|
288
|
+
*
|
|
289
|
+
* Useful for STI schema generation where the base table must accommodate
|
|
290
|
+
* columns from every subclass.
|
|
291
|
+
*
|
|
292
|
+
* @param className - The ancestor class name to search from.
|
|
293
|
+
* @returns An array of class names (local classes only) whose resolved
|
|
294
|
+
* inheritance chain includes `className`. Does not include `className`
|
|
295
|
+
* itself.
|
|
296
|
+
*/
|
|
297
|
+
getDescendants(className) {
|
|
298
|
+
const descendants = [];
|
|
299
|
+
for (const [name] of this.classMap) {
|
|
300
|
+
if (name === className) continue;
|
|
301
|
+
const chain = this.resolveInheritanceChain(name);
|
|
302
|
+
if (chain.includes(className)) {
|
|
303
|
+
descendants.push(name);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return descendants;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Check whether a class participates in an STI hierarchy.
|
|
310
|
+
*
|
|
311
|
+
* @param className - Name of the class to check.
|
|
312
|
+
* @returns `true` if any class in the resolved inheritance chain declares
|
|
313
|
+
* `tableStrategy: 'sti'`, `false` otherwise.
|
|
314
|
+
*/
|
|
315
|
+
isSTIClass(className) {
|
|
316
|
+
const chain = this.resolveInheritanceChain(className);
|
|
317
|
+
return this.findSTIBase(chain) !== null;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Return aggregate statistics about the classes registered with this resolver.
|
|
321
|
+
*
|
|
322
|
+
* @returns An object with:
|
|
323
|
+
* - `totalClasses` — total number of classes in the class map.
|
|
324
|
+
* - `smrtClasses` — classes that carry `@smrt()`.
|
|
325
|
+
* - `stiClasses` — `@smrt()` classes in an STI hierarchy.
|
|
326
|
+
* - `maxInheritanceDepth` — length of the deepest inheritance chain among
|
|
327
|
+
* `@smrt()` classes.
|
|
328
|
+
*/
|
|
329
|
+
getStats() {
|
|
330
|
+
let smrtClasses = 0;
|
|
331
|
+
let stiClasses = 0;
|
|
332
|
+
let maxInheritanceDepth = 0;
|
|
333
|
+
for (const classDef of this.classMap.values()) {
|
|
334
|
+
if (classDef.hasSmartDecorator) {
|
|
335
|
+
smrtClasses++;
|
|
336
|
+
const chain = this.resolveInheritanceChain(classDef.className);
|
|
337
|
+
maxInheritanceDepth = Math.max(maxInheritanceDepth, chain.length);
|
|
338
|
+
if (this.findSTIBase(chain)) {
|
|
339
|
+
stiClasses++;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
totalClasses: this.classMap.size,
|
|
345
|
+
smrtClasses,
|
|
346
|
+
stiClasses,
|
|
347
|
+
maxInheritanceDepth
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function getLangFromFilename(filename) {
|
|
352
|
+
if (filename.endsWith(".tsx")) return "tsx";
|
|
353
|
+
if (filename.endsWith(".ts")) return "ts";
|
|
354
|
+
if (filename.endsWith(".jsx")) return "jsx";
|
|
355
|
+
return "js";
|
|
356
|
+
}
|
|
357
|
+
function getLineColumn(sourceText, offset) {
|
|
358
|
+
if (offset < 0 || offset > sourceText.length) {
|
|
359
|
+
return void 0;
|
|
360
|
+
}
|
|
361
|
+
let line = 1;
|
|
362
|
+
let lastNewlinePos = -1;
|
|
363
|
+
for (let i = 0; i < offset; i++) {
|
|
364
|
+
if (sourceText[i] === "\n") {
|
|
365
|
+
line++;
|
|
366
|
+
lastNewlinePos = i;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
line,
|
|
371
|
+
column: offset - lastNewlinePos
|
|
372
|
+
// 1-based column
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function getRange(node) {
|
|
376
|
+
if (node.range) return node.range;
|
|
377
|
+
if (node.start !== void 0 && node.end !== void 0)
|
|
378
|
+
return [node.start, node.end];
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
function sliceSource(node, sourceText) {
|
|
382
|
+
const range = getRange(node);
|
|
383
|
+
return range ? sourceText.slice(range[0], range[1]) : null;
|
|
384
|
+
}
|
|
385
|
+
function parseFile(filePath) {
|
|
386
|
+
const startTime = performance.now();
|
|
387
|
+
const errors = [];
|
|
388
|
+
const classes = [];
|
|
389
|
+
let typeAliases = {};
|
|
390
|
+
let smrtImports;
|
|
391
|
+
try {
|
|
392
|
+
const sourceText = readFileSync(filePath, "utf-8");
|
|
393
|
+
const result = parseSync(filePath, sourceText, {
|
|
394
|
+
lang: getLangFromFilename(filePath),
|
|
395
|
+
preserveParens: false
|
|
396
|
+
});
|
|
397
|
+
if (result.errors && result.errors.length > 0) {
|
|
398
|
+
for (const error of result.errors) {
|
|
399
|
+
const loc = error.labels?.[0] ? getLineColumn(sourceText, error.labels[0].start) : void 0;
|
|
400
|
+
errors.push({
|
|
401
|
+
message: error.message || "Parse error",
|
|
402
|
+
filePath,
|
|
403
|
+
line: loc?.line,
|
|
404
|
+
column: loc?.column,
|
|
405
|
+
severity: error.severity === "Error" ? "error" : "warning"
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const program = result.program;
|
|
410
|
+
if (program?.body) {
|
|
411
|
+
const importAliases = extractImportAliases(program.body);
|
|
412
|
+
typeAliases = extractTypeAliases(program.body);
|
|
413
|
+
smrtImports = extractSmrtImports(program.body);
|
|
414
|
+
for (const node of program.body) {
|
|
415
|
+
const extracted = extractClassFromNode(
|
|
416
|
+
node,
|
|
417
|
+
filePath,
|
|
418
|
+
sourceText,
|
|
419
|
+
importAliases
|
|
420
|
+
);
|
|
421
|
+
if (extracted) {
|
|
422
|
+
classes.push(extracted);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} catch (error) {
|
|
427
|
+
errors.push({
|
|
428
|
+
message: error instanceof Error ? error.message : String(error),
|
|
429
|
+
filePath,
|
|
430
|
+
severity: "error"
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
const result2 = {
|
|
434
|
+
filePath,
|
|
435
|
+
classes,
|
|
436
|
+
errors,
|
|
437
|
+
parseTimeMs: performance.now() - startTime,
|
|
438
|
+
typeAliases
|
|
439
|
+
};
|
|
440
|
+
if (smrtImports && smrtImports.size > 0) {
|
|
441
|
+
result2.smrtImports = smrtImports;
|
|
442
|
+
}
|
|
443
|
+
return result2;
|
|
444
|
+
}
|
|
445
|
+
function parseSource(sourceText, filename = "test.ts") {
|
|
446
|
+
const startTime = performance.now();
|
|
447
|
+
const errors = [];
|
|
448
|
+
const classes = [];
|
|
449
|
+
let typeAliases = {};
|
|
450
|
+
let smrtImports;
|
|
451
|
+
try {
|
|
452
|
+
const result = parseSync(filename, sourceText, {
|
|
453
|
+
lang: getLangFromFilename(filename),
|
|
454
|
+
preserveParens: false
|
|
455
|
+
});
|
|
456
|
+
if (result.errors && result.errors.length > 0) {
|
|
457
|
+
for (const error of result.errors) {
|
|
458
|
+
const loc = error.labels?.[0] ? getLineColumn(sourceText, error.labels[0].start) : void 0;
|
|
459
|
+
errors.push({
|
|
460
|
+
message: error.message || "Parse error",
|
|
461
|
+
filePath: filename,
|
|
462
|
+
line: loc?.line,
|
|
463
|
+
column: loc?.column,
|
|
464
|
+
severity: error.severity === "Error" ? "error" : "warning"
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const program = result.program;
|
|
469
|
+
if (program?.body) {
|
|
470
|
+
const importAliases = extractImportAliases(program.body);
|
|
471
|
+
typeAliases = extractTypeAliases(program.body);
|
|
472
|
+
smrtImports = extractSmrtImports(program.body);
|
|
473
|
+
for (const node of program.body) {
|
|
474
|
+
const extracted = extractClassFromNode(
|
|
475
|
+
node,
|
|
476
|
+
filename,
|
|
477
|
+
sourceText,
|
|
478
|
+
importAliases
|
|
479
|
+
);
|
|
480
|
+
if (extracted) {
|
|
481
|
+
classes.push(extracted);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
} catch (error) {
|
|
486
|
+
errors.push({
|
|
487
|
+
message: error instanceof Error ? error.message : String(error),
|
|
488
|
+
filePath: filename,
|
|
489
|
+
severity: "error"
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
const result2 = {
|
|
493
|
+
filePath: filename,
|
|
494
|
+
classes,
|
|
495
|
+
errors,
|
|
496
|
+
parseTimeMs: performance.now() - startTime,
|
|
497
|
+
typeAliases
|
|
498
|
+
};
|
|
499
|
+
if (smrtImports && smrtImports.size > 0) {
|
|
500
|
+
result2.smrtImports = smrtImports;
|
|
501
|
+
}
|
|
502
|
+
return result2;
|
|
503
|
+
}
|
|
504
|
+
const FORBIDDEN_OBJECT_KEYS = /* @__PURE__ */ new Set([
|
|
505
|
+
"__proto__",
|
|
506
|
+
"constructor",
|
|
507
|
+
"prototype"
|
|
508
|
+
]);
|
|
509
|
+
function isSafeObjectKey(key) {
|
|
510
|
+
return !FORBIDDEN_OBJECT_KEYS.has(key);
|
|
511
|
+
}
|
|
512
|
+
function extractImportAliases(body) {
|
|
513
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
514
|
+
for (const node of body) {
|
|
515
|
+
if (node.type === "ImportDeclaration" && node.specifiers) {
|
|
516
|
+
for (const spec of node.specifiers) {
|
|
517
|
+
if (spec.type === "ImportSpecifier" && spec.imported && spec.local) {
|
|
518
|
+
const original = spec.imported.name;
|
|
519
|
+
const local = spec.local.name;
|
|
520
|
+
if (original !== local) {
|
|
521
|
+
aliases.set(local, original);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return aliases;
|
|
528
|
+
}
|
|
529
|
+
function extractSmrtImports(body) {
|
|
530
|
+
const imports = /* @__PURE__ */ new Map();
|
|
531
|
+
for (const node of body) {
|
|
532
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
533
|
+
const source = node.source;
|
|
534
|
+
if (!source || !source.value) continue;
|
|
535
|
+
const moduleName = source.value;
|
|
536
|
+
if (!moduleName.startsWith("@happyvertical/smrt-")) continue;
|
|
537
|
+
if (!imports.has(moduleName)) {
|
|
538
|
+
imports.set(moduleName, /* @__PURE__ */ new Set());
|
|
539
|
+
}
|
|
540
|
+
const classSet = imports.get(moduleName);
|
|
541
|
+
if (!node.specifiers || node.specifiers.length === 0) {
|
|
542
|
+
classSet.add("*");
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
for (const spec of node.specifiers) {
|
|
546
|
+
if (spec.type === "ImportSpecifier" && spec.imported && spec.local) {
|
|
547
|
+
const importedName = spec.imported.name;
|
|
548
|
+
if (/^[A-Z][A-Za-z0-9]*$/.test(importedName)) {
|
|
549
|
+
classSet.add(importedName);
|
|
550
|
+
}
|
|
551
|
+
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
552
|
+
classSet.add("*");
|
|
553
|
+
} else if (spec.type === "ImportDefaultSpecifier" && spec.local) {
|
|
554
|
+
const defaultName = spec.local.name;
|
|
555
|
+
if (/^[A-Z][A-Za-z0-9]*$/.test(defaultName)) {
|
|
556
|
+
classSet.add(defaultName);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return imports;
|
|
562
|
+
}
|
|
563
|
+
function extractTypeAliases(body) {
|
|
564
|
+
const aliases = {};
|
|
565
|
+
for (const node of body) {
|
|
566
|
+
if (node.type === "TSTypeAliasDeclaration") {
|
|
567
|
+
const name = node.id?.name;
|
|
568
|
+
const resolved = node.typeAnnotation ? extractTypeName(node.typeAnnotation) : null;
|
|
569
|
+
if (name && resolved && isSafeObjectKey(name)) aliases[name] = resolved;
|
|
570
|
+
}
|
|
571
|
+
if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "TSTypeAliasDeclaration") {
|
|
572
|
+
const decl = node.declaration;
|
|
573
|
+
const name = decl.id?.name;
|
|
574
|
+
const resolved = decl.typeAnnotation ? extractTypeName(decl.typeAnnotation) : null;
|
|
575
|
+
if (name && resolved && isSafeObjectKey(name)) aliases[name] = resolved;
|
|
576
|
+
}
|
|
577
|
+
const enumDecl = node.type === "TSEnumDeclaration" ? node : node.type === "ExportNamedDeclaration" && node.declaration?.type === "TSEnumDeclaration" ? node.declaration : null;
|
|
578
|
+
if (enumDecl) {
|
|
579
|
+
const name = enumDecl.id?.name;
|
|
580
|
+
const members = enumDecl.body?.members ?? enumDecl.members;
|
|
581
|
+
if (name && isSafeObjectKey(name) && members?.length > 0) {
|
|
582
|
+
const values = members.map((m) => {
|
|
583
|
+
if (m.initializer?.type === "Literal") {
|
|
584
|
+
const val = m.initializer.value;
|
|
585
|
+
if (typeof val === "string") return `'${val}'`;
|
|
586
|
+
if (typeof val === "number") return String(val);
|
|
587
|
+
}
|
|
588
|
+
return null;
|
|
589
|
+
}).filter(Boolean);
|
|
590
|
+
if (values.length > 0) {
|
|
591
|
+
const allStrings = values.every((v) => v.startsWith("'"));
|
|
592
|
+
if (allStrings) {
|
|
593
|
+
aliases[name] = values.join(" | ");
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return aliases;
|
|
600
|
+
}
|
|
601
|
+
function extractClassFromNode(node, filePath, sourceText, importAliases) {
|
|
602
|
+
if (node.type === "ExportNamedDeclaration" && node.declaration) {
|
|
603
|
+
return extractClassFromNode(
|
|
604
|
+
node.declaration,
|
|
605
|
+
filePath,
|
|
606
|
+
sourceText,
|
|
607
|
+
importAliases
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
if (node.type === "ExportDefaultDeclaration" && node.declaration) {
|
|
611
|
+
return extractClassFromNode(
|
|
612
|
+
node.declaration,
|
|
613
|
+
filePath,
|
|
614
|
+
sourceText,
|
|
615
|
+
importAliases
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
if (node.type === "ClassDeclaration") {
|
|
619
|
+
return extractClassDeclaration(node, filePath, sourceText, importAliases);
|
|
620
|
+
}
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
function extractClassDeclaration(node, filePath, sourceText, importAliases) {
|
|
624
|
+
const className = node.id?.name || "AnonymousClass";
|
|
625
|
+
const decorators = node.decorators || [];
|
|
626
|
+
const smrtDecorator = decorators.find((d) => isSmrtDecorator(d));
|
|
627
|
+
const tenantScopedDecorator = decorators.find(
|
|
628
|
+
(d) => isNamedDecorator(d, "TenantScoped")
|
|
629
|
+
);
|
|
630
|
+
const hasSmartDecorator = !!smrtDecorator;
|
|
631
|
+
const smrtConfig = smrtDecorator ? extractDecoratorConfig(smrtDecorator, sourceText) : null;
|
|
632
|
+
const decoratorConfig = tenantScopedDecorator ? {
|
|
633
|
+
...smrtConfig ?? {},
|
|
634
|
+
tenantScoped: extractDecoratorConfig(tenantScopedDecorator, sourceText)
|
|
635
|
+
} : smrtConfig;
|
|
636
|
+
const { extendsClause, extendsTypeArg } = extractExtendsClause(
|
|
637
|
+
node,
|
|
638
|
+
importAliases
|
|
639
|
+
);
|
|
640
|
+
const fields = [];
|
|
641
|
+
const methods = [];
|
|
642
|
+
for (const member of node.body.body) {
|
|
643
|
+
if (member.type === "PropertyDefinition") {
|
|
644
|
+
const field = extractPropertyDefinition(member, sourceText);
|
|
645
|
+
if (field) {
|
|
646
|
+
fields.push(field);
|
|
647
|
+
}
|
|
648
|
+
} else if (member.type === "MethodDefinition") {
|
|
649
|
+
const method = extractMethodDefinition(member, sourceText);
|
|
650
|
+
if (method) {
|
|
651
|
+
methods.push(method);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return {
|
|
656
|
+
className,
|
|
657
|
+
filePath,
|
|
658
|
+
extendsClause,
|
|
659
|
+
extendsTypeArg,
|
|
660
|
+
decoratorConfig,
|
|
661
|
+
hasSmartDecorator,
|
|
662
|
+
fields,
|
|
663
|
+
methods,
|
|
664
|
+
startLine: node.loc?.start.line || 1,
|
|
665
|
+
endLine: node.loc?.end.line || 1
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function isSmrtDecorator(decorator) {
|
|
669
|
+
return isNamedDecorator(decorator, "smrt");
|
|
670
|
+
}
|
|
671
|
+
function isNamedDecorator(decorator, name) {
|
|
672
|
+
const expr = decorator.expression;
|
|
673
|
+
if (expr.type === "CallExpression") {
|
|
674
|
+
const callee = expr.callee;
|
|
675
|
+
if (callee.type === "Identifier" && callee.name === name) {
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (expr.type === "Identifier" && expr.name === name) {
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
function extractDecoratorConfig(decorator, sourceText) {
|
|
685
|
+
const expr = decorator.expression;
|
|
686
|
+
if (expr.type === "CallExpression" && expr.arguments.length > 0) {
|
|
687
|
+
const arg = expr.arguments[0];
|
|
688
|
+
if (arg.type === "ObjectExpression") {
|
|
689
|
+
return extractObjectLiteral(arg, sourceText);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return {};
|
|
693
|
+
}
|
|
694
|
+
function extractObjectLiteral(node, sourceText) {
|
|
695
|
+
const result = {};
|
|
696
|
+
for (const prop of node.properties) {
|
|
697
|
+
if (prop.type === "Property" && !prop.computed) {
|
|
698
|
+
const key = getPropertyKey(prop.key);
|
|
699
|
+
if (key && isSafeObjectKey(key)) {
|
|
700
|
+
result[key] = extractValue(prop.value, sourceText);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return result;
|
|
705
|
+
}
|
|
706
|
+
function getPropertyKey(node) {
|
|
707
|
+
if (node.type === "Identifier") {
|
|
708
|
+
return node.name;
|
|
709
|
+
}
|
|
710
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
711
|
+
return node.value;
|
|
712
|
+
}
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
function extractValue(node, sourceText) {
|
|
716
|
+
switch (node.type) {
|
|
717
|
+
case "Literal":
|
|
718
|
+
return node.value;
|
|
719
|
+
case "Identifier":
|
|
720
|
+
if (node.name === "undefined") return void 0;
|
|
721
|
+
if (node.name === "null") return null;
|
|
722
|
+
if (node.name === "true") return true;
|
|
723
|
+
if (node.name === "false") return false;
|
|
724
|
+
return node.name;
|
|
725
|
+
// Return as string for class references
|
|
726
|
+
case "ArrayExpression":
|
|
727
|
+
return node.elements.filter(
|
|
728
|
+
(el) => el !== null && typeof el === "object" && "type" in el && el.type !== "SpreadElement"
|
|
729
|
+
).map((el) => extractValue(el, sourceText));
|
|
730
|
+
case "ObjectExpression":
|
|
731
|
+
return extractObjectLiteral(node, sourceText);
|
|
732
|
+
case "UnaryExpression":
|
|
733
|
+
if (node.operator === "-" && node.argument?.type === "Literal") {
|
|
734
|
+
const value = node.argument.value;
|
|
735
|
+
if (typeof value === "number") {
|
|
736
|
+
return -value;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
break;
|
|
740
|
+
case "CallExpression":
|
|
741
|
+
case "NewExpression": {
|
|
742
|
+
const src = sliceSource(node, sourceText);
|
|
743
|
+
if (src) return src;
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
const rawSrc = sliceSource(node, sourceText);
|
|
748
|
+
if (rawSrc) return rawSrc;
|
|
749
|
+
return void 0;
|
|
750
|
+
}
|
|
751
|
+
function extractExtendsClause(node, importAliases) {
|
|
752
|
+
if (!node.superClass) {
|
|
753
|
+
return { extendsClause: null, extendsTypeArg: null };
|
|
754
|
+
}
|
|
755
|
+
let extendsClause = null;
|
|
756
|
+
let extendsTypeArg = null;
|
|
757
|
+
if (node.superClass.type === "Identifier") {
|
|
758
|
+
extendsClause = node.superClass.name;
|
|
759
|
+
} else if (node.superClass.type === "MemberExpression") {
|
|
760
|
+
extendsClause = getMemberExpressionString(node.superClass);
|
|
761
|
+
}
|
|
762
|
+
if (extendsClause && importAliases.has(extendsClause)) {
|
|
763
|
+
extendsClause = importAliases.get(extendsClause);
|
|
764
|
+
}
|
|
765
|
+
const params = node.superTypeArguments?.params || node.superTypeParameters?.params;
|
|
766
|
+
if (params && params.length > 0) {
|
|
767
|
+
const typeParam = params[0];
|
|
768
|
+
extendsTypeArg = extractTypeName(typeParam);
|
|
769
|
+
}
|
|
770
|
+
return { extendsClause, extendsTypeArg };
|
|
771
|
+
}
|
|
772
|
+
function getMemberExpressionString(node) {
|
|
773
|
+
const parts = [];
|
|
774
|
+
let current = node;
|
|
775
|
+
while (current.type === "MemberExpression") {
|
|
776
|
+
if (current.property.type === "Identifier") {
|
|
777
|
+
parts.unshift(current.property.name);
|
|
778
|
+
}
|
|
779
|
+
current = current.object;
|
|
780
|
+
}
|
|
781
|
+
if (current.type === "Identifier") {
|
|
782
|
+
parts.unshift(current.name);
|
|
783
|
+
}
|
|
784
|
+
return parts.join(".");
|
|
785
|
+
}
|
|
786
|
+
function reconstructCallExpression(node, sourceText) {
|
|
787
|
+
const src = sliceSource(node, sourceText);
|
|
788
|
+
if (src) return src;
|
|
789
|
+
let callee = "";
|
|
790
|
+
if (node.callee.type === "Identifier") {
|
|
791
|
+
callee = node.callee.name;
|
|
792
|
+
} else if (node.callee.type === "MemberExpression") {
|
|
793
|
+
callee = getMemberExpressionString(node.callee);
|
|
794
|
+
} else {
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
const args = [];
|
|
798
|
+
for (const arg of node.arguments) {
|
|
799
|
+
const argSrc = sliceSource(arg, sourceText);
|
|
800
|
+
if (argSrc) {
|
|
801
|
+
args.push(argSrc);
|
|
802
|
+
} else if (arg.type === "Identifier") {
|
|
803
|
+
args.push(arg.name);
|
|
804
|
+
} else if (arg.type === "Literal") {
|
|
805
|
+
args.push(arg.raw || String(arg.value));
|
|
806
|
+
} else if (arg.type === "ObjectExpression") {
|
|
807
|
+
const objStr = reconstructObjectExpression(arg, sourceText);
|
|
808
|
+
if (objStr) args.push(objStr);
|
|
809
|
+
} else {
|
|
810
|
+
args.push("...");
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return `${callee}(${args.join(", ")})`;
|
|
814
|
+
}
|
|
815
|
+
function reconstructObjectExpression(node, sourceText) {
|
|
816
|
+
const src = sliceSource(node, sourceText);
|
|
817
|
+
if (src) return src;
|
|
818
|
+
const props = [];
|
|
819
|
+
for (const prop of node.properties) {
|
|
820
|
+
if (prop.type === "SpreadElement") continue;
|
|
821
|
+
if (prop.type === "Property") {
|
|
822
|
+
let key = "";
|
|
823
|
+
if (prop.key.type === "Identifier") {
|
|
824
|
+
key = prop.key.name;
|
|
825
|
+
} else if (prop.key.type === "Literal") {
|
|
826
|
+
key = String(prop.key.value);
|
|
827
|
+
}
|
|
828
|
+
if (!key) continue;
|
|
829
|
+
let value = "";
|
|
830
|
+
const valSrc = sliceSource(prop.value, sourceText);
|
|
831
|
+
if (valSrc) {
|
|
832
|
+
value = valSrc;
|
|
833
|
+
} else if (prop.value.type === "ObjectExpression") {
|
|
834
|
+
value = reconstructObjectExpression(
|
|
835
|
+
prop.value,
|
|
836
|
+
sourceText
|
|
837
|
+
) || "";
|
|
838
|
+
} else if (prop.value.type === "ArrayExpression") {
|
|
839
|
+
value = reconstructArrayExpression(
|
|
840
|
+
prop.value,
|
|
841
|
+
sourceText
|
|
842
|
+
) || "";
|
|
843
|
+
} else if (prop.value.type === "Identifier") {
|
|
844
|
+
value = prop.value.name;
|
|
845
|
+
} else if (prop.value.type === "Literal") {
|
|
846
|
+
value = prop.value.raw || String(prop.value.value);
|
|
847
|
+
}
|
|
848
|
+
if (value) {
|
|
849
|
+
props.push(`${key}: ${value}`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return `{ ${props.join(", ")} }`;
|
|
854
|
+
}
|
|
855
|
+
function reconstructArrayExpression(node, sourceText) {
|
|
856
|
+
const src = sliceSource(node, sourceText);
|
|
857
|
+
if (src) return src;
|
|
858
|
+
const elements = [];
|
|
859
|
+
for (const el of node.elements) {
|
|
860
|
+
if (!el) continue;
|
|
861
|
+
if (el.type === "SpreadElement") {
|
|
862
|
+
elements.push("...");
|
|
863
|
+
} else {
|
|
864
|
+
const elSrc = sliceSource(el, sourceText);
|
|
865
|
+
if (elSrc) {
|
|
866
|
+
elements.push(elSrc);
|
|
867
|
+
} else if (el.type === "Identifier") {
|
|
868
|
+
elements.push(el.name);
|
|
869
|
+
} else if (el.type === "Literal") {
|
|
870
|
+
elements.push(el.raw || String(el.value));
|
|
871
|
+
} else if (el.type === "ObjectExpression") {
|
|
872
|
+
const objStr = reconstructObjectExpression(
|
|
873
|
+
el,
|
|
874
|
+
sourceText
|
|
875
|
+
);
|
|
876
|
+
if (objStr) elements.push(objStr);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return `[${elements.join(", ")}]`;
|
|
881
|
+
}
|
|
882
|
+
function extractTypeName(type) {
|
|
883
|
+
switch (type.type) {
|
|
884
|
+
case "TSTypeReference": {
|
|
885
|
+
let baseName = null;
|
|
886
|
+
if (type.typeName.type === "Identifier") {
|
|
887
|
+
baseName = type.typeName.name;
|
|
888
|
+
} else if (type.typeName.type === "TSQualifiedName") {
|
|
889
|
+
baseName = getQualifiedName(type.typeName);
|
|
890
|
+
}
|
|
891
|
+
const typeParams = type.typeArguments?.params || type.typeParameters?.params;
|
|
892
|
+
if (baseName && typeParams?.length) {
|
|
893
|
+
const typeArgs = typeParams.map((p) => extractTypeName(p)).filter(Boolean);
|
|
894
|
+
if (typeArgs.length > 0) {
|
|
895
|
+
return `${baseName}<${typeArgs.join(", ")}>`;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return baseName;
|
|
899
|
+
}
|
|
900
|
+
case "TSStringKeyword":
|
|
901
|
+
return "string";
|
|
902
|
+
case "TSNumberKeyword":
|
|
903
|
+
return "number";
|
|
904
|
+
case "TSBooleanKeyword":
|
|
905
|
+
return "boolean";
|
|
906
|
+
case "TSAnyKeyword":
|
|
907
|
+
return "any";
|
|
908
|
+
case "TSVoidKeyword":
|
|
909
|
+
return "void";
|
|
910
|
+
case "TSNullKeyword":
|
|
911
|
+
return "null";
|
|
912
|
+
case "TSUndefinedKeyword":
|
|
913
|
+
return "undefined";
|
|
914
|
+
case "TSLiteralType": {
|
|
915
|
+
const literal = type.literal;
|
|
916
|
+
if (!literal) return null;
|
|
917
|
+
if (typeof literal.value === "string") return `'${literal.value}'`;
|
|
918
|
+
if (typeof literal.value === "number") return String(literal.value);
|
|
919
|
+
if (typeof literal.value === "boolean") return String(literal.value);
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
case "TSArrayType": {
|
|
923
|
+
const elementType = extractTypeName(type.elementType);
|
|
924
|
+
return elementType ? `${elementType}[]` : null;
|
|
925
|
+
}
|
|
926
|
+
case "TSUnionType": {
|
|
927
|
+
const types = type.types.map((t) => extractTypeName(t)).filter(Boolean);
|
|
928
|
+
return types.join(" | ");
|
|
929
|
+
}
|
|
930
|
+
// Inline object type literal: { subject?: string; from?: string; body?: string }
|
|
931
|
+
// Maps to 'object' which the ManifestAdapter resolves as json
|
|
932
|
+
case "TSTypeLiteral":
|
|
933
|
+
return "object";
|
|
934
|
+
case "TSFunctionType":
|
|
935
|
+
return "Function";
|
|
936
|
+
}
|
|
937
|
+
return null;
|
|
938
|
+
}
|
|
939
|
+
function getQualifiedName(node) {
|
|
940
|
+
const parts = [];
|
|
941
|
+
let current = node;
|
|
942
|
+
while (current.type === "TSQualifiedName") {
|
|
943
|
+
parts.unshift(current.right.name);
|
|
944
|
+
current = current.left;
|
|
945
|
+
}
|
|
946
|
+
if (current.type === "Identifier") {
|
|
947
|
+
parts.unshift(current.name);
|
|
948
|
+
}
|
|
949
|
+
return parts.join(".");
|
|
950
|
+
}
|
|
951
|
+
function extractPropertyDefinition(node, sourceText) {
|
|
952
|
+
if (node.computed) return null;
|
|
953
|
+
const name = getPropertyKey(node.key);
|
|
954
|
+
if (!name) return null;
|
|
955
|
+
if (!isSafeObjectKey(name)) return null;
|
|
956
|
+
const typeAnnotation = node.typeAnnotation ? extractTypeName(node.typeAnnotation.typeAnnotation) : null;
|
|
957
|
+
let initializer = null;
|
|
958
|
+
let hasDecimalPoint = false;
|
|
959
|
+
let numericValue = null;
|
|
960
|
+
if (node.value) {
|
|
961
|
+
if (node.value.type === "UnaryExpression" && node.value.operator === "-" && node.value.argument?.type === "Literal" && typeof node.value.argument.value === "number") {
|
|
962
|
+
numericValue = -node.value.argument.value;
|
|
963
|
+
if (node.value.argument.raw) {
|
|
964
|
+
hasDecimalPoint = node.value.argument.raw.includes(".");
|
|
965
|
+
}
|
|
966
|
+
} else if (node.value.type === "Literal" && typeof node.value.value === "number") {
|
|
967
|
+
numericValue = node.value.value;
|
|
968
|
+
if (node.value.raw) {
|
|
969
|
+
hasDecimalPoint = node.value.raw.includes(".");
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
const valueSrc = sliceSource(node.value, sourceText);
|
|
973
|
+
if (valueSrc) {
|
|
974
|
+
initializer = valueSrc;
|
|
975
|
+
} else if (node.value.type === "Literal" && node.value.raw) {
|
|
976
|
+
initializer = node.value.raw;
|
|
977
|
+
} else if (node.value.type === "Literal") {
|
|
978
|
+
const val = node.value.value;
|
|
979
|
+
if (typeof val === "string") {
|
|
980
|
+
initializer = `'${val}'`;
|
|
981
|
+
} else if (val !== null && val !== void 0) {
|
|
982
|
+
initializer = String(val);
|
|
983
|
+
}
|
|
984
|
+
} else if (node.value.type === "CallExpression" || node.value.type === "NewExpression") {
|
|
985
|
+
initializer = reconstructCallExpression(
|
|
986
|
+
node.value,
|
|
987
|
+
sourceText
|
|
988
|
+
);
|
|
989
|
+
} else if (node.value.type === "ArrayExpression") {
|
|
990
|
+
initializer = reconstructArrayExpression(node.value, sourceText);
|
|
991
|
+
} else if (node.value.type === "ObjectExpression") {
|
|
992
|
+
initializer = reconstructObjectExpression(
|
|
993
|
+
node.value,
|
|
994
|
+
sourceText
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
const decorators = [];
|
|
999
|
+
if (node.decorators) {
|
|
1000
|
+
for (const dec of node.decorators) {
|
|
1001
|
+
const extracted = extractFieldDecorator(dec, sourceText);
|
|
1002
|
+
if (extracted) {
|
|
1003
|
+
decorators.push(extracted);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return {
|
|
1008
|
+
name,
|
|
1009
|
+
typeAnnotation,
|
|
1010
|
+
initializer,
|
|
1011
|
+
hasDecimalPoint,
|
|
1012
|
+
numericValue,
|
|
1013
|
+
decorators,
|
|
1014
|
+
optional: node.optional || false,
|
|
1015
|
+
isStatic: node.static || false,
|
|
1016
|
+
readonly: node.readonly || false,
|
|
1017
|
+
accessibility: node.accessibility || "public",
|
|
1018
|
+
line: node.loc?.start.line || 0
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
function extractFieldDecorator(decorator, sourceText) {
|
|
1022
|
+
const expr = decorator.expression;
|
|
1023
|
+
let name = null;
|
|
1024
|
+
const args = [];
|
|
1025
|
+
if (expr.type === "CallExpression") {
|
|
1026
|
+
if (expr.callee.type === "Identifier") {
|
|
1027
|
+
name = expr.callee.name;
|
|
1028
|
+
}
|
|
1029
|
+
for (const arg of expr.arguments) {
|
|
1030
|
+
const argSrc = sliceSource(arg, sourceText);
|
|
1031
|
+
if (argSrc) args.push(argSrc);
|
|
1032
|
+
}
|
|
1033
|
+
} else if (expr.type === "Identifier") {
|
|
1034
|
+
name = expr.name;
|
|
1035
|
+
}
|
|
1036
|
+
if (!name) return null;
|
|
1037
|
+
return { name, arguments: args };
|
|
1038
|
+
}
|
|
1039
|
+
function extractMethodDefinition(node, sourceText) {
|
|
1040
|
+
if (node.kind !== "method") return null;
|
|
1041
|
+
const name = getPropertyKey(node.key);
|
|
1042
|
+
if (!name) return null;
|
|
1043
|
+
const func = node.value;
|
|
1044
|
+
const parameters = [];
|
|
1045
|
+
for (const param of func.params) {
|
|
1046
|
+
const extracted = extractParameter(param, sourceText);
|
|
1047
|
+
if (extracted) {
|
|
1048
|
+
parameters.push(extracted);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
const returnType = func.returnType ? extractTypeName(func.returnType.typeAnnotation) : null;
|
|
1052
|
+
return {
|
|
1053
|
+
name,
|
|
1054
|
+
async: func.async,
|
|
1055
|
+
isStatic: node.static,
|
|
1056
|
+
accessibility: node.accessibility || "public",
|
|
1057
|
+
parameters,
|
|
1058
|
+
returnType,
|
|
1059
|
+
description: null,
|
|
1060
|
+
// TODO: Extract JSDoc
|
|
1061
|
+
line: node.loc?.start.line || 0
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
function extractParameter(param, sourceText) {
|
|
1065
|
+
if (param.type === "AssignmentPattern") {
|
|
1066
|
+
const left = param.left;
|
|
1067
|
+
if (left.type === "Identifier") {
|
|
1068
|
+
return {
|
|
1069
|
+
name: left.name,
|
|
1070
|
+
type: left.typeAnnotation ? extractTypeName(left.typeAnnotation.typeAnnotation) : null,
|
|
1071
|
+
optional: true,
|
|
1072
|
+
defaultValue: sliceSource(param.right, sourceText)
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
if (left.type === "ObjectPattern" || left.type === "ArrayPattern") {
|
|
1076
|
+
return {
|
|
1077
|
+
name: "options",
|
|
1078
|
+
type: left.typeAnnotation ? extractTypeName(left.typeAnnotation.typeAnnotation) : "any",
|
|
1079
|
+
optional: true,
|
|
1080
|
+
defaultValue: sliceSource(param.right, sourceText)
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
if (param.type === "RestElement") {
|
|
1086
|
+
const arg = param.argument;
|
|
1087
|
+
if (arg.type === "Identifier") {
|
|
1088
|
+
return {
|
|
1089
|
+
name: `...${arg.name}`,
|
|
1090
|
+
type: param.typeAnnotation ? extractTypeName(param.typeAnnotation.typeAnnotation) : null,
|
|
1091
|
+
optional: true,
|
|
1092
|
+
defaultValue: null
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
if (param.type === "Identifier") {
|
|
1098
|
+
return {
|
|
1099
|
+
name: param.name,
|
|
1100
|
+
type: param.typeAnnotation ? extractTypeName(param.typeAnnotation.typeAnnotation) : null,
|
|
1101
|
+
optional: param.optional || false,
|
|
1102
|
+
defaultValue: null
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
if (param.type === "ObjectPattern") {
|
|
1106
|
+
return {
|
|
1107
|
+
name: "options",
|
|
1108
|
+
type: param.typeAnnotation ? extractTypeName(param.typeAnnotation.typeAnnotation) : "any",
|
|
1109
|
+
optional: false,
|
|
1110
|
+
defaultValue: null
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
return null;
|
|
1114
|
+
}
|
|
1115
|
+
function sanitizeParsed(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
1116
|
+
if (value === null || typeof value !== "object") return value;
|
|
1117
|
+
const isArray = Array.isArray(value);
|
|
1118
|
+
const proto = Object.getPrototypeOf(value);
|
|
1119
|
+
if (!isArray && proto !== Object.prototype && proto !== null) return value;
|
|
1120
|
+
if (seen.has(value)) return void 0;
|
|
1121
|
+
seen.add(value);
|
|
1122
|
+
if (isArray) {
|
|
1123
|
+
return value.map((item) => sanitizeParsed(item, seen));
|
|
1124
|
+
}
|
|
1125
|
+
const clean = {};
|
|
1126
|
+
for (const key of Object.keys(value)) {
|
|
1127
|
+
if (!isSafeObjectKey(key)) continue;
|
|
1128
|
+
clean[key] = sanitizeParsed(value[key], seen);
|
|
1129
|
+
}
|
|
1130
|
+
return clean;
|
|
1131
|
+
}
|
|
1132
|
+
function parseLiteralInitializer(source) {
|
|
1133
|
+
const trimmed = source?.trim();
|
|
1134
|
+
if (!trimmed || !trimmed.startsWith("{") && !trimmed.startsWith("["))
|
|
1135
|
+
return null;
|
|
1136
|
+
try {
|
|
1137
|
+
const parsed = new Function(`return (${source})`)();
|
|
1138
|
+
return sanitizeParsed(parsed);
|
|
1139
|
+
} catch {
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
function stripQuotes(value) {
|
|
1144
|
+
if (!value) return value;
|
|
1145
|
+
const match = value.match(/^(['"`])(.+)\1$/);
|
|
1146
|
+
return match ? match[2] : value;
|
|
1147
|
+
}
|
|
1148
|
+
function createQualifiedName(packageName, className) {
|
|
1149
|
+
return `${packageName}:${className}`;
|
|
1150
|
+
}
|
|
1151
|
+
class ManifestAdapter {
|
|
1152
|
+
typeAliases = {};
|
|
1153
|
+
_aliasDepth;
|
|
1154
|
+
/**
|
|
1155
|
+
* Convert an array of resolved class definitions into a `SmartObjectManifest`.
|
|
1156
|
+
*
|
|
1157
|
+
* Each class is converted to a `SmartObjectDefinition` via
|
|
1158
|
+
* {@link toSmartObjectDefinition} and stored under its qualified name key
|
|
1159
|
+
* (e.g. `@my-org/my-package:MyClass`) when `packageName` is provided, or
|
|
1160
|
+
* under its lowercased class name otherwise.
|
|
1161
|
+
*
|
|
1162
|
+
* @param resolved - Resolved class definitions from {@link OxcScanner.resolve}
|
|
1163
|
+
* or {@link OxcScanner.scanAndResolve}.
|
|
1164
|
+
* @param options.packageName - npm package name used to generate qualified
|
|
1165
|
+
* class names for namespace isolation across multi-package projects.
|
|
1166
|
+
* @param options.packageVersion - Package version recorded in the manifest
|
|
1167
|
+
* metadata.
|
|
1168
|
+
* @param options.typeAliases - Map of type alias names to their resolved type
|
|
1169
|
+
* strings (from {@link ScanResults.typeAliases}). Used to resolve custom
|
|
1170
|
+
* types like `type Status = 'active' | 'inactive'` during field inference.
|
|
1171
|
+
* @returns A complete `SmartObjectManifest` ready for serialisation.
|
|
1172
|
+
*
|
|
1173
|
+
* @example
|
|
1174
|
+
* ```typescript
|
|
1175
|
+
* const manifest = adapter.toManifest(resolved, {
|
|
1176
|
+
* packageName: '@my-org/my-package',
|
|
1177
|
+
* packageVersion: '1.0.0',
|
|
1178
|
+
* typeAliases: results.typeAliases,
|
|
1179
|
+
* });
|
|
1180
|
+
* fs.writeFileSync('manifest.json', JSON.stringify(manifest, null, 2));
|
|
1181
|
+
* ```
|
|
1182
|
+
*/
|
|
1183
|
+
toManifest(resolved, options = {}) {
|
|
1184
|
+
this.typeAliases = options.typeAliases || {};
|
|
1185
|
+
const objects = {};
|
|
1186
|
+
for (const classDef of resolved) {
|
|
1187
|
+
const definition = this.toSmartObjectDefinition(classDef, options);
|
|
1188
|
+
const manifestKey = definition.qualifiedName || definition.name.toLowerCase();
|
|
1189
|
+
objects[manifestKey] = definition;
|
|
1190
|
+
}
|
|
1191
|
+
return {
|
|
1192
|
+
version: "1.0.0",
|
|
1193
|
+
timestamp: Date.now(),
|
|
1194
|
+
packageName: options.packageName,
|
|
1195
|
+
packageVersion: options.packageVersion,
|
|
1196
|
+
objects,
|
|
1197
|
+
moduleType: "smrt"
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Convert a single resolved class definition to a `SmartObjectDefinition`.
|
|
1202
|
+
*
|
|
1203
|
+
* Handles:
|
|
1204
|
+
* - Static property capture (`uiSlots`, `adminRoutes`) with child-wins
|
|
1205
|
+
* semantics for overridden statics.
|
|
1206
|
+
* - Field conversion (non-static public fields only) via {@link convertField}.
|
|
1207
|
+
* - Method conversion (public instance/static methods) via {@link convertMethod}.
|
|
1208
|
+
* - Collection name pluralisation.
|
|
1209
|
+
* - Qualified name generation when `packageName` is supplied.
|
|
1210
|
+
*
|
|
1211
|
+
* @param classDef - A fully-resolved class definition.
|
|
1212
|
+
* @param options.packageName - Package name used to build the qualified class
|
|
1213
|
+
* name (`@pkg:ClassName`).
|
|
1214
|
+
* @param options.packageVersion - Package version (informational, stored in
|
|
1215
|
+
* the definition).
|
|
1216
|
+
* @returns A `SmartObjectDefinition` ready to be stored in a manifest.
|
|
1217
|
+
*
|
|
1218
|
+
* @see {@link toManifest} for the bulk conversion entry point.
|
|
1219
|
+
*/
|
|
1220
|
+
toSmartObjectDefinition(classDef, options = {}) {
|
|
1221
|
+
let staticProperties;
|
|
1222
|
+
const knownStaticProps = ["uiSlots", "adminRoutes", "signalSubscriptions"];
|
|
1223
|
+
const ownStaticNames = /* @__PURE__ */ new Set();
|
|
1224
|
+
for (const field of classDef.fields) {
|
|
1225
|
+
if (field.isStatic && knownStaticProps.includes(field.name) && field.initializer) {
|
|
1226
|
+
try {
|
|
1227
|
+
const parsed = parseLiteralInitializer(field.initializer);
|
|
1228
|
+
if (parsed) {
|
|
1229
|
+
if (!staticProperties) staticProperties = {};
|
|
1230
|
+
staticProperties[field.name] = parsed;
|
|
1231
|
+
ownStaticNames.add(field.name);
|
|
1232
|
+
}
|
|
1233
|
+
} catch {
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
for (const field of classDef.allFields) {
|
|
1238
|
+
if (field.isStatic && knownStaticProps.includes(field.name) && field.initializer) {
|
|
1239
|
+
if (ownStaticNames.has(field.name)) continue;
|
|
1240
|
+
try {
|
|
1241
|
+
const parsed = parseLiteralInitializer(field.initializer);
|
|
1242
|
+
if (parsed) {
|
|
1243
|
+
if (!staticProperties) staticProperties = {};
|
|
1244
|
+
staticProperties[field.name] = parsed;
|
|
1245
|
+
}
|
|
1246
|
+
} catch {
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
const fields = {};
|
|
1251
|
+
for (const field of classDef.allFields) {
|
|
1252
|
+
if (field.isStatic) continue;
|
|
1253
|
+
const converted = this.convertField(field);
|
|
1254
|
+
if (converted) {
|
|
1255
|
+
fields[field.name] = converted;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
const methods = {};
|
|
1259
|
+
for (const method of classDef.methods) {
|
|
1260
|
+
const converted = this.convertMethod(method);
|
|
1261
|
+
if (converted) {
|
|
1262
|
+
methods[method.name] = converted;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
const collection = this.pluralize(classDef.className);
|
|
1266
|
+
const packageName = options.packageName || classDef.packageName;
|
|
1267
|
+
const qualifiedName = packageName ? createQualifiedName(packageName, classDef.className) : void 0;
|
|
1268
|
+
return {
|
|
1269
|
+
name: classDef.className.toLowerCase(),
|
|
1270
|
+
className: classDef.className,
|
|
1271
|
+
qualifiedName,
|
|
1272
|
+
collection,
|
|
1273
|
+
filePath: classDef.filePath,
|
|
1274
|
+
packageName: packageName || void 0,
|
|
1275
|
+
fields,
|
|
1276
|
+
methods,
|
|
1277
|
+
decoratorConfig: classDef.decoratorConfig || {},
|
|
1278
|
+
extends: classDef.extendsClause || void 0,
|
|
1279
|
+
extendsTypeArg: classDef.extendsTypeArg || void 0,
|
|
1280
|
+
exportName: classDef.className,
|
|
1281
|
+
collectionExportName: `${classDef.className}Collection`,
|
|
1282
|
+
staticProperties
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Framework internal fields that should NOT be included in manifests
|
|
1287
|
+
* These are SmrtObject internals used by the framework, not user-defined fields
|
|
1288
|
+
*/
|
|
1289
|
+
static FRAMEWORK_INTERNAL_FIELDS = /* @__PURE__ */ new Set([
|
|
1290
|
+
"_tableName",
|
|
1291
|
+
"options",
|
|
1292
|
+
"_loadedRelationships",
|
|
1293
|
+
"_db",
|
|
1294
|
+
"_ai",
|
|
1295
|
+
"_fs",
|
|
1296
|
+
"_isInitialized",
|
|
1297
|
+
"_errors",
|
|
1298
|
+
"_warnings"
|
|
1299
|
+
]);
|
|
1300
|
+
/**
|
|
1301
|
+
* Convert a single raw field definition to a manifest `FieldDefinition`.
|
|
1302
|
+
*
|
|
1303
|
+
* Returns `null` for fields that should be omitted from the manifest:
|
|
1304
|
+
* - `private` or `protected` fields.
|
|
1305
|
+
* - Framework-internal fields (`_tableName`, `_db`, `_ai`, etc.).
|
|
1306
|
+
*
|
|
1307
|
+
* Delegates type inference to {@link inferFieldType} and applies additional
|
|
1308
|
+
* post-processing:
|
|
1309
|
+
* - Marks fields with `Function` type annotation as `transient`.
|
|
1310
|
+
* - Marks fields with `@field({ transient: true })` decorator as `transient`.
|
|
1311
|
+
* - Populates `_meta.underlyingType` for STI `Meta<T>` fields.
|
|
1312
|
+
*
|
|
1313
|
+
* @param field - Raw field definition from a scanned class.
|
|
1314
|
+
* @returns A `FieldDefinition` for the manifest, or `null` if the field
|
|
1315
|
+
* should be excluded.
|
|
1316
|
+
*
|
|
1317
|
+
* @see {@link inferFieldType} for the type inference logic.
|
|
1318
|
+
*/
|
|
1319
|
+
convertField(field) {
|
|
1320
|
+
if (field.accessibility !== "public") {
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
if (ManifestAdapter.FRAMEWORK_INTERNAL_FIELDS.has(field.name)) {
|
|
1324
|
+
return null;
|
|
1325
|
+
}
|
|
1326
|
+
const isFunctionType = field.typeAnnotation === "Function";
|
|
1327
|
+
const fieldDecoratorOptions = this.extractFieldDecoratorOptions(field);
|
|
1328
|
+
const inference = this.inferFieldType(field);
|
|
1329
|
+
const definition = {
|
|
1330
|
+
type: inference.type,
|
|
1331
|
+
required: inference.required
|
|
1332
|
+
};
|
|
1333
|
+
if (inference.related) {
|
|
1334
|
+
definition.related = inference.related;
|
|
1335
|
+
}
|
|
1336
|
+
if (inference.defaultValue !== void 0) {
|
|
1337
|
+
definition.default = inference.defaultValue;
|
|
1338
|
+
}
|
|
1339
|
+
if (fieldDecoratorOptions.type) {
|
|
1340
|
+
definition.type = fieldDecoratorOptions.type;
|
|
1341
|
+
}
|
|
1342
|
+
if (fieldDecoratorOptions.nullable === true) {
|
|
1343
|
+
definition.required = false;
|
|
1344
|
+
} else if (fieldDecoratorOptions.required !== void 0) {
|
|
1345
|
+
definition.required = fieldDecoratorOptions.required;
|
|
1346
|
+
}
|
|
1347
|
+
if (fieldDecoratorOptions.default !== void 0) {
|
|
1348
|
+
definition.default = fieldDecoratorOptions.default;
|
|
1349
|
+
}
|
|
1350
|
+
if (fieldDecoratorOptions.related !== void 0) {
|
|
1351
|
+
definition.related = fieldDecoratorOptions.related;
|
|
1352
|
+
}
|
|
1353
|
+
if (fieldDecoratorOptions.description !== void 0) {
|
|
1354
|
+
definition.description = fieldDecoratorOptions.description;
|
|
1355
|
+
}
|
|
1356
|
+
if (fieldDecoratorOptions.min !== void 0) {
|
|
1357
|
+
definition.min = fieldDecoratorOptions.min;
|
|
1358
|
+
}
|
|
1359
|
+
if (fieldDecoratorOptions.max !== void 0) {
|
|
1360
|
+
definition.max = fieldDecoratorOptions.max;
|
|
1361
|
+
}
|
|
1362
|
+
if (fieldDecoratorOptions.minLength !== void 0) {
|
|
1363
|
+
definition.minLength = fieldDecoratorOptions.minLength;
|
|
1364
|
+
}
|
|
1365
|
+
if (fieldDecoratorOptions.maxLength !== void 0) {
|
|
1366
|
+
definition.maxLength = fieldDecoratorOptions.maxLength;
|
|
1367
|
+
}
|
|
1368
|
+
if (Object.keys(fieldDecoratorOptions).length > 0) {
|
|
1369
|
+
definition._meta = {
|
|
1370
|
+
...definition._meta,
|
|
1371
|
+
...fieldDecoratorOptions
|
|
1372
|
+
};
|
|
1373
|
+
if (definition._meta?.type) {
|
|
1374
|
+
delete definition._meta.type;
|
|
1375
|
+
}
|
|
1376
|
+
if (definition.related !== void 0 && definition._meta?.related) {
|
|
1377
|
+
delete definition._meta.related;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
if (inference._meta && Object.keys(inference._meta).length > 0) {
|
|
1381
|
+
definition._meta = {
|
|
1382
|
+
...definition._meta,
|
|
1383
|
+
...inference._meta
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
if (inference.underlyingType) {
|
|
1387
|
+
definition._meta = {
|
|
1388
|
+
...definition._meta,
|
|
1389
|
+
underlyingType: inference.underlyingType
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
if (isFunctionType) {
|
|
1393
|
+
definition.transient = true;
|
|
1394
|
+
}
|
|
1395
|
+
if (fieldDecoratorOptions.transient === true) {
|
|
1396
|
+
definition.transient = true;
|
|
1397
|
+
}
|
|
1398
|
+
if (fieldDecoratorOptions.sensitive === true) {
|
|
1399
|
+
definition.sensitive = true;
|
|
1400
|
+
}
|
|
1401
|
+
if (fieldDecoratorOptions.readonly === true) {
|
|
1402
|
+
definition.readonly = true;
|
|
1403
|
+
}
|
|
1404
|
+
return definition;
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Infer the SMRT field type and required flag from a raw field definition.
|
|
1408
|
+
*
|
|
1409
|
+
* Inference is attempted in the following priority order:
|
|
1410
|
+
* 1. **Field helper call in initializer** — currently always returns `null`
|
|
1411
|
+
* (field helpers removed); reserved for future use.
|
|
1412
|
+
* 2. **Decorator** — `@foreignKey`, `@oneToMany`, `@manyToMany`, `@field({ type })`.
|
|
1413
|
+
* 3. **Type annotation** — `string` → `text`, `number` with `0` vs `0.0`
|
|
1414
|
+
* heuristic → `integer` / `decimal`, `boolean`, `Date` → `datetime`,
|
|
1415
|
+
* arrays → `json`, `Record<>` / `object` → `json`, union types with
|
|
1416
|
+
* `null`, inline string/number literal unions, `Meta<T>` wrapper,
|
|
1417
|
+
* and type alias resolution (up to depth 5).
|
|
1418
|
+
* 4. **Numeric literal without annotation** — `version = 1` → `integer`.
|
|
1419
|
+
* 5. **Boolean literal without annotation** — `isRead = false` → `boolean`.
|
|
1420
|
+
* 6. **Default** — falls back to `text`.
|
|
1421
|
+
*
|
|
1422
|
+
* @param field - The raw field definition to analyse.
|
|
1423
|
+
* @returns A {@link FieldTypeInference} describing the inferred type,
|
|
1424
|
+
* required flag, default value, related class name (for relationships),
|
|
1425
|
+
* and the inference source for debugging.
|
|
1426
|
+
*
|
|
1427
|
+
* @see {@link FieldTypeInference} for the result shape.
|
|
1428
|
+
* @see {@link InferredFieldType} for valid type values.
|
|
1429
|
+
*/
|
|
1430
|
+
inferFieldType(field) {
|
|
1431
|
+
if (field.initializer) {
|
|
1432
|
+
const helperResult = this.inferFromHelper(field.initializer);
|
|
1433
|
+
if (helperResult) {
|
|
1434
|
+
return helperResult;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
for (const decorator of field.decorators) {
|
|
1438
|
+
const decoratorResult = this.inferFromDecorator(decorator, field);
|
|
1439
|
+
if (decoratorResult) {
|
|
1440
|
+
return decoratorResult;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
if (field.typeAnnotation) {
|
|
1444
|
+
return this.inferFromAnnotation(field);
|
|
1445
|
+
}
|
|
1446
|
+
if (field.numericValue !== null) {
|
|
1447
|
+
const fieldType = field.hasDecimalPoint ? "decimal" : "integer";
|
|
1448
|
+
return {
|
|
1449
|
+
type: fieldType,
|
|
1450
|
+
required: !field.optional,
|
|
1451
|
+
defaultValue: field.numericValue,
|
|
1452
|
+
source: "heuristic"
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
if (field.initializer === "true" || field.initializer === "false") {
|
|
1456
|
+
return {
|
|
1457
|
+
type: "boolean",
|
|
1458
|
+
required: !field.optional,
|
|
1459
|
+
defaultValue: field.initializer === "true",
|
|
1460
|
+
source: "heuristic"
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
const hasDefaultValue = field.initializer !== null;
|
|
1464
|
+
return {
|
|
1465
|
+
type: "text",
|
|
1466
|
+
required: !field.optional && !hasDefaultValue,
|
|
1467
|
+
source: "default"
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Infer type from field helper call (removed)
|
|
1472
|
+
*
|
|
1473
|
+
* Field helpers have been removed in favor of decorators and TypeScript types:
|
|
1474
|
+
* - Use TypeScript types: name: string = '', price: number = 0.0
|
|
1475
|
+
* - Use @field() decorator for constraints: @field({ required: true })
|
|
1476
|
+
* - Use @foreignKey(), @oneToMany(), @manyToMany() decorators for relationships
|
|
1477
|
+
*/
|
|
1478
|
+
inferFromHelper(_initializer) {
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Infer type from field decorator
|
|
1483
|
+
*/
|
|
1484
|
+
inferFromDecorator(decorator, field) {
|
|
1485
|
+
if (decorator.name === "field" && decorator.arguments.length > 0) {
|
|
1486
|
+
const fieldOptions = this.parseFieldDecoratorOptions(
|
|
1487
|
+
decorator.arguments[0]
|
|
1488
|
+
);
|
|
1489
|
+
const type = this.normalizeFieldType(fieldOptions?.type);
|
|
1490
|
+
if (type) {
|
|
1491
|
+
const hasDefaultValue = field.initializer !== null || fieldOptions?.default !== void 0;
|
|
1492
|
+
let required = !field.optional && !hasDefaultValue;
|
|
1493
|
+
if (fieldOptions?.nullable === true) {
|
|
1494
|
+
required = false;
|
|
1495
|
+
} else if (fieldOptions?.required !== void 0) {
|
|
1496
|
+
required = fieldOptions.required;
|
|
1497
|
+
}
|
|
1498
|
+
return {
|
|
1499
|
+
type,
|
|
1500
|
+
required,
|
|
1501
|
+
defaultValue: fieldOptions?.default,
|
|
1502
|
+
related: typeof fieldOptions?.related === "string" ? fieldOptions.related : void 0,
|
|
1503
|
+
source: "decorator"
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
if (decorator.name === "meta") {
|
|
1508
|
+
const parsedOptions = this.parseFieldDecoratorOptions(
|
|
1509
|
+
decorator.arguments[0]
|
|
1510
|
+
);
|
|
1511
|
+
const hasDefaultValue = field.initializer !== null;
|
|
1512
|
+
const meta = {};
|
|
1513
|
+
if (parsedOptions?.indexed !== void 0)
|
|
1514
|
+
meta.indexed = parsedOptions.indexed;
|
|
1515
|
+
if (parsedOptions?.nullable !== void 0)
|
|
1516
|
+
meta.nullable = parsedOptions.nullable;
|
|
1517
|
+
return {
|
|
1518
|
+
type: "meta",
|
|
1519
|
+
required: parsedOptions?.required !== void 0 ? Boolean(parsedOptions.required) : !field.optional && !hasDefaultValue,
|
|
1520
|
+
defaultValue: parsedOptions?.default !== void 0 ? parsedOptions.default : void 0,
|
|
1521
|
+
...Object.keys(meta).length > 0 ? { _meta: meta } : {},
|
|
1522
|
+
source: "decorator"
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
if (decorator.name === "foreignKey") {
|
|
1526
|
+
const relatedClass = stripQuotes(decorator.arguments[0]?.trim());
|
|
1527
|
+
const parsedOptions = this.parseFieldDecoratorOptions(
|
|
1528
|
+
decorator.arguments[1]
|
|
1529
|
+
);
|
|
1530
|
+
const meta = {};
|
|
1531
|
+
const META_KEYS = [
|
|
1532
|
+
"required",
|
|
1533
|
+
"nullable",
|
|
1534
|
+
"unique",
|
|
1535
|
+
"description",
|
|
1536
|
+
"default"
|
|
1537
|
+
];
|
|
1538
|
+
if (parsedOptions) {
|
|
1539
|
+
for (const key of META_KEYS) {
|
|
1540
|
+
if (parsedOptions[key] !== void 0) {
|
|
1541
|
+
meta[key] = parsedOptions[key];
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
const hasDefaultValue = field.initializer !== null;
|
|
1546
|
+
return {
|
|
1547
|
+
type: "foreignKey",
|
|
1548
|
+
related: relatedClass || void 0,
|
|
1549
|
+
required: parsedOptions?.required !== void 0 ? Boolean(parsedOptions.required) : !field.optional && !hasDefaultValue,
|
|
1550
|
+
defaultValue: parsedOptions?.default !== void 0 ? parsedOptions.default : void 0,
|
|
1551
|
+
...Object.keys(meta).length > 0 ? { _meta: meta } : {},
|
|
1552
|
+
source: "decorator"
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
if (decorator.name === "tenantId") {
|
|
1556
|
+
const parsedOptions = this.parseFieldDecoratorOptions(
|
|
1557
|
+
decorator.arguments[0]
|
|
1558
|
+
);
|
|
1559
|
+
const nullable = parsedOptions?.nullable === true;
|
|
1560
|
+
const required = parsedOptions?.required !== void 0 ? Boolean(parsedOptions.required) : !nullable;
|
|
1561
|
+
return {
|
|
1562
|
+
type: "text",
|
|
1563
|
+
required,
|
|
1564
|
+
_meta: {
|
|
1565
|
+
sqlType: "UUID",
|
|
1566
|
+
...parsedOptions ?? {},
|
|
1567
|
+
__tenancy: {
|
|
1568
|
+
isTenantIdField: true,
|
|
1569
|
+
autoFilter: parsedOptions?.autoFilter ?? true,
|
|
1570
|
+
required,
|
|
1571
|
+
autoPopulate: parsedOptions?.autoPopulate ?? true,
|
|
1572
|
+
nullable
|
|
1573
|
+
}
|
|
1574
|
+
},
|
|
1575
|
+
source: "decorator"
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
if (decorator.name === "crossPackageRef") {
|
|
1579
|
+
const qualifiedName = stripQuotes(decorator.arguments[0]?.trim());
|
|
1580
|
+
const hasDefaultValue = field.initializer !== null;
|
|
1581
|
+
const parsedOptions = this.parseFieldDecoratorOptions(
|
|
1582
|
+
decorator.arguments[1]
|
|
1583
|
+
);
|
|
1584
|
+
const meta = {};
|
|
1585
|
+
const META_KEYS = [
|
|
1586
|
+
"validate",
|
|
1587
|
+
"nullable",
|
|
1588
|
+
"unique",
|
|
1589
|
+
"description",
|
|
1590
|
+
"default",
|
|
1591
|
+
"indexed",
|
|
1592
|
+
"idType"
|
|
1593
|
+
];
|
|
1594
|
+
if (parsedOptions) {
|
|
1595
|
+
for (const key of META_KEYS) {
|
|
1596
|
+
if (parsedOptions[key] !== void 0) {
|
|
1597
|
+
meta[key] = parsedOptions[key];
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
return {
|
|
1602
|
+
type: "crossPackageRef",
|
|
1603
|
+
related: qualifiedName || void 0,
|
|
1604
|
+
required: parsedOptions?.required !== void 0 ? Boolean(parsedOptions.required) : !field.optional && !hasDefaultValue,
|
|
1605
|
+
defaultValue: parsedOptions?.default !== void 0 ? parsedOptions.default : void 0,
|
|
1606
|
+
...Object.keys(meta).length > 0 ? { _meta: meta } : {},
|
|
1607
|
+
source: "decorator"
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
if (decorator.name === "oneToMany") {
|
|
1611
|
+
const relatedClass = stripQuotes(decorator.arguments[0]?.trim());
|
|
1612
|
+
const parsedOptions = this.parseFieldDecoratorOptions(
|
|
1613
|
+
decorator.arguments[1]
|
|
1614
|
+
);
|
|
1615
|
+
const meta = {};
|
|
1616
|
+
if (parsedOptions?.foreignKey !== void 0) {
|
|
1617
|
+
meta.foreignKey = parsedOptions.foreignKey;
|
|
1618
|
+
}
|
|
1619
|
+
return {
|
|
1620
|
+
type: "oneToMany",
|
|
1621
|
+
related: relatedClass || void 0,
|
|
1622
|
+
required: false,
|
|
1623
|
+
...Object.keys(meta).length > 0 ? { _meta: meta } : {},
|
|
1624
|
+
source: "decorator"
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
if (decorator.name === "manyToMany") {
|
|
1628
|
+
const relatedClass = stripQuotes(decorator.arguments[0]?.trim());
|
|
1629
|
+
const parsedOptions = this.parseFieldDecoratorOptions(
|
|
1630
|
+
decorator.arguments[1]
|
|
1631
|
+
);
|
|
1632
|
+
const meta = {};
|
|
1633
|
+
if (parsedOptions?.through !== void 0)
|
|
1634
|
+
meta.through = parsedOptions.through;
|
|
1635
|
+
if (parsedOptions?.sourceKey !== void 0)
|
|
1636
|
+
meta.sourceKey = parsedOptions.sourceKey;
|
|
1637
|
+
if (parsedOptions?.targetKey !== void 0)
|
|
1638
|
+
meta.targetKey = parsedOptions.targetKey;
|
|
1639
|
+
return {
|
|
1640
|
+
type: "manyToMany",
|
|
1641
|
+
related: relatedClass || void 0,
|
|
1642
|
+
required: false,
|
|
1643
|
+
...Object.keys(meta).length > 0 ? { _meta: meta } : {},
|
|
1644
|
+
source: "decorator"
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
return null;
|
|
1648
|
+
}
|
|
1649
|
+
extractFieldDecoratorOptions(field) {
|
|
1650
|
+
for (const decorator of field.decorators) {
|
|
1651
|
+
if (decorator.name !== "field") continue;
|
|
1652
|
+
const parsed = this.parseFieldDecoratorOptions(decorator.arguments[0]);
|
|
1653
|
+
if (parsed) {
|
|
1654
|
+
return parsed;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
return {};
|
|
1658
|
+
}
|
|
1659
|
+
parseFieldDecoratorOptions(rawArgument) {
|
|
1660
|
+
if (!rawArgument) return null;
|
|
1661
|
+
const parsed = parseLiteralInitializer(rawArgument);
|
|
1662
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
|
|
1663
|
+
return null;
|
|
1664
|
+
}
|
|
1665
|
+
return parsed;
|
|
1666
|
+
}
|
|
1667
|
+
normalizeFieldType(value) {
|
|
1668
|
+
switch (value) {
|
|
1669
|
+
case "text":
|
|
1670
|
+
case "decimal":
|
|
1671
|
+
case "boolean":
|
|
1672
|
+
case "integer":
|
|
1673
|
+
case "datetime":
|
|
1674
|
+
case "json":
|
|
1675
|
+
case "foreignKey":
|
|
1676
|
+
case "crossPackageRef":
|
|
1677
|
+
case "oneToMany":
|
|
1678
|
+
case "manyToMany":
|
|
1679
|
+
case "meta":
|
|
1680
|
+
return value;
|
|
1681
|
+
default:
|
|
1682
|
+
return void 0;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Infer type from TypeScript type annotation
|
|
1687
|
+
*/
|
|
1688
|
+
inferFromAnnotation(field) {
|
|
1689
|
+
const type = field.typeAnnotation;
|
|
1690
|
+
const hasDefaultValue = field.initializer !== null;
|
|
1691
|
+
const isRequired = !field.optional && !hasDefaultValue;
|
|
1692
|
+
if (type?.startsWith("Meta<") && type.endsWith(">")) {
|
|
1693
|
+
const innerType = type.slice(5, -1);
|
|
1694
|
+
const underlyingInference = this.inferFromAnnotation({
|
|
1695
|
+
...field,
|
|
1696
|
+
typeAnnotation: innerType
|
|
1697
|
+
});
|
|
1698
|
+
return {
|
|
1699
|
+
type: "meta",
|
|
1700
|
+
required: isRequired,
|
|
1701
|
+
defaultValue: underlyingInference.defaultValue,
|
|
1702
|
+
source: "annotation",
|
|
1703
|
+
// Store underlying type for hydration coercion
|
|
1704
|
+
underlyingType: underlyingInference.type
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
if (type === "string") {
|
|
1708
|
+
return {
|
|
1709
|
+
type: "text",
|
|
1710
|
+
required: isRequired,
|
|
1711
|
+
defaultValue: this.parseDefaultValue(field.initializer, "string"),
|
|
1712
|
+
source: "annotation"
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
if (type === "number") {
|
|
1716
|
+
const fieldType = field.hasDecimalPoint ? "decimal" : "integer";
|
|
1717
|
+
return {
|
|
1718
|
+
type: fieldType,
|
|
1719
|
+
required: isRequired,
|
|
1720
|
+
defaultValue: field.numericValue ?? void 0,
|
|
1721
|
+
source: "heuristic"
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
if (type === "boolean") {
|
|
1725
|
+
return {
|
|
1726
|
+
type: "boolean",
|
|
1727
|
+
required: isRequired,
|
|
1728
|
+
defaultValue: this.parseDefaultValue(field.initializer, "boolean"),
|
|
1729
|
+
source: "annotation"
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
if (type === "Date") {
|
|
1733
|
+
return {
|
|
1734
|
+
type: "datetime",
|
|
1735
|
+
required: isRequired,
|
|
1736
|
+
source: "annotation"
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
if (type?.endsWith("[]")) {
|
|
1740
|
+
return {
|
|
1741
|
+
type: "json",
|
|
1742
|
+
required: isRequired,
|
|
1743
|
+
defaultValue: [],
|
|
1744
|
+
source: "annotation"
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
if (type?.startsWith("Record<") || type === "object") {
|
|
1748
|
+
return {
|
|
1749
|
+
type: "json",
|
|
1750
|
+
required: isRequired,
|
|
1751
|
+
defaultValue: {},
|
|
1752
|
+
source: "annotation"
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
if (type?.includes(" | null") || type?.includes("null | ") || type?.includes(" | undefined") || type?.includes("undefined | ")) {
|
|
1756
|
+
const baseType = type.replace(/\s*\|\s*null/g, "").replace(/\s*\|\s*undefined/g, "").replace(/\bnull\s*\|\s*/g, "").replace(/\bundefined\s*\|\s*/g, "").trim();
|
|
1757
|
+
const inference = this.inferFromAnnotation({
|
|
1758
|
+
...field,
|
|
1759
|
+
typeAnnotation: baseType,
|
|
1760
|
+
optional: true
|
|
1761
|
+
});
|
|
1762
|
+
return inference;
|
|
1763
|
+
}
|
|
1764
|
+
if (type && /^'[^']*'(\s*\|\s*'[^']*')+$/.test(type)) {
|
|
1765
|
+
return {
|
|
1766
|
+
type: "text",
|
|
1767
|
+
required: isRequired,
|
|
1768
|
+
defaultValue: this.parseDefaultValue(field.initializer, "string"),
|
|
1769
|
+
source: "annotation"
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
if (type && /^-?\d+(\s*\|\s*-?\d+)+$/.test(type)) {
|
|
1773
|
+
return {
|
|
1774
|
+
type: "integer",
|
|
1775
|
+
required: isRequired,
|
|
1776
|
+
defaultValue: field.numericValue ?? void 0,
|
|
1777
|
+
source: "annotation"
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
if (type && !type.includes(" ") && !type.includes("<") && this.typeAliases[type] && (this._aliasDepth ?? 0) < 5) {
|
|
1781
|
+
const resolved = this.typeAliases[type];
|
|
1782
|
+
this._aliasDepth = (this._aliasDepth ?? 0) + 1;
|
|
1783
|
+
try {
|
|
1784
|
+
return this.inferFromAnnotation({ ...field, typeAnnotation: resolved });
|
|
1785
|
+
} finally {
|
|
1786
|
+
this._aliasDepth = (this._aliasDepth ?? 0) - 1;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
if (field.initializer?.match(/^(['"]).*\1$/)) {
|
|
1790
|
+
return {
|
|
1791
|
+
type: "text",
|
|
1792
|
+
required: isRequired,
|
|
1793
|
+
defaultValue: this.parseDefaultValue(field.initializer, "string"),
|
|
1794
|
+
source: "heuristic"
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
return {
|
|
1798
|
+
type: "json",
|
|
1799
|
+
required: isRequired,
|
|
1800
|
+
source: "default"
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* Parse default value from initializer string
|
|
1805
|
+
*/
|
|
1806
|
+
parseDefaultValue(initializer, expectedType) {
|
|
1807
|
+
if (!initializer) return void 0;
|
|
1808
|
+
switch (expectedType) {
|
|
1809
|
+
case "string": {
|
|
1810
|
+
const stringMatch = initializer.match(/^(['"`])(.*)\1$/s);
|
|
1811
|
+
if (stringMatch) {
|
|
1812
|
+
return stringMatch[2];
|
|
1813
|
+
}
|
|
1814
|
+
break;
|
|
1815
|
+
}
|
|
1816
|
+
case "boolean":
|
|
1817
|
+
if (initializer === "true") return true;
|
|
1818
|
+
if (initializer === "false") return false;
|
|
1819
|
+
break;
|
|
1820
|
+
case "number": {
|
|
1821
|
+
const num = parseFloat(initializer);
|
|
1822
|
+
if (!Number.isNaN(num)) return num;
|
|
1823
|
+
break;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
return void 0;
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Convert a raw method definition to a manifest `MethodDefinition`.
|
|
1830
|
+
*
|
|
1831
|
+
* Returns `null` for `private` or `protected` methods, which are excluded
|
|
1832
|
+
* from the manifest. Parameters are mapped to the manifest parameter shape
|
|
1833
|
+
* and default values are parsed via `parseDefaultValue`.
|
|
1834
|
+
*
|
|
1835
|
+
* @param method - Raw method definition from a scanned class.
|
|
1836
|
+
* @returns A manifest-compatible `MethodDefinition`, or `null` if the method
|
|
1837
|
+
* should be excluded.
|
|
1838
|
+
*/
|
|
1839
|
+
convertMethod(method) {
|
|
1840
|
+
if (method.accessibility !== "public") {
|
|
1841
|
+
return null;
|
|
1842
|
+
}
|
|
1843
|
+
return {
|
|
1844
|
+
name: method.name,
|
|
1845
|
+
async: method.async,
|
|
1846
|
+
parameters: method.parameters.map((p) => ({
|
|
1847
|
+
name: p.name,
|
|
1848
|
+
type: p.type || "any",
|
|
1849
|
+
optional: p.optional,
|
|
1850
|
+
default: p.defaultValue ? this.parseDefaultValue(p.defaultValue, "string") : void 0
|
|
1851
|
+
})),
|
|
1852
|
+
returnType: method.returnType || "any",
|
|
1853
|
+
description: method.description || void 0,
|
|
1854
|
+
isStatic: method.isStatic,
|
|
1855
|
+
isPublic: true
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Simple pluralization for collection names.
|
|
1860
|
+
*
|
|
1861
|
+
* This produces the manifest's `collection` label only; the authoritative DDL
|
|
1862
|
+
* table name is derived independently by core (`classnameToTablename` →
|
|
1863
|
+
* the `pluralize` library), so this needs to stay self-consistent rather than
|
|
1864
|
+
* cover every irregular plural. Note the `y → ies` rule fires only after a
|
|
1865
|
+
* consonant, so vowel+y words pluralise correctly (`Day` → `days`, not
|
|
1866
|
+
* `daies`).
|
|
1867
|
+
*/
|
|
1868
|
+
pluralize(name) {
|
|
1869
|
+
const lower = name.toLowerCase();
|
|
1870
|
+
if (/[^aeiou]y$/.test(lower)) {
|
|
1871
|
+
return `${lower.slice(0, -1)}ies`;
|
|
1872
|
+
}
|
|
1873
|
+
if (lower.endsWith("s") || lower.endsWith("x") || lower.endsWith("z")) {
|
|
1874
|
+
return `${lower}es`;
|
|
1875
|
+
}
|
|
1876
|
+
if (lower.endsWith("ch") || lower.endsWith("sh")) {
|
|
1877
|
+
return `${lower}es`;
|
|
1878
|
+
}
|
|
1879
|
+
return `${lower}s`;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
const DEFAULT_INCLUDE = ["**/*.ts", "**/*.tsx"];
|
|
1883
|
+
const DEFAULT_EXCLUDE = [
|
|
1884
|
+
"**/node_modules/**",
|
|
1885
|
+
"**/dist/**",
|
|
1886
|
+
"**/build/**",
|
|
1887
|
+
"**/*.d.ts",
|
|
1888
|
+
"**/*.test.ts",
|
|
1889
|
+
"**/*.spec.ts",
|
|
1890
|
+
"**/__tests__/**"
|
|
1891
|
+
];
|
|
1892
|
+
class OxcScanner {
|
|
1893
|
+
options;
|
|
1894
|
+
resolver;
|
|
1895
|
+
scanResults = null;
|
|
1896
|
+
/**
|
|
1897
|
+
* Create a new `OxcScanner` with the given options.
|
|
1898
|
+
*
|
|
1899
|
+
* All options are optional. By default the scanner targets every `.ts` and
|
|
1900
|
+
* `.tsx` file under `process.cwd()`, excluding `node_modules`, `dist`,
|
|
1901
|
+
* `build`, declaration files, and test files.
|
|
1902
|
+
*
|
|
1903
|
+
* @param options - Scanner configuration. See {@link OxcScannerOptions}.
|
|
1904
|
+
*/
|
|
1905
|
+
constructor(options = {}) {
|
|
1906
|
+
this.options = {
|
|
1907
|
+
include: options.include || DEFAULT_INCLUDE,
|
|
1908
|
+
exclude: options.exclude || DEFAULT_EXCLUDE,
|
|
1909
|
+
cwd: options.cwd || process.cwd(),
|
|
1910
|
+
tsconfig: options.tsconfig || "",
|
|
1911
|
+
followImports: options.followImports ?? false,
|
|
1912
|
+
baseClasses: options.baseClasses || [],
|
|
1913
|
+
includePrivateMethods: options.includePrivateMethods ?? false,
|
|
1914
|
+
includeStaticMethods: options.includeStaticMethods ?? true,
|
|
1915
|
+
externalManifests: options.externalManifests || /* @__PURE__ */ new Map()
|
|
1916
|
+
};
|
|
1917
|
+
this.resolver = new InheritanceResolver({
|
|
1918
|
+
baseClasses: this.options.baseClasses,
|
|
1919
|
+
externalManifests: this.options.externalManifests
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Phase 1 — Discover and parse TypeScript files using OXC.
|
|
1924
|
+
*
|
|
1925
|
+
* Uses `fast-glob` to enumerate matching files and then parses them in
|
|
1926
|
+
* parallel with OXC (Rust). The raw class definitions are registered with
|
|
1927
|
+
* the internal {@link InheritanceResolver} for use in the subsequent
|
|
1928
|
+
* {@link resolve} call.
|
|
1929
|
+
*
|
|
1930
|
+
* @returns A {@link ScanResults} object containing all classes found, any
|
|
1931
|
+
* parse errors, accumulated type aliases, SMRT import metadata, and
|
|
1932
|
+
* aggregate timing information.
|
|
1933
|
+
*
|
|
1934
|
+
* @example
|
|
1935
|
+
* ```typescript
|
|
1936
|
+
* const scanner = new OxcScanner({ cwd: '/project' });
|
|
1937
|
+
* const results = await scanner.scan();
|
|
1938
|
+
* console.log(`Parsed ${results.fileCount} files in ${results.totalParseTimeMs.toFixed(1)}ms`);
|
|
1939
|
+
* ```
|
|
1940
|
+
*/
|
|
1941
|
+
async scan() {
|
|
1942
|
+
const startTime = performance.now();
|
|
1943
|
+
const files = await this.discoverFiles();
|
|
1944
|
+
const fileResults = await Promise.all(
|
|
1945
|
+
files.map((filePath) => this.parseFileWithTiming(filePath))
|
|
1946
|
+
);
|
|
1947
|
+
const results = {
|
|
1948
|
+
files: fileResults,
|
|
1949
|
+
classes: [],
|
|
1950
|
+
errors: [],
|
|
1951
|
+
totalParseTimeMs: performance.now() - startTime,
|
|
1952
|
+
fileCount: files.length,
|
|
1953
|
+
typeAliases: {}
|
|
1954
|
+
};
|
|
1955
|
+
for (const file of fileResults) {
|
|
1956
|
+
for (const classDef of file.classes) {
|
|
1957
|
+
results.classes.push(classDef);
|
|
1958
|
+
}
|
|
1959
|
+
for (const error of file.errors) {
|
|
1960
|
+
results.errors.push(error);
|
|
1961
|
+
}
|
|
1962
|
+
Object.assign(results.typeAliases, file.typeAliases);
|
|
1963
|
+
}
|
|
1964
|
+
this.resolver.addClasses(results.classes);
|
|
1965
|
+
this.scanResults = results;
|
|
1966
|
+
return results;
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Phase 2 — Resolve inheritance chains for all scanned classes.
|
|
1970
|
+
*
|
|
1971
|
+
* Must be called after {@link scan}. Walks each class's extends chain,
|
|
1972
|
+
* detects STI hierarchies, merges ancestor fields for STI subclasses, and
|
|
1973
|
+
* marks framework base classes.
|
|
1974
|
+
*
|
|
1975
|
+
* @returns An array of {@link ResolvedClassDefinition} objects — one for
|
|
1976
|
+
* every class that either carries `@smrt()` or extends a framework base
|
|
1977
|
+
* class (`SmrtObject`, `SmrtClass`, `SmrtCollection`).
|
|
1978
|
+
*
|
|
1979
|
+
* @throws {Error} If called before {@link scan}.
|
|
1980
|
+
*
|
|
1981
|
+
* @see {@link scanAndResolve} to run both phases in one call.
|
|
1982
|
+
*/
|
|
1983
|
+
resolve() {
|
|
1984
|
+
if (!this.scanResults) {
|
|
1985
|
+
throw new Error("Must call scan() before resolve()");
|
|
1986
|
+
}
|
|
1987
|
+
return this.resolver.resolveAll();
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Run both scan phases in a single call.
|
|
1991
|
+
*
|
|
1992
|
+
* Equivalent to calling `await scanner.scan()` followed by
|
|
1993
|
+
* `scanner.resolve()`. This is the most common entry point for callers
|
|
1994
|
+
* that want the fully-resolved manifest-ready data in one step.
|
|
1995
|
+
*
|
|
1996
|
+
* @returns An object with:
|
|
1997
|
+
* - `results` — raw {@link ScanResults} from Phase 1.
|
|
1998
|
+
* - `resolved` — array of {@link ResolvedClassDefinition} from Phase 2.
|
|
1999
|
+
*
|
|
2000
|
+
* @example
|
|
2001
|
+
* ```typescript
|
|
2002
|
+
* const scanner = new OxcScanner({ cwd: '/project/src' });
|
|
2003
|
+
* const { results, resolved } = await scanner.scanAndResolve();
|
|
2004
|
+
* // resolved is ready to pass to ManifestAdapter.toManifest()
|
|
2005
|
+
* ```
|
|
2006
|
+
*
|
|
2007
|
+
* @see {@link ManifestAdapter} to convert `resolved` into a manifest JSON.
|
|
2008
|
+
*/
|
|
2009
|
+
async scanAndResolve() {
|
|
2010
|
+
const results = await this.scan();
|
|
2011
|
+
const resolved = this.resolve();
|
|
2012
|
+
return { results, resolved };
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Register an external package manifest for cross-package base class resolution.
|
|
2016
|
+
*
|
|
2017
|
+
* When a project class extends a class defined in an installed SMRT package,
|
|
2018
|
+
* the resolver needs access to that package's class definitions to walk the
|
|
2019
|
+
* full inheritance chain. Call this method with each external package's
|
|
2020
|
+
* {@link ExternalManifest} before calling {@link scan} or {@link resolve}.
|
|
2021
|
+
*
|
|
2022
|
+
* @param manifest - The external manifest to register, including `packageName`,
|
|
2023
|
+
* `packageVersion`, and a `classes` map keyed by class name.
|
|
2024
|
+
*
|
|
2025
|
+
* @see {@link ExternalManifest}
|
|
2026
|
+
*/
|
|
2027
|
+
addExternalManifest(manifest) {
|
|
2028
|
+
this.resolver.addExternalManifest(manifest);
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Scan all discovered files for @happyvertical/smrt-* imports.
|
|
2032
|
+
* Returns a map of package name → Set of imported class names.
|
|
2033
|
+
*
|
|
2034
|
+
* Used for tree-shaking: only external objects that are actually imported
|
|
2035
|
+
* in the project's source files will be included in the manifest.
|
|
2036
|
+
*
|
|
2037
|
+
* Must be called after scan() or as part of scanAndResolve().
|
|
2038
|
+
*
|
|
2039
|
+
* @example
|
|
2040
|
+
* ```typescript
|
|
2041
|
+
* const scanner = new OxcScanner({ cwd: process.cwd() });
|
|
2042
|
+
* await scanner.scan();
|
|
2043
|
+
* const imports = scanner.scanSmrtImports();
|
|
2044
|
+
* // Map { '@happyvertical/smrt-profiles' => Set { 'Person', 'Organization' } }
|
|
2045
|
+
* ```
|
|
2046
|
+
*/
|
|
2047
|
+
scanSmrtImports() {
|
|
2048
|
+
if (!this.scanResults) {
|
|
2049
|
+
throw new Error("Must call scan() before scanSmrtImports()");
|
|
2050
|
+
}
|
|
2051
|
+
const merged = /* @__PURE__ */ new Map();
|
|
2052
|
+
for (const file of this.scanResults.files) {
|
|
2053
|
+
if (file.smrtImports) {
|
|
2054
|
+
for (const [pkg, classes] of file.smrtImports) {
|
|
2055
|
+
if (!merged.has(pkg)) {
|
|
2056
|
+
merged.set(pkg, /* @__PURE__ */ new Set());
|
|
2057
|
+
}
|
|
2058
|
+
const mergedSet = merged.get(pkg);
|
|
2059
|
+
for (const cls of classes) {
|
|
2060
|
+
mergedSet.add(cls);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
return merged;
|
|
2066
|
+
}
|
|
2067
|
+
/**
|
|
2068
|
+
* Return aggregate statistics about the last scan.
|
|
2069
|
+
*
|
|
2070
|
+
* Can be called after {@link scan} has completed. Returns counts useful for
|
|
2071
|
+
* diagnostics and the `--stats` CLI flag.
|
|
2072
|
+
*
|
|
2073
|
+
* @returns An object with:
|
|
2074
|
+
* - `totalClasses` — total class declarations seen (including non-SMRT).
|
|
2075
|
+
* - `smrtClasses` — classes with `@smrt()` decorator.
|
|
2076
|
+
* - `stiClasses` — SMRT classes participating in an STI hierarchy.
|
|
2077
|
+
* - `maxInheritanceDepth` — length of the deepest inheritance chain.
|
|
2078
|
+
* - `fileCount` — number of files scanned.
|
|
2079
|
+
* - `parseTimeMs` — total wall-clock parse time in milliseconds.
|
|
2080
|
+
*/
|
|
2081
|
+
getStats() {
|
|
2082
|
+
const resolverStats = this.resolver.getStats();
|
|
2083
|
+
return {
|
|
2084
|
+
...resolverStats,
|
|
2085
|
+
fileCount: this.scanResults?.fileCount || 0,
|
|
2086
|
+
parseTimeMs: this.scanResults?.totalParseTimeMs || 0
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Discover files to scan using fast-glob
|
|
2091
|
+
*/
|
|
2092
|
+
async discoverFiles() {
|
|
2093
|
+
const patterns = this.options.include;
|
|
2094
|
+
const files = await fg(patterns, {
|
|
2095
|
+
cwd: this.options.cwd,
|
|
2096
|
+
ignore: this.options.exclude,
|
|
2097
|
+
absolute: true,
|
|
2098
|
+
onlyFiles: true
|
|
2099
|
+
});
|
|
2100
|
+
return files;
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Parse a single file with timing
|
|
2104
|
+
*/
|
|
2105
|
+
async parseFileWithTiming(filePath) {
|
|
2106
|
+
return parseFile(filePath);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
export {
|
|
2110
|
+
InheritanceResolver as I,
|
|
2111
|
+
ManifestAdapter as M,
|
|
2112
|
+
OxcScanner as O,
|
|
2113
|
+
parseSource as a,
|
|
2114
|
+
extractSmrtImports as e,
|
|
2115
|
+
parseFile as p
|
|
2116
|
+
};
|
|
2117
|
+
//# sourceMappingURL=scanner-3K_xuVXN.js.map
|