@happyvertical/smrt-scanner 0.37.2 → 0.37.3

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