@vertz/compiler 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +724 -0
  2. package/dist/index.js +2814 -0
  3. package/package.json +34 -0
package/dist/index.js ADDED
@@ -0,0 +1,2814 @@
1
+ // src/analyzers/app-analyzer.ts
2
+ import { SyntaxKind as SyntaxKind2 } from "ts-morph";
3
+
4
+ // src/errors.ts
5
+ function createDiagnostic(options) {
6
+ return { ...options };
7
+ }
8
+ function createDiagnosticFromLocation(location, options) {
9
+ return {
10
+ ...options,
11
+ file: location.sourceFile,
12
+ line: location.sourceLine,
13
+ column: location.sourceColumn
14
+ };
15
+ }
16
+ function hasErrors(diagnostics) {
17
+ return diagnostics.some((d) => d.severity === "error");
18
+ }
19
+ function filterBySeverity(diagnostics, severity) {
20
+ return diagnostics.filter((d) => d.severity === severity);
21
+ }
22
+ function mergeDiagnostics(a, b) {
23
+ return [...a, ...b];
24
+ }
25
+
26
+ // src/utils/ast-helpers.ts
27
+ import {
28
+ SyntaxKind
29
+ } from "ts-morph";
30
+ function matchPropertyAccess(call, objectName, methodName) {
31
+ const expr = call.getExpression();
32
+ if (!expr.isKind(SyntaxKind.PropertyAccessExpression))
33
+ return null;
34
+ const obj = expr.getExpression();
35
+ if (!obj.isKind(SyntaxKind.Identifier) || obj.getText() !== objectName || expr.getName() !== methodName) {
36
+ return null;
37
+ }
38
+ return expr;
39
+ }
40
+ function findCallExpressions(file, objectName, methodName) {
41
+ return file.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => matchPropertyAccess(call, objectName, methodName) !== null);
42
+ }
43
+ function findMethodCallsOnVariable(file, variableName, methodName) {
44
+ return file.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
45
+ const access = matchPropertyAccess(call, variableName, methodName);
46
+ if (!access)
47
+ return false;
48
+ const obj = access.getExpression();
49
+ return obj.getDefinitionNodes().some((d) => d.isKind(SyntaxKind.VariableDeclaration));
50
+ });
51
+ }
52
+ function extractObjectLiteral(callExpr, argIndex) {
53
+ const arg = callExpr.getArguments()[argIndex];
54
+ if (arg?.isKind(SyntaxKind.ObjectLiteralExpression))
55
+ return arg;
56
+ return null;
57
+ }
58
+ function getPropertyValue(obj, key) {
59
+ for (const prop of obj.getProperties()) {
60
+ if (prop.isKind(SyntaxKind.PropertyAssignment) && prop.getName() === key) {
61
+ return prop.getInitializerOrThrow();
62
+ }
63
+ if (prop.isKind(SyntaxKind.ShorthandPropertyAssignment) && prop.getName() === key) {
64
+ return prop.getNameNode();
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+ function getProperties(obj) {
70
+ const result = [];
71
+ for (const prop of obj.getProperties()) {
72
+ if (prop.isKind(SyntaxKind.PropertyAssignment)) {
73
+ result.push({ name: prop.getName(), value: prop.getInitializerOrThrow() });
74
+ } else if (prop.isKind(SyntaxKind.ShorthandPropertyAssignment)) {
75
+ result.push({ name: prop.getName(), value: prop.getNameNode() });
76
+ }
77
+ }
78
+ return result;
79
+ }
80
+ function getStringValue(expr) {
81
+ if (expr.isKind(SyntaxKind.StringLiteral) || expr.isKind(SyntaxKind.NoSubstitutionTemplateLiteral)) {
82
+ return expr.getLiteralValue();
83
+ }
84
+ return null;
85
+ }
86
+ function getBooleanValue(expr) {
87
+ if (expr.isKind(SyntaxKind.TrueKeyword))
88
+ return true;
89
+ if (expr.isKind(SyntaxKind.FalseKeyword))
90
+ return false;
91
+ return null;
92
+ }
93
+ function getNumberValue(expr) {
94
+ if (expr.isKind(SyntaxKind.NumericLiteral)) {
95
+ return expr.getLiteralValue();
96
+ }
97
+ if (expr.isKind(SyntaxKind.PrefixUnaryExpression)) {
98
+ const operator = expr.getOperatorToken();
99
+ const operand = expr.getOperand();
100
+ if (operand.isKind(SyntaxKind.NumericLiteral)) {
101
+ if (operator === SyntaxKind.MinusToken)
102
+ return -operand.getLiteralValue();
103
+ if (operator === SyntaxKind.PlusToken)
104
+ return operand.getLiteralValue();
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+ function getArrayElements(expr) {
110
+ if (expr.isKind(SyntaxKind.ArrayLiteralExpression)) {
111
+ return expr.getElements();
112
+ }
113
+ return [];
114
+ }
115
+ function getVariableNameForCall(callExpr) {
116
+ const parent = callExpr.getParent();
117
+ if (parent?.isKind(SyntaxKind.VariableDeclaration)) {
118
+ return parent.getName();
119
+ }
120
+ return null;
121
+ }
122
+ function getSourceLocation(node) {
123
+ const file = node.getSourceFile();
124
+ const pos = node.getStart();
125
+ const { line, column } = file.getLineAndColumnAtPos(pos);
126
+ return {
127
+ sourceFile: file.getFilePath(),
128
+ sourceLine: line,
129
+ sourceColumn: column
130
+ };
131
+ }
132
+
133
+ // src/utils/import-resolver.ts
134
+ function resolveIdentifier(identifier, project) {
135
+ const importInfo = findImportForIdentifier(identifier);
136
+ if (!importInfo)
137
+ return null;
138
+ const moduleSourceFile = importInfo.importDecl.getModuleSpecifierSourceFile();
139
+ if (!moduleSourceFile)
140
+ return null;
141
+ return resolveExport(moduleSourceFile, importInfo.originalName, project);
142
+ }
143
+ function resolveExport(file, exportName, project) {
144
+ for (const exportDecl of file.getExportDeclarations()) {
145
+ const moduleSourceFile = exportDecl.getModuleSpecifierSourceFile();
146
+ if (!moduleSourceFile)
147
+ continue;
148
+ for (const named of exportDecl.getNamedExports()) {
149
+ const name = named.getAliasNode()?.getText() ?? named.getName();
150
+ if (name === exportName) {
151
+ return resolveExport(moduleSourceFile, named.getName(), project);
152
+ }
153
+ }
154
+ }
155
+ const exportedDecls = file.getExportedDeclarations().get(exportName);
156
+ if (exportedDecls && exportedDecls.length > 0) {
157
+ const decl = exportedDecls.at(0);
158
+ if (!decl)
159
+ return null;
160
+ return {
161
+ declaration: decl,
162
+ sourceFile: decl.getSourceFile(),
163
+ exportName
164
+ };
165
+ }
166
+ return null;
167
+ }
168
+ function isFromImport(identifier, moduleSpecifier) {
169
+ const importInfo = findImportForIdentifier(identifier);
170
+ if (!importInfo)
171
+ return false;
172
+ return importInfo.importDecl.getModuleSpecifierValue() === moduleSpecifier;
173
+ }
174
+ function findImportForIdentifier(identifier) {
175
+ const sourceFile = identifier.getSourceFile();
176
+ const name = identifier.getText();
177
+ for (const importDecl of sourceFile.getImportDeclarations()) {
178
+ for (const specifier of importDecl.getNamedImports()) {
179
+ const localName = specifier.getAliasNode()?.getText() ?? specifier.getName();
180
+ if (localName === name) {
181
+ return { importDecl, originalName: specifier.getName() };
182
+ }
183
+ }
184
+ const nsImport = importDecl.getNamespaceImport();
185
+ if (nsImport && nsImport.getText() === name) {
186
+ return { importDecl, originalName: "*" };
187
+ }
188
+ }
189
+ return null;
190
+ }
191
+
192
+ // src/analyzers/base-analyzer.ts
193
+ class BaseAnalyzer {
194
+ project;
195
+ config;
196
+ _diagnostics = [];
197
+ constructor(project, config) {
198
+ this.project = project;
199
+ this.config = config;
200
+ }
201
+ addDiagnostic(diagnostic) {
202
+ this._diagnostics.push(diagnostic);
203
+ }
204
+ getDiagnostics() {
205
+ return [...this._diagnostics];
206
+ }
207
+ }
208
+
209
+ // src/analyzers/app-analyzer.ts
210
+ class AppAnalyzer extends BaseAnalyzer {
211
+ async analyze() {
212
+ const allAppCalls = [];
213
+ for (const file2 of this.project.getSourceFiles()) {
214
+ const appCalls = findCallExpressions(file2, "vertz", "app");
215
+ for (const call2 of appCalls) {
216
+ allAppCalls.push({ call: call2, file: file2 });
217
+ }
218
+ }
219
+ if (allAppCalls.length === 0) {
220
+ this.addDiagnostic(createDiagnosticFromLocation({ sourceFile: "", sourceLine: 0, sourceColumn: 0 }, {
221
+ severity: "error",
222
+ code: "VERTZ_APP_NOT_FOUND",
223
+ message: "No vertz.app() call found in the project."
224
+ }));
225
+ return {
226
+ app: {
227
+ basePath: "/",
228
+ globalMiddleware: [],
229
+ moduleRegistrations: [],
230
+ sourceFile: "",
231
+ sourceLine: 0,
232
+ sourceColumn: 0
233
+ }
234
+ };
235
+ }
236
+ if (allAppCalls.length > 1) {
237
+ const loc2 = getSourceLocation(allAppCalls[1].call);
238
+ this.addDiagnostic(createDiagnosticFromLocation(loc2, {
239
+ severity: "error",
240
+ code: "VERTZ_APP_DUPLICATE",
241
+ message: "Multiple vertz.app() calls found. Only one is allowed."
242
+ }));
243
+ }
244
+ const { call, file } = allAppCalls[0];
245
+ const obj = extractObjectLiteral(call, 0);
246
+ const basePathExpr = obj ? getPropertyValue(obj, "basePath") : null;
247
+ const basePath = basePathExpr ? getStringValue(basePathExpr) ?? "/" : "/";
248
+ const versionExpr = obj ? getPropertyValue(obj, "version") : null;
249
+ const version = versionExpr ? getStringValue(versionExpr) ?? undefined : undefined;
250
+ const loc = getSourceLocation(call);
251
+ if (basePath !== "/" && !basePath.startsWith("/")) {
252
+ this.addDiagnostic(createDiagnosticFromLocation(loc, {
253
+ severity: "warning",
254
+ code: "VERTZ_APP_BASEPATH_FORMAT",
255
+ message: `basePath "${basePath}" should start with "/".`
256
+ }));
257
+ }
258
+ const chainedCalls = this.collectChainedCalls(call);
259
+ const globalMiddleware = this.extractMiddlewares(chainedCalls, file);
260
+ const moduleRegistrations = this.extractRegistrations(chainedCalls);
261
+ return {
262
+ app: {
263
+ basePath,
264
+ version,
265
+ globalMiddleware,
266
+ moduleRegistrations,
267
+ ...loc
268
+ }
269
+ };
270
+ }
271
+ collectChainedCalls(appCall) {
272
+ const results = [];
273
+ let current = appCall;
274
+ while (current.getParent()?.isKind(SyntaxKind2.PropertyAccessExpression)) {
275
+ const propAccess = current.getParentOrThrow();
276
+ if (!propAccess.isKind(SyntaxKind2.PropertyAccessExpression))
277
+ break;
278
+ const methodName = propAccess.getName();
279
+ const parentCall = propAccess.getParent();
280
+ if (!parentCall?.isKind(SyntaxKind2.CallExpression))
281
+ break;
282
+ results.push({ methodName, call: parentCall });
283
+ current = parentCall;
284
+ }
285
+ return results;
286
+ }
287
+ extractMiddlewares(chainedCalls, file) {
288
+ const middleware = [];
289
+ for (const { methodName, call } of chainedCalls) {
290
+ if (methodName !== "middlewares")
291
+ continue;
292
+ const arrArg = call.getArguments().at(0);
293
+ if (!arrArg)
294
+ continue;
295
+ const elements = getArrayElements(arrArg);
296
+ for (const el of elements) {
297
+ if (el.isKind(SyntaxKind2.Identifier)) {
298
+ const resolved = resolveIdentifier(el, this.project);
299
+ middleware.push({
300
+ name: el.getText(),
301
+ sourceFile: resolved ? resolved.sourceFile.getFilePath() : file.getFilePath()
302
+ });
303
+ }
304
+ }
305
+ }
306
+ return middleware;
307
+ }
308
+ extractRegistrations(chainedCalls) {
309
+ const registrations = [];
310
+ for (const { methodName, call } of chainedCalls) {
311
+ if (methodName !== "register")
312
+ continue;
313
+ const args = call.getArguments();
314
+ const moduleArg = args.at(0);
315
+ if (!moduleArg)
316
+ continue;
317
+ const moduleName = moduleArg.isKind(SyntaxKind2.Identifier) ? moduleArg.getText() : undefined;
318
+ if (!moduleName) {
319
+ this.addDiagnostic(createDiagnosticFromLocation(getSourceLocation(call), {
320
+ severity: "warning",
321
+ code: "VERTZ_APP_INLINE_MODULE",
322
+ message: ".register() argument should be a module identifier, not an inline expression."
323
+ }));
324
+ continue;
325
+ }
326
+ const optionsObj = extractObjectLiteral(call, 1);
327
+ const options = optionsObj ? this.extractOptions(optionsObj) : undefined;
328
+ registrations.push({ moduleName, options });
329
+ }
330
+ return registrations;
331
+ }
332
+ extractOptions(obj) {
333
+ const result = {};
334
+ for (const prop of obj.getProperties()) {
335
+ if (!prop.isKind(SyntaxKind2.PropertyAssignment))
336
+ continue;
337
+ const name = prop.getName();
338
+ const init = prop.getInitializerOrThrow();
339
+ const strValue = getStringValue(init);
340
+ if (strValue !== null) {
341
+ result[name] = strValue;
342
+ } else if (init.isKind(SyntaxKind2.TrueKeyword)) {
343
+ result[name] = true;
344
+ } else if (init.isKind(SyntaxKind2.FalseKeyword)) {
345
+ result[name] = false;
346
+ } else if (init.isKind(SyntaxKind2.NumericLiteral)) {
347
+ result[name] = Number(init.getText());
348
+ } else if (init.isKind(SyntaxKind2.ObjectLiteralExpression)) {
349
+ result[name] = this.extractOptions(init);
350
+ }
351
+ }
352
+ return result;
353
+ }
354
+ }
355
+ // src/analyzers/dependency-graph-analyzer.ts
356
+ class DependencyGraphAnalyzer extends BaseAnalyzer {
357
+ input = { modules: [], middleware: [] };
358
+ setInput(input) {
359
+ this.input = input;
360
+ }
361
+ async analyze(input) {
362
+ const { modules, middleware } = input ?? this.input;
363
+ const nodes = this.buildNodes(modules, middleware);
364
+ const serviceTokenMap = this.buildServiceTokenMap(modules);
365
+ const edges = this.buildEdges(modules, middleware, serviceTokenMap);
366
+ const moduleNames = modules.map((m) => m.name);
367
+ const { initializationOrder, circularDependencies } = this.computeModuleOrder(moduleNames, edges);
368
+ this.emitCycleDiagnostics(circularDependencies);
369
+ this.emitUnresolvedInjectDiagnostics(modules, middleware, serviceTokenMap);
370
+ if (initializationOrder.length > 0) {
371
+ this.addDiagnostic(createDiagnostic({
372
+ severity: "info",
373
+ code: "VERTZ_DEP_INIT_ORDER",
374
+ message: `Module initialization order: ${initializationOrder.join(", ")}`
375
+ }));
376
+ }
377
+ return {
378
+ graph: { nodes, edges, initializationOrder, circularDependencies }
379
+ };
380
+ }
381
+ buildNodes(modules, middleware) {
382
+ const nodes = [];
383
+ for (const mod of modules) {
384
+ nodes.push({ id: `module:${mod.name}`, kind: "module", name: mod.name });
385
+ for (const svc of mod.services) {
386
+ nodes.push({
387
+ id: `service:${mod.name}.${svc.name}`,
388
+ kind: "service",
389
+ name: svc.name,
390
+ moduleName: mod.name
391
+ });
392
+ }
393
+ for (const router of mod.routers) {
394
+ nodes.push({
395
+ id: `router:${mod.name}.${router.name}`,
396
+ kind: "router",
397
+ name: router.name,
398
+ moduleName: mod.name
399
+ });
400
+ }
401
+ }
402
+ for (const mw of middleware) {
403
+ nodes.push({ id: `middleware:${mw.name}`, kind: "middleware", name: mw.name });
404
+ }
405
+ return nodes;
406
+ }
407
+ buildServiceTokenMap(modules) {
408
+ const map = new Map;
409
+ for (const mod of modules) {
410
+ for (const svc of mod.services) {
411
+ map.set(svc.name, `service:${mod.name}.${svc.name}`);
412
+ }
413
+ }
414
+ return map;
415
+ }
416
+ buildEdges(modules, middleware, serviceTokenMap) {
417
+ const edges = [];
418
+ for (const mod of modules) {
419
+ const seen = new Set;
420
+ for (const imp of mod.imports) {
421
+ if (imp.isEnvImport || !imp.sourceModule)
422
+ continue;
423
+ if (seen.has(imp.sourceModule))
424
+ continue;
425
+ seen.add(imp.sourceModule);
426
+ edges.push({
427
+ from: `module:${mod.name}`,
428
+ to: `module:${imp.sourceModule}`,
429
+ kind: "imports"
430
+ });
431
+ }
432
+ }
433
+ for (const mod of modules) {
434
+ for (const svc of mod.services) {
435
+ this.addInjectEdges(edges, `service:${mod.name}.${svc.name}`, svc.inject, serviceTokenMap);
436
+ }
437
+ for (const router of mod.routers) {
438
+ this.addInjectEdges(edges, `router:${mod.name}.${router.name}`, router.inject, serviceTokenMap);
439
+ }
440
+ }
441
+ for (const mw of middleware) {
442
+ this.addInjectEdges(edges, `middleware:${mw.name}`, mw.inject, serviceTokenMap);
443
+ }
444
+ for (const mod of modules) {
445
+ for (const router of mod.routers) {
446
+ for (const route of router.routes) {
447
+ for (const mwRef of route.middleware) {
448
+ edges.push({
449
+ from: `router:${mod.name}.${router.name}`,
450
+ to: `middleware:${mwRef.name}`,
451
+ kind: "uses-middleware"
452
+ });
453
+ }
454
+ }
455
+ }
456
+ }
457
+ for (const mod of modules) {
458
+ for (const exportName of mod.exports) {
459
+ const targetId = serviceTokenMap.get(exportName);
460
+ if (targetId) {
461
+ edges.push({ from: `module:${mod.name}`, to: targetId, kind: "exports" });
462
+ }
463
+ }
464
+ }
465
+ return edges;
466
+ }
467
+ addInjectEdges(edges, fromId, injectRefs, serviceTokenMap) {
468
+ for (const inj of injectRefs) {
469
+ const targetId = serviceTokenMap.get(inj.resolvedToken);
470
+ if (targetId) {
471
+ edges.push({ from: fromId, to: targetId, kind: "inject" });
472
+ }
473
+ }
474
+ }
475
+ computeModuleOrder(moduleNames, edges) {
476
+ const inDegree = new Map;
477
+ const adjacency = new Map;
478
+ for (const name of moduleNames) {
479
+ inDegree.set(name, 0);
480
+ adjacency.set(name, []);
481
+ }
482
+ for (const edge of edges) {
483
+ if (edge.kind !== "imports")
484
+ continue;
485
+ const fromMod = edge.from.replace("module:", "");
486
+ const toMod = edge.to.replace("module:", "");
487
+ adjacency.get(toMod)?.push(fromMod);
488
+ inDegree.set(fromMod, (inDegree.get(fromMod) ?? 0) + 1);
489
+ }
490
+ const queue = [];
491
+ for (const name of moduleNames) {
492
+ if (inDegree.get(name) === 0) {
493
+ queue.push(name);
494
+ }
495
+ }
496
+ const initializationOrder = [];
497
+ while (queue.length > 0) {
498
+ const current = queue.shift();
499
+ if (!current)
500
+ break;
501
+ initializationOrder.push(current);
502
+ for (const neighbor of adjacency.get(current) ?? []) {
503
+ const newDegree = (inDegree.get(neighbor) ?? 1) - 1;
504
+ inDegree.set(neighbor, newDegree);
505
+ if (newDegree === 0) {
506
+ queue.push(neighbor);
507
+ }
508
+ }
509
+ }
510
+ const circularDependencies = this.detectCycles(moduleNames, initializationOrder, edges);
511
+ const sorted = new Set(initializationOrder);
512
+ for (const name of moduleNames) {
513
+ if (!sorted.has(name)) {
514
+ initializationOrder.push(name);
515
+ }
516
+ }
517
+ return { initializationOrder, circularDependencies };
518
+ }
519
+ detectCycles(moduleNames, sortedNames, edges) {
520
+ const sorted = new Set(sortedNames);
521
+ const unsorted = moduleNames.filter((n) => !sorted.has(n));
522
+ if (unsorted.length === 0)
523
+ return [];
524
+ const depAdj = new Map;
525
+ for (const name of unsorted) {
526
+ depAdj.set(name, []);
527
+ }
528
+ for (const edge of edges) {
529
+ if (edge.kind !== "imports")
530
+ continue;
531
+ const fromMod = edge.from.replace("module:", "");
532
+ const toMod = edge.to.replace("module:", "");
533
+ if (depAdj.has(fromMod) && depAdj.has(toMod)) {
534
+ depAdj.get(fromMod)?.push(toMod);
535
+ }
536
+ }
537
+ const cycles = [];
538
+ const visited = new Set;
539
+ for (const start of unsorted) {
540
+ if (visited.has(start))
541
+ continue;
542
+ const component = [];
543
+ const stack = [start];
544
+ while (stack.length > 0) {
545
+ const node = stack.pop();
546
+ if (!node)
547
+ break;
548
+ if (visited.has(node))
549
+ continue;
550
+ visited.add(node);
551
+ component.push(node);
552
+ for (const dep of depAdj.get(node) ?? []) {
553
+ if (!visited.has(dep))
554
+ stack.push(dep);
555
+ }
556
+ for (const [other, deps] of depAdj) {
557
+ if (deps.includes(node) && !visited.has(other)) {
558
+ stack.push(other);
559
+ }
560
+ }
561
+ }
562
+ if (component.length >= 1) {
563
+ cycles.push(component);
564
+ }
565
+ }
566
+ return cycles;
567
+ }
568
+ emitCycleDiagnostics(circularDependencies) {
569
+ for (const cycle of circularDependencies) {
570
+ const path = [...cycle, cycle.at(0)].join(" -> ");
571
+ this.addDiagnostic(createDiagnostic({
572
+ severity: "error",
573
+ code: "VERTZ_DEP_CIRCULAR",
574
+ message: `Circular dependency detected: ${path}`
575
+ }));
576
+ }
577
+ }
578
+ emitUnresolvedInjectDiagnostics(modules, middleware, serviceTokenMap) {
579
+ for (const mod of modules) {
580
+ for (const svc of mod.services) {
581
+ this.warnUnresolvedInjects(svc.inject, serviceTokenMap, `service "${svc.name}" of module "${mod.name}"`);
582
+ }
583
+ for (const router of mod.routers) {
584
+ this.warnUnresolvedInjects(router.inject, serviceTokenMap, `router "${router.name}" of module "${mod.name}"`);
585
+ }
586
+ }
587
+ for (const mw of middleware) {
588
+ this.warnUnresolvedInjects(mw.inject, serviceTokenMap, `middleware "${mw.name}"`);
589
+ }
590
+ }
591
+ warnUnresolvedInjects(injectRefs, serviceTokenMap, context) {
592
+ for (const inj of injectRefs) {
593
+ if (!serviceTokenMap.has(inj.resolvedToken)) {
594
+ this.addDiagnostic(createDiagnostic({
595
+ severity: "warning",
596
+ code: "VERTZ_DEP_UNRESOLVED_INJECT",
597
+ message: `Unresolved inject "${inj.resolvedToken}" in ${context}.`
598
+ }));
599
+ }
600
+ }
601
+ }
602
+ }
603
+ // src/analyzers/env-analyzer.ts
604
+ import { SyntaxKind as SyntaxKind4 } from "ts-morph";
605
+
606
+ // src/analyzers/schema-analyzer.ts
607
+ import { SyntaxKind as SyntaxKind3 } from "ts-morph";
608
+ class SchemaAnalyzer extends BaseAnalyzer {
609
+ async analyze() {
610
+ const schemas = [];
611
+ for (const file of this.project.getSourceFiles()) {
612
+ if (!isSchemaFile(file))
613
+ continue;
614
+ for (const exportSymbol of file.getExportSymbols()) {
615
+ const declarations = exportSymbol.getDeclarations();
616
+ for (const decl of declarations) {
617
+ if (!decl.isKind(SyntaxKind3.VariableDeclaration))
618
+ continue;
619
+ const initializer = decl.getInitializer();
620
+ if (!initializer)
621
+ continue;
622
+ if (!isSchemaExpression(file, initializer))
623
+ continue;
624
+ const name = exportSymbol.getName();
625
+ const loc = getSourceLocation(decl);
626
+ const id = extractSchemaId(initializer);
627
+ schemas.push({
628
+ name,
629
+ ...loc,
630
+ id: id ?? undefined,
631
+ namingConvention: parseSchemaName(name),
632
+ isNamed: id !== null
633
+ });
634
+ }
635
+ }
636
+ }
637
+ return { schemas };
638
+ }
639
+ }
640
+ var VALID_OPERATIONS = ["create", "read", "update", "list", "delete"];
641
+ var VALID_PARTS = ["Body", "Response", "Query", "Params", "Headers"];
642
+ function parseSchemaName(name) {
643
+ for (const op of VALID_OPERATIONS) {
644
+ if (!name.startsWith(op))
645
+ continue;
646
+ const rest = name.slice(op.length);
647
+ if (rest.length === 0)
648
+ continue;
649
+ for (const part of VALID_PARTS) {
650
+ if (!rest.endsWith(part))
651
+ continue;
652
+ const entity = rest.slice(0, -part.length);
653
+ if (entity.length === 0)
654
+ continue;
655
+ const firstChar = entity.at(0);
656
+ if (!firstChar || firstChar !== firstChar.toUpperCase())
657
+ continue;
658
+ return { operation: op, entity, part };
659
+ }
660
+ }
661
+ return {};
662
+ }
663
+ function isSchemaExpression(_file, expr) {
664
+ const root = findRootIdentifier(expr);
665
+ if (!root)
666
+ return false;
667
+ return isFromImport(root, "@vertz/schema");
668
+ }
669
+ function extractSchemaId(expr) {
670
+ let current = expr;
671
+ while (current.isKind(SyntaxKind3.CallExpression)) {
672
+ const access = current.getExpression();
673
+ if (access.isKind(SyntaxKind3.PropertyAccessExpression) && access.getName() === "id") {
674
+ const args = current.getArguments();
675
+ if (args.length === 1) {
676
+ const firstArg = args.at(0);
677
+ const value = firstArg ? getStringValue(firstArg) : null;
678
+ if (value !== null)
679
+ return value;
680
+ }
681
+ }
682
+ if (access.isKind(SyntaxKind3.PropertyAccessExpression)) {
683
+ current = access.getExpression();
684
+ } else {
685
+ break;
686
+ }
687
+ }
688
+ return null;
689
+ }
690
+ function isSchemaFile(file) {
691
+ return file.getImportDeclarations().some((decl) => decl.getModuleSpecifierValue() === "@vertz/schema");
692
+ }
693
+ function createNamedSchemaRef(schemaName, sourceFile) {
694
+ return { kind: "named", schemaName, sourceFile };
695
+ }
696
+ function createInlineSchemaRef(sourceFile) {
697
+ return { kind: "inline", sourceFile };
698
+ }
699
+ function findRootIdentifier(expr) {
700
+ if (expr.isKind(SyntaxKind3.CallExpression)) {
701
+ return findRootIdentifier(expr.getExpression());
702
+ }
703
+ if (expr.isKind(SyntaxKind3.PropertyAccessExpression)) {
704
+ return findRootIdentifier(expr.getExpression());
705
+ }
706
+ if (expr.isKind(SyntaxKind3.Identifier)) {
707
+ return expr;
708
+ }
709
+ return null;
710
+ }
711
+
712
+ // src/analyzers/env-analyzer.ts
713
+ class EnvAnalyzer extends BaseAnalyzer {
714
+ async analyze() {
715
+ let env;
716
+ for (const file of this.project.getSourceFiles()) {
717
+ const calls = findCallExpressions(file, "vertz", "env");
718
+ for (const call of calls) {
719
+ const obj = extractObjectLiteral(call, 0);
720
+ if (!obj)
721
+ continue;
722
+ const loc = getSourceLocation(call);
723
+ if (env) {
724
+ this.addDiagnostic(createDiagnosticFromLocation(loc, {
725
+ severity: "error",
726
+ code: "VERTZ_ENV_DUPLICATE",
727
+ message: "Multiple vertz.env() calls found. Only one is allowed."
728
+ }));
729
+ continue;
730
+ }
731
+ const loadExpr = getPropertyValue(obj, "load");
732
+ const loadFiles = loadExpr ? getArrayElements(loadExpr).map((e) => getStringValue(e)).filter((v) => v !== null) : [];
733
+ const schemaExpr = getPropertyValue(obj, "schema");
734
+ let schema;
735
+ if (schemaExpr?.isKind(SyntaxKind4.Identifier)) {
736
+ schema = createNamedSchemaRef(schemaExpr.getText(), file.getFilePath());
737
+ }
738
+ env = {
739
+ ...loc,
740
+ loadFiles,
741
+ schema,
742
+ variables: []
743
+ };
744
+ }
745
+ }
746
+ return { env };
747
+ }
748
+ }
749
+ // src/analyzers/middleware-analyzer.ts
750
+ import { SyntaxKind as SyntaxKind6 } from "ts-morph";
751
+
752
+ // src/analyzers/service-analyzer.ts
753
+ import { SyntaxKind as SyntaxKind5 } from "ts-morph";
754
+ class ServiceAnalyzer extends BaseAnalyzer {
755
+ async analyze() {
756
+ return { services: [] };
757
+ }
758
+ async analyzeForModule(moduleDefVarName, moduleName) {
759
+ const services = [];
760
+ for (const file of this.project.getSourceFiles()) {
761
+ const calls = findMethodCallsOnVariable(file, moduleDefVarName, "service");
762
+ for (const call of calls) {
763
+ const name = getVariableNameForCall(call);
764
+ if (!name)
765
+ continue;
766
+ const obj = extractObjectLiteral(call, 0);
767
+ const inject = obj ? parseInjectFromObj(obj) : [];
768
+ const methods = obj ? parseMethodsFromObj(obj) : [];
769
+ const loc = getSourceLocation(call);
770
+ services.push({
771
+ name,
772
+ moduleName,
773
+ ...loc,
774
+ inject,
775
+ methods
776
+ });
777
+ }
778
+ }
779
+ return services;
780
+ }
781
+ }
782
+ function parseInjectFromObj(obj) {
783
+ const injectExpr = getPropertyValue(obj, "inject");
784
+ if (!injectExpr?.isKind(SyntaxKind5.ObjectLiteralExpression))
785
+ return [];
786
+ return parseInjectRefs(injectExpr);
787
+ }
788
+ function parseMethodsFromObj(obj) {
789
+ const methodsExpr = getPropertyValue(obj, "methods");
790
+ if (!methodsExpr)
791
+ return [];
792
+ return extractMethodSignatures(methodsExpr);
793
+ }
794
+ function parseInjectRefs(obj) {
795
+ return getProperties(obj).map(({ name, value }) => {
796
+ const resolvedToken = value.isKind(SyntaxKind5.Identifier) ? value.getText() : name;
797
+ return { localName: name, resolvedToken };
798
+ });
799
+ }
800
+ function extractMethodSignatures(expr) {
801
+ if (!expr.isKind(SyntaxKind5.ArrowFunction) && !expr.isKind(SyntaxKind5.FunctionExpression)) {
802
+ return [];
803
+ }
804
+ const body = expr.getBody();
805
+ let returnObj = null;
806
+ if (body.isKind(SyntaxKind5.ObjectLiteralExpression)) {
807
+ returnObj = body;
808
+ } else if (body.isKind(SyntaxKind5.ParenthesizedExpression)) {
809
+ const inner = body.getExpression();
810
+ if (inner.isKind(SyntaxKind5.ObjectLiteralExpression)) {
811
+ returnObj = inner;
812
+ }
813
+ } else if (body.isKind(SyntaxKind5.Block)) {
814
+ const returnStmt = body.getStatements().find((s) => s.isKind(SyntaxKind5.ReturnStatement));
815
+ const retExpr = returnStmt?.asKind(SyntaxKind5.ReturnStatement)?.getExpression();
816
+ if (retExpr?.isKind(SyntaxKind5.ObjectLiteralExpression)) {
817
+ returnObj = retExpr;
818
+ }
819
+ }
820
+ if (!returnObj)
821
+ return [];
822
+ const methods = [];
823
+ for (const prop of returnObj.getProperties()) {
824
+ if (!prop.isKind(SyntaxKind5.PropertyAssignment))
825
+ continue;
826
+ const methodName = prop.getName();
827
+ const init = prop.getInitializer();
828
+ if (!init)
829
+ continue;
830
+ const params = extractFunctionParams(init);
831
+ const returnType = inferReturnType(init);
832
+ methods.push({
833
+ name: methodName,
834
+ parameters: params,
835
+ returnType
836
+ });
837
+ }
838
+ return methods;
839
+ }
840
+ function extractFunctionParams(expr) {
841
+ if (!expr.isKind(SyntaxKind5.ArrowFunction) && !expr.isKind(SyntaxKind5.FunctionExpression)) {
842
+ return [];
843
+ }
844
+ return expr.getParameters().map((p) => ({
845
+ name: p.getName(),
846
+ type: p.getType().getText(p)
847
+ }));
848
+ }
849
+ function inferReturnType(expr) {
850
+ if (expr.isKind(SyntaxKind5.ArrowFunction) || expr.isKind(SyntaxKind5.FunctionExpression)) {
851
+ const retType = expr.getReturnType();
852
+ return retType.getText(expr);
853
+ }
854
+ return "unknown";
855
+ }
856
+
857
+ // src/analyzers/middleware-analyzer.ts
858
+ class MiddlewareAnalyzer extends BaseAnalyzer {
859
+ async analyze() {
860
+ const middleware = [];
861
+ for (const file of this.project.getSourceFiles()) {
862
+ const calls = findCallExpressions(file, "vertz", "middleware");
863
+ for (const call of calls) {
864
+ const obj = extractObjectLiteral(call, 0);
865
+ if (!obj) {
866
+ const callLoc = getSourceLocation(call);
867
+ this.addDiagnostic(createDiagnosticFromLocation(callLoc, {
868
+ severity: "warning",
869
+ code: "VERTZ_MW_NON_OBJECT_CONFIG",
870
+ message: "Middleware config must be an object literal for static analysis.",
871
+ suggestion: "Pass an inline object literal to vertz.middleware()."
872
+ }));
873
+ continue;
874
+ }
875
+ const loc = getSourceLocation(call);
876
+ const nameExpr = getPropertyValue(obj, "name");
877
+ if (!nameExpr) {
878
+ this.addDiagnostic(createDiagnosticFromLocation(loc, {
879
+ severity: "error",
880
+ code: "VERTZ_MW_MISSING_NAME",
881
+ message: "Middleware must have a 'name' property.",
882
+ suggestion: "Add a 'name' property to the middleware config."
883
+ }));
884
+ continue;
885
+ }
886
+ const name = getStringValue(nameExpr);
887
+ if (!name) {
888
+ this.addDiagnostic(createDiagnosticFromLocation(loc, {
889
+ severity: "warning",
890
+ code: "VERTZ_MW_DYNAMIC_NAME",
891
+ message: "Middleware name should be a string literal for static analysis.",
892
+ suggestion: "Use a string literal for the middleware name."
893
+ }));
894
+ continue;
895
+ }
896
+ const handlerExpr = getPropertyValue(obj, "handler");
897
+ if (!handlerExpr) {
898
+ this.addDiagnostic(createDiagnosticFromLocation(loc, {
899
+ severity: "error",
900
+ code: "VERTZ_MW_MISSING_HANDLER",
901
+ message: "Middleware must have a 'handler' property.",
902
+ suggestion: "Add a 'handler' property to the middleware config."
903
+ }));
904
+ continue;
905
+ }
906
+ const injectExpr = getPropertyValue(obj, "inject");
907
+ const inject = injectExpr?.isKind(SyntaxKind6.ObjectLiteralExpression) ? parseInjectRefs(injectExpr) : [];
908
+ const filePath = file.getFilePath();
909
+ const headers = this.resolveSchemaRef(obj, "headers", filePath);
910
+ const params = this.resolveSchemaRef(obj, "params", filePath);
911
+ const query = this.resolveSchemaRef(obj, "query", filePath);
912
+ const body = this.resolveSchemaRef(obj, "body", filePath);
913
+ const requires = this.resolveSchemaRef(obj, "requires", filePath);
914
+ const provides = this.resolveSchemaRef(obj, "provides", filePath);
915
+ middleware.push({
916
+ name,
917
+ ...loc,
918
+ inject,
919
+ headers,
920
+ params,
921
+ query,
922
+ body,
923
+ requires,
924
+ provides
925
+ });
926
+ }
927
+ }
928
+ return { middleware };
929
+ }
930
+ resolveSchemaRef(obj, prop, filePath) {
931
+ const expr = getPropertyValue(obj, prop);
932
+ if (!expr)
933
+ return;
934
+ if (expr.isKind(SyntaxKind6.Identifier)) {
935
+ const resolved = resolveIdentifier(expr, this.project);
936
+ const resolvedPath = resolved ? resolved.sourceFile.getFilePath() : filePath;
937
+ return createNamedSchemaRef(expr.getText(), resolvedPath);
938
+ }
939
+ if (isSchemaExpression(expr.getSourceFile(), expr)) {
940
+ return createInlineSchemaRef(filePath);
941
+ }
942
+ return;
943
+ }
944
+ }
945
+ // src/analyzers/module-analyzer.ts
946
+ import { SyntaxKind as SyntaxKind7 } from "ts-morph";
947
+ class ModuleAnalyzer extends BaseAnalyzer {
948
+ async analyze() {
949
+ const modules = [];
950
+ const defVarToIndex = new Map;
951
+ for (const file of this.project.getSourceFiles()) {
952
+ const defCalls = findCallExpressions(file, "vertz", "moduleDef");
953
+ for (const call of defCalls) {
954
+ const obj = extractObjectLiteral(call, 0);
955
+ if (!obj)
956
+ continue;
957
+ const nameExpr = getPropertyValue(obj, "name");
958
+ const name = nameExpr ? getStringValue(nameExpr) : null;
959
+ if (!name) {
960
+ this.addDiagnostic(createDiagnosticFromLocation(getSourceLocation(call), {
961
+ severity: "error",
962
+ code: "VERTZ_MODULE_DYNAMIC_NAME",
963
+ message: "vertz.moduleDef() requires a static string `name` property."
964
+ }));
965
+ continue;
966
+ }
967
+ const varName = getVariableNameForCall(call);
968
+ const importsExpr = getPropertyValue(obj, "imports");
969
+ const imports = importsExpr?.isKind(SyntaxKind7.ObjectLiteralExpression) ? parseImports(importsExpr) : [];
970
+ const optionsExpr = getPropertyValue(obj, "options");
971
+ let options;
972
+ if (optionsExpr?.isKind(SyntaxKind7.Identifier)) {
973
+ options = createNamedSchemaRef(optionsExpr.getText(), file.getFilePath());
974
+ }
975
+ const loc = getSourceLocation(call);
976
+ const idx = modules.length;
977
+ modules.push({
978
+ name,
979
+ ...loc,
980
+ imports,
981
+ options,
982
+ services: [],
983
+ routers: [],
984
+ exports: []
985
+ });
986
+ if (varName) {
987
+ defVarToIndex.set(varName, idx);
988
+ }
989
+ }
990
+ }
991
+ for (const file of this.project.getSourceFiles()) {
992
+ const moduleCalls = findCallExpressions(file, "vertz", "module");
993
+ for (const call of moduleCalls) {
994
+ const args = call.getArguments();
995
+ if (args.length < 2)
996
+ continue;
997
+ const defArg = args.at(0);
998
+ if (!defArg?.isKind(SyntaxKind7.Identifier))
999
+ continue;
1000
+ const defVarName = defArg.getText();
1001
+ const idx = defVarToIndex.get(defVarName);
1002
+ if (idx === undefined)
1003
+ continue;
1004
+ const mod = modules.at(idx);
1005
+ if (!mod)
1006
+ continue;
1007
+ const assemblyObj = extractObjectLiteral(call, 1);
1008
+ if (!assemblyObj)
1009
+ continue;
1010
+ const exportsExpr = getPropertyValue(assemblyObj, "exports");
1011
+ if (exportsExpr) {
1012
+ mod.exports = extractIdentifierNames(exportsExpr);
1013
+ }
1014
+ }
1015
+ }
1016
+ return { modules };
1017
+ }
1018
+ }
1019
+ function parseImports(obj) {
1020
+ return getProperties(obj).map(({ name }) => ({
1021
+ localName: name,
1022
+ isEnvImport: false
1023
+ }));
1024
+ }
1025
+ function extractIdentifierNames(expr) {
1026
+ if (!expr.isKind(SyntaxKind7.ArrayLiteralExpression))
1027
+ return [];
1028
+ return expr.getElements().filter((e) => e.isKind(SyntaxKind7.Identifier)).map((e) => e.getText());
1029
+ }
1030
+ // src/analyzers/route-analyzer.ts
1031
+ import { SyntaxKind as SyntaxKind8 } from "ts-morph";
1032
+ var HTTP_METHODS = {
1033
+ get: "GET",
1034
+ post: "POST",
1035
+ put: "PUT",
1036
+ patch: "PATCH",
1037
+ delete: "DELETE",
1038
+ head: "HEAD"
1039
+ };
1040
+
1041
+ class RouteAnalyzer extends BaseAnalyzer {
1042
+ async analyze() {
1043
+ return { routers: [] };
1044
+ }
1045
+ async analyzeForModules(context) {
1046
+ const routers = [];
1047
+ const knownModuleDefVars = new Set(context.moduleDefVariables.keys());
1048
+ for (const file of this.project.getSourceFiles()) {
1049
+ for (const [moduleDefVar, moduleName] of context.moduleDefVariables) {
1050
+ const routerCalls = findMethodCallsOnVariable(file, moduleDefVar, "router");
1051
+ for (const call of routerCalls) {
1052
+ const varName = getVariableNameForCall(call);
1053
+ if (!varName)
1054
+ continue;
1055
+ const obj = extractObjectLiteral(call, 0);
1056
+ const prefixExpr = obj ? getPropertyValue(obj, "prefix") : null;
1057
+ const prefix = prefixExpr ? getStringValue(prefixExpr) ?? "/" : "/";
1058
+ if (!prefixExpr) {
1059
+ this.addDiagnostic(createDiagnosticFromLocation(getSourceLocation(call), {
1060
+ severity: "warning",
1061
+ code: "VERTZ_RT_MISSING_PREFIX",
1062
+ message: "Router should have a 'prefix' property.",
1063
+ suggestion: "Add a 'prefix' property to the router config."
1064
+ }));
1065
+ }
1066
+ const loc = getSourceLocation(call);
1067
+ const injectExpr = obj ? getPropertyValue(obj, "inject") : null;
1068
+ const inject = injectExpr?.isKind(SyntaxKind8.ObjectLiteralExpression) ? parseInjectRefs(injectExpr) : [];
1069
+ const routes = this.extractRoutes(file, varName, prefix, moduleName);
1070
+ routers.push({
1071
+ name: varName,
1072
+ moduleName,
1073
+ ...loc,
1074
+ prefix,
1075
+ inject,
1076
+ routes
1077
+ });
1078
+ }
1079
+ }
1080
+ this.detectUnknownRouterCalls(file, knownModuleDefVars);
1081
+ }
1082
+ return { routers };
1083
+ }
1084
+ detectUnknownRouterCalls(file, knownModuleDefVars) {
1085
+ const allCalls = file.getDescendantsOfKind(SyntaxKind8.CallExpression);
1086
+ for (const call of allCalls) {
1087
+ const expr = call.getExpression();
1088
+ if (!expr.isKind(SyntaxKind8.PropertyAccessExpression))
1089
+ continue;
1090
+ if (expr.getName() !== "router")
1091
+ continue;
1092
+ const obj = expr.getExpression();
1093
+ if (!obj.isKind(SyntaxKind8.Identifier))
1094
+ continue;
1095
+ if (knownModuleDefVars.has(obj.getText()))
1096
+ continue;
1097
+ const varName = getVariableNameForCall(call);
1098
+ if (!varName)
1099
+ continue;
1100
+ const hasHttpMethodCalls = Object.keys(HTTP_METHODS).some((method) => findMethodCallsOnVariable(file, varName, method).length > 0);
1101
+ if (!hasHttpMethodCalls)
1102
+ continue;
1103
+ this.addDiagnostic(createDiagnosticFromLocation(getSourceLocation(call), {
1104
+ severity: "error",
1105
+ code: "VERTZ_RT_UNKNOWN_MODULE_DEF",
1106
+ message: `'${obj.getText()}' is not a known moduleDef variable.`,
1107
+ suggestion: "Ensure the variable is declared with vertz.moduleDef() and is included in the module context."
1108
+ }));
1109
+ }
1110
+ }
1111
+ extractRoutes(file, routerVarName, prefix, moduleName) {
1112
+ const routes = [];
1113
+ const usedOperationIds = new Set;
1114
+ for (const [methodName, httpMethod] of Object.entries(HTTP_METHODS)) {
1115
+ const directCalls = findMethodCallsOnVariable(file, routerVarName, methodName);
1116
+ const chainedCalls = this.findChainedHttpCalls(file, routerVarName, methodName);
1117
+ const allCalls = [...directCalls, ...chainedCalls];
1118
+ for (const call of allCalls) {
1119
+ const route = this.extractRoute(call, httpMethod, prefix, moduleName, file, usedOperationIds);
1120
+ if (route)
1121
+ routes.push(route);
1122
+ }
1123
+ }
1124
+ return routes;
1125
+ }
1126
+ findChainedHttpCalls(file, routerVarName, methodName) {
1127
+ return file.getDescendantsOfKind(SyntaxKind8.CallExpression).filter((call) => {
1128
+ const expr = call.getExpression();
1129
+ if (!expr.isKind(SyntaxKind8.PropertyAccessExpression))
1130
+ return false;
1131
+ if (expr.getName() !== methodName)
1132
+ return false;
1133
+ const obj = expr.getExpression();
1134
+ if (!obj.isKind(SyntaxKind8.CallExpression))
1135
+ return false;
1136
+ return this.chainResolvesToVariable(obj, routerVarName);
1137
+ });
1138
+ }
1139
+ chainResolvesToVariable(expr, varName) {
1140
+ if (expr.isKind(SyntaxKind8.Identifier)) {
1141
+ return expr.getText() === varName;
1142
+ }
1143
+ if (expr.isKind(SyntaxKind8.CallExpression)) {
1144
+ const inner = expr.getExpression();
1145
+ if (inner.isKind(SyntaxKind8.PropertyAccessExpression)) {
1146
+ return this.chainResolvesToVariable(inner.getExpression(), varName);
1147
+ }
1148
+ }
1149
+ return false;
1150
+ }
1151
+ extractRoute(call, method, prefix, moduleName, file, usedOperationIds) {
1152
+ const args = call.getArguments();
1153
+ const pathArg = args[0];
1154
+ if (!pathArg)
1155
+ return null;
1156
+ const path = getStringValue(pathArg);
1157
+ if (path === null) {
1158
+ this.addDiagnostic(createDiagnosticFromLocation(getSourceLocation(call), {
1159
+ severity: "error",
1160
+ code: "VERTZ_RT_DYNAMIC_PATH",
1161
+ message: "Route paths must be string literals for static analysis.",
1162
+ suggestion: "Use a string literal for the route path."
1163
+ }));
1164
+ return null;
1165
+ }
1166
+ const fullPath = joinPaths(prefix, path);
1167
+ const loc = getSourceLocation(call);
1168
+ const filePath = file.getFilePath();
1169
+ const obj = extractObjectLiteral(call, 1);
1170
+ if (!obj && args.length > 1) {
1171
+ this.addDiagnostic(createDiagnosticFromLocation(loc, {
1172
+ severity: "warning",
1173
+ code: "VERTZ_RT_DYNAMIC_CONFIG",
1174
+ message: "Route config must be an object literal for static analysis.",
1175
+ suggestion: "Pass an inline object literal as the second argument."
1176
+ }));
1177
+ }
1178
+ const params = obj ? this.resolveSchemaRef(obj, "params", filePath) : undefined;
1179
+ const query = obj ? this.resolveSchemaRef(obj, "query", filePath) : undefined;
1180
+ const body = obj ? this.resolveSchemaRef(obj, "body", filePath) : undefined;
1181
+ const headers = obj ? this.resolveSchemaRef(obj, "headers", filePath) : undefined;
1182
+ const response = obj ? this.resolveSchemaRef(obj, "response", filePath) : undefined;
1183
+ const middleware = obj ? this.extractMiddlewareRefs(obj, filePath) : [];
1184
+ const descriptionExpr = obj ? getPropertyValue(obj, "description") : null;
1185
+ const description = descriptionExpr ? getStringValue(descriptionExpr) ?? undefined : undefined;
1186
+ const tagsExpr = obj ? getPropertyValue(obj, "tags") : null;
1187
+ const tags = tagsExpr ? getArrayElements(tagsExpr).map((e) => getStringValue(e)).filter((v) => v !== null) : [];
1188
+ const handlerExpr = obj ? getPropertyValue(obj, "handler") : null;
1189
+ const operationId = this.generateOperationId(moduleName, method, path, handlerExpr, usedOperationIds);
1190
+ if (obj && !handlerExpr) {
1191
+ this.addDiagnostic(createDiagnosticFromLocation(loc, {
1192
+ severity: "error",
1193
+ code: "VERTZ_RT_MISSING_HANDLER",
1194
+ message: "Route must have a 'handler' property.",
1195
+ suggestion: "Add a 'handler' property to the route config."
1196
+ }));
1197
+ return null;
1198
+ }
1199
+ return {
1200
+ method,
1201
+ path,
1202
+ fullPath,
1203
+ ...loc,
1204
+ operationId,
1205
+ params,
1206
+ query,
1207
+ body,
1208
+ headers,
1209
+ response,
1210
+ middleware,
1211
+ description,
1212
+ tags
1213
+ };
1214
+ }
1215
+ resolveSchemaRef(obj, prop, filePath) {
1216
+ const expr = getPropertyValue(obj, prop);
1217
+ if (!expr)
1218
+ return;
1219
+ if (expr.isKind(SyntaxKind8.Identifier)) {
1220
+ const resolved = resolveIdentifier(expr, this.project);
1221
+ const resolvedPath = resolved ? resolved.sourceFile.getFilePath() : filePath;
1222
+ return createNamedSchemaRef(expr.getText(), resolvedPath);
1223
+ }
1224
+ if (isSchemaExpression(expr.getSourceFile(), expr)) {
1225
+ return createInlineSchemaRef(filePath);
1226
+ }
1227
+ return;
1228
+ }
1229
+ extractMiddlewareRefs(obj, filePath) {
1230
+ const expr = getPropertyValue(obj, "middlewares");
1231
+ if (!expr)
1232
+ return [];
1233
+ const elements = getArrayElements(expr);
1234
+ return elements.filter((el) => el.isKind(SyntaxKind8.Identifier)).map((el) => {
1235
+ const resolved = resolveIdentifier(el, this.project);
1236
+ return {
1237
+ name: el.getText(),
1238
+ sourceFile: resolved ? resolved.sourceFile.getFilePath() : filePath
1239
+ };
1240
+ });
1241
+ }
1242
+ generateOperationId(moduleName, method, path, handlerExpr, usedIds) {
1243
+ let handlerName = null;
1244
+ if (handlerExpr?.isKind(SyntaxKind8.Identifier)) {
1245
+ handlerName = handlerExpr.getText();
1246
+ } else if (handlerExpr?.isKind(SyntaxKind8.PropertyAccessExpression)) {
1247
+ handlerName = handlerExpr.getName();
1248
+ }
1249
+ const id = handlerName ? `${moduleName}_${handlerName}` : `${moduleName}_${method.toLowerCase()}_${sanitizePath(path)}`;
1250
+ if (!usedIds.has(id)) {
1251
+ usedIds.add(id);
1252
+ return id;
1253
+ }
1254
+ let counter = 2;
1255
+ while (usedIds.has(`${id}_${counter}`))
1256
+ counter++;
1257
+ const uniqueId = `${id}_${counter}`;
1258
+ usedIds.add(uniqueId);
1259
+ return uniqueId;
1260
+ }
1261
+ }
1262
+ function joinPaths(prefix, path) {
1263
+ const normalizedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1264
+ if (path === "/")
1265
+ return normalizedPrefix || "/";
1266
+ return normalizedPrefix + path;
1267
+ }
1268
+ function sanitizePath(path) {
1269
+ return path.replace(/^\//, "").replace(/[/:.]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "") || "root";
1270
+ }
1271
+ // src/compiler.ts
1272
+ import { Project } from "ts-morph";
1273
+
1274
+ // src/config.ts
1275
+ function defineConfig(config) {
1276
+ return config;
1277
+ }
1278
+ function resolveConfig(config) {
1279
+ return {
1280
+ strict: config?.strict ?? false,
1281
+ forceGenerate: config?.forceGenerate ?? false,
1282
+ compiler: {
1283
+ sourceDir: config?.compiler?.sourceDir ?? "src",
1284
+ outputDir: config?.compiler?.outputDir ?? ".vertz/generated",
1285
+ entryFile: config?.compiler?.entryFile ?? "src/app.ts",
1286
+ schemas: {
1287
+ enforceNaming: config?.compiler?.schemas?.enforceNaming ?? true,
1288
+ enforcePlacement: config?.compiler?.schemas?.enforcePlacement ?? true
1289
+ },
1290
+ openapi: {
1291
+ output: config?.compiler?.openapi?.output ?? ".vertz/generated/openapi.json",
1292
+ info: {
1293
+ title: config?.compiler?.openapi?.info?.title ?? "Vertz API",
1294
+ version: config?.compiler?.openapi?.info?.version ?? "1.0.0",
1295
+ description: config?.compiler?.openapi?.info?.description
1296
+ }
1297
+ },
1298
+ validation: {
1299
+ requireResponseSchema: config?.compiler?.validation?.requireResponseSchema ?? true,
1300
+ detectDeadCode: config?.compiler?.validation?.detectDeadCode ?? true
1301
+ }
1302
+ }
1303
+ };
1304
+ }
1305
+
1306
+ // src/generators/boot-generator.ts
1307
+ import { writeFile } from "node:fs/promises";
1308
+ import { relative } from "node:path";
1309
+
1310
+ // src/generators/base-generator.ts
1311
+ import { join } from "node:path";
1312
+
1313
+ class BaseGenerator {
1314
+ config;
1315
+ constructor(config) {
1316
+ this.config = config;
1317
+ }
1318
+ resolveOutputPath(outputDir, fileName) {
1319
+ return join(outputDir, fileName);
1320
+ }
1321
+ }
1322
+
1323
+ // src/generators/boot-generator.ts
1324
+ function buildBootManifest(ir) {
1325
+ const moduleMap = new Map(ir.modules.map((m) => [m.name, m]));
1326
+ const registrationMap = new Map(ir.app.moduleRegistrations.map((r) => [r.moduleName, r]));
1327
+ const modules = [];
1328
+ for (const name of ir.dependencyGraph.initializationOrder) {
1329
+ const mod = moduleMap.get(name);
1330
+ if (!mod)
1331
+ continue;
1332
+ const reg = registrationMap.get(name);
1333
+ modules.push({
1334
+ name: mod.name,
1335
+ importPath: mod.sourceFile,
1336
+ variableName: `${mod.name}Module`,
1337
+ ...reg?.options && { options: reg.options }
1338
+ });
1339
+ }
1340
+ const globalMiddleware = ir.app.globalMiddleware.map((ref) => ({
1341
+ name: ref.name,
1342
+ importPath: ref.sourceFile,
1343
+ variableName: ref.name
1344
+ }));
1345
+ return {
1346
+ initializationOrder: ir.dependencyGraph.initializationOrder,
1347
+ modules,
1348
+ globalMiddleware
1349
+ };
1350
+ }
1351
+ function resolveImportPath(from, to) {
1352
+ const rel = relative(from, to).replace(/\.ts$/, "");
1353
+ return rel.startsWith(".") ? rel : `./${rel}`;
1354
+ }
1355
+ function renderBootFile(manifest, outputDir) {
1356
+ const lines = [];
1357
+ lines.push("// Auto-generated by @vertz/compiler — do not edit");
1358
+ for (const mod of manifest.modules) {
1359
+ const importPath = resolveImportPath(outputDir, mod.importPath);
1360
+ lines.push(`import { ${mod.variableName} } from '${importPath}';`);
1361
+ }
1362
+ for (const mw of manifest.globalMiddleware) {
1363
+ const importPath = resolveImportPath(outputDir, mw.importPath);
1364
+ lines.push(`import { ${mw.variableName} } from '${importPath}';`);
1365
+ }
1366
+ lines.push("");
1367
+ lines.push("export const bootSequence = {");
1368
+ lines.push(` initializationOrder: [${manifest.initializationOrder.map((n) => `'${n}'`).join(", ")}],`);
1369
+ lines.push(" modules: {");
1370
+ for (const mod of manifest.modules) {
1371
+ if (mod.options) {
1372
+ lines.push(` ${mod.name}: { module: ${mod.variableName}, options: ${JSON.stringify(mod.options)} },`);
1373
+ } else {
1374
+ lines.push(` ${mod.name}: { module: ${mod.variableName} },`);
1375
+ }
1376
+ }
1377
+ lines.push(" },");
1378
+ lines.push(` globalMiddleware: [${manifest.globalMiddleware.map((mw) => mw.variableName).join(", ")}],`);
1379
+ lines.push("} as const;");
1380
+ lines.push("");
1381
+ return lines.join(`
1382
+ `);
1383
+ }
1384
+
1385
+ class BootGenerator extends BaseGenerator {
1386
+ name = "boot";
1387
+ async generate(ir, outputDir) {
1388
+ const manifest = buildBootManifest(ir);
1389
+ const content = renderBootFile(manifest, outputDir);
1390
+ const outputPath = this.resolveOutputPath(outputDir, "boot.ts");
1391
+ await writeFile(outputPath, content);
1392
+ }
1393
+ }
1394
+
1395
+ // src/generators/manifest-generator.ts
1396
+ import { writeFile as writeFile2 } from "node:fs/promises";
1397
+ function groupImportsByModule(imports) {
1398
+ const groups = new Map;
1399
+ for (const imp of imports) {
1400
+ if (!imp.sourceModule)
1401
+ continue;
1402
+ const existing = groups.get(imp.sourceModule) ?? [];
1403
+ existing.push(imp.localName);
1404
+ groups.set(imp.sourceModule, existing);
1405
+ }
1406
+ return Array.from(groups.entries()).map(([from, items]) => ({ from, items }));
1407
+ }
1408
+ function resolveSchemaRef(ref) {
1409
+ if (!ref)
1410
+ return;
1411
+ if (ref.kind === "named")
1412
+ return { $ref: `#/schemas/${ref.schemaName}` };
1413
+ return ref.jsonSchema;
1414
+ }
1415
+ function buildManifest(ir) {
1416
+ return {
1417
+ version: "1.0.0",
1418
+ app: {
1419
+ basePath: ir.app.basePath,
1420
+ ...ir.app.version && { version: ir.app.version }
1421
+ },
1422
+ modules: ir.modules.map((mod) => ({
1423
+ name: mod.name,
1424
+ services: mod.services.map((s) => s.name),
1425
+ routers: mod.routers.map((r) => r.name),
1426
+ exports: mod.exports,
1427
+ imports: groupImportsByModule(mod.imports)
1428
+ })),
1429
+ routes: ir.modules.flatMap((mod) => mod.routers.flatMap((router) => router.routes.map((route) => {
1430
+ const params = resolveSchemaRef(route.params);
1431
+ const query = resolveSchemaRef(route.query);
1432
+ const body = resolveSchemaRef(route.body);
1433
+ const headers = resolveSchemaRef(route.headers);
1434
+ const response = resolveSchemaRef(route.response);
1435
+ return {
1436
+ method: route.method,
1437
+ path: route.fullPath,
1438
+ operationId: route.operationId,
1439
+ module: mod.name,
1440
+ router: router.name,
1441
+ middleware: route.middleware.map((m) => m.name),
1442
+ ...params && { params },
1443
+ ...query && { query },
1444
+ ...body && { body },
1445
+ ...headers && { headers },
1446
+ ...response && { response }
1447
+ };
1448
+ }))),
1449
+ schemas: Object.fromEntries(ir.schemas.filter((s) => s.isNamed && s.jsonSchema).map((s) => [s.name, s.jsonSchema])),
1450
+ middleware: ir.middleware.map((mw) => ({
1451
+ name: mw.name,
1452
+ ...mw.provides?.jsonSchema && { provides: mw.provides.jsonSchema },
1453
+ ...mw.requires?.jsonSchema && { requires: mw.requires.jsonSchema }
1454
+ })),
1455
+ dependencyGraph: {
1456
+ initializationOrder: ir.dependencyGraph.initializationOrder,
1457
+ edges: ir.dependencyGraph.edges.map((e) => ({
1458
+ from: e.from,
1459
+ to: e.to,
1460
+ type: e.kind
1461
+ }))
1462
+ },
1463
+ diagnostics: {
1464
+ errors: ir.diagnostics.filter((d) => d.severity === "error").length,
1465
+ warnings: ir.diagnostics.filter((d) => d.severity === "warning").length,
1466
+ items: ir.diagnostics.map((d) => ({
1467
+ severity: d.severity,
1468
+ code: d.code,
1469
+ message: d.message,
1470
+ ...d.file && { file: d.file },
1471
+ ...d.line && { line: d.line },
1472
+ ...d.suggestion && { suggestion: d.suggestion }
1473
+ }))
1474
+ }
1475
+ };
1476
+ }
1477
+
1478
+ class ManifestGenerator extends BaseGenerator {
1479
+ name = "manifest";
1480
+ async generate(ir, outputDir) {
1481
+ const manifest = buildManifest(ir);
1482
+ const content = JSON.stringify(manifest, null, 2);
1483
+ const outputPath = this.resolveOutputPath(outputDir, "manifest.json");
1484
+ await writeFile2(outputPath, content);
1485
+ }
1486
+ }
1487
+
1488
+ // src/generators/openapi-generator.ts
1489
+ import { writeFile as writeFile3 } from "node:fs/promises";
1490
+ class OpenAPIGenerator extends BaseGenerator {
1491
+ name = "openapi";
1492
+ async generate(ir, outputDir) {
1493
+ const doc = this.buildDocument(ir);
1494
+ const outputPath = this.resolveOutputPath(outputDir, "openapi.json");
1495
+ await writeFile3(outputPath, JSON.stringify(doc, null, 2));
1496
+ }
1497
+ buildDocument(ir) {
1498
+ const version = ir.app.version ?? this.config.compiler.openapi.info.version;
1499
+ const { description } = this.config.compiler.openapi.info;
1500
+ const middlewareMap = new Map;
1501
+ for (const mw of ir.middleware) {
1502
+ middlewareMap.set(mw.name, mw);
1503
+ }
1504
+ const paths = {};
1505
+ const components = {};
1506
+ for (const mod of ir.modules) {
1507
+ for (const router of mod.routers) {
1508
+ for (const route of router.routes) {
1509
+ const pathKey = this.convertPath(route.fullPath);
1510
+ if (!paths[pathKey])
1511
+ paths[pathKey] = {};
1512
+ const method = route.method.toLowerCase();
1513
+ const operation = this.buildOperation(route, middlewareMap, components);
1514
+ paths[pathKey][method] = operation;
1515
+ }
1516
+ }
1517
+ }
1518
+ for (const schema of ir.schemas) {
1519
+ if (schema.isNamed && schema.id && schema.jsonSchema) {
1520
+ components[schema.id] = schema.jsonSchema;
1521
+ }
1522
+ }
1523
+ const info = {
1524
+ title: this.config.compiler.openapi.info.title,
1525
+ version
1526
+ };
1527
+ if (description) {
1528
+ info.description = description;
1529
+ }
1530
+ return {
1531
+ openapi: "3.1.0",
1532
+ info,
1533
+ servers: [{ url: ir.app.basePath || "/" }],
1534
+ paths,
1535
+ components: { schemas: components },
1536
+ tags: this.collectTags(ir)
1537
+ };
1538
+ }
1539
+ buildOperation(route, middlewareMap, components) {
1540
+ const operation = {
1541
+ operationId: route.operationId,
1542
+ tags: route.tags,
1543
+ parameters: this.buildParameters(route, middlewareMap),
1544
+ responses: this.buildResponses(route, components)
1545
+ };
1546
+ if (route.description) {
1547
+ operation.description = route.description;
1548
+ }
1549
+ if (route.body) {
1550
+ operation.requestBody = {
1551
+ required: true,
1552
+ content: {
1553
+ "application/json": { schema: this.resolveAndLift(route.body, components) }
1554
+ }
1555
+ };
1556
+ }
1557
+ return operation;
1558
+ }
1559
+ buildResponses(route, components) {
1560
+ if (!route.response) {
1561
+ if (route.method === "DELETE") {
1562
+ return { "204": { description: "No Content" } };
1563
+ }
1564
+ return { "200": { description: "OK" } };
1565
+ }
1566
+ const statusCode = this.getSuccessStatusCode(route.method);
1567
+ return {
1568
+ [statusCode]: {
1569
+ description: "OK",
1570
+ content: {
1571
+ "application/json": { schema: this.resolveAndLift(route.response, components) }
1572
+ }
1573
+ }
1574
+ };
1575
+ }
1576
+ resolveAndLift(schemaRef, components) {
1577
+ const resolved = this.resolveSchemaRef(schemaRef);
1578
+ if (schemaRef.kind === "inline" && resolved.$defs) {
1579
+ return this.liftDefsToComponents(resolved, components);
1580
+ }
1581
+ return resolved;
1582
+ }
1583
+ getSuccessStatusCode(method) {
1584
+ if (method === "POST")
1585
+ return "201";
1586
+ return "200";
1587
+ }
1588
+ convertPath(routePath) {
1589
+ return routePath.replace(/:(\w+)/g, "{$1}");
1590
+ }
1591
+ buildParameters(route, middlewareMap) {
1592
+ const params = [];
1593
+ const headerMap = new Map;
1594
+ for (const mwRef of route.middleware) {
1595
+ const mw = middlewareMap.get(mwRef.name);
1596
+ if (mw?.headers?.jsonSchema) {
1597
+ this.extractHeaderParams(mw.headers.jsonSchema, headerMap);
1598
+ }
1599
+ }
1600
+ if (route.params?.jsonSchema) {
1601
+ const schema = route.params.jsonSchema;
1602
+ for (const [name, propSchema] of Object.entries(schema.properties ?? {})) {
1603
+ params.push({ name, in: "path", required: true, schema: propSchema });
1604
+ }
1605
+ }
1606
+ if (route.query?.jsonSchema) {
1607
+ this.extractParamsFromSchema(route.query.jsonSchema, "query", params);
1608
+ }
1609
+ if (route.headers?.jsonSchema) {
1610
+ this.extractHeaderParams(route.headers.jsonSchema, headerMap);
1611
+ }
1612
+ params.push(...headerMap.values());
1613
+ return params;
1614
+ }
1615
+ resolveSchemaRef(schemaRef) {
1616
+ if (schemaRef.kind === "named") {
1617
+ return { $ref: `#/components/schemas/${schemaRef.schemaName}` };
1618
+ }
1619
+ return schemaRef.jsonSchema ?? {};
1620
+ }
1621
+ extractParamsFromSchema(schema, location, target) {
1622
+ const required = new Set(schema.required ?? []);
1623
+ for (const [name, propSchema] of Object.entries(schema.properties ?? {})) {
1624
+ target.push({ name, in: location, required: required.has(name), schema: propSchema });
1625
+ }
1626
+ }
1627
+ extractHeaderParams(schema, headerMap) {
1628
+ const required = new Set(schema.required ?? []);
1629
+ for (const [name, propSchema] of Object.entries(schema.properties ?? {})) {
1630
+ headerMap.set(name, { name, in: "header", required: required.has(name), schema: propSchema });
1631
+ }
1632
+ }
1633
+ liftDefsToComponents(schema, components) {
1634
+ const result = { ...schema };
1635
+ if (result.$defs) {
1636
+ for (const [name, defSchema] of Object.entries(result.$defs)) {
1637
+ const lifted = this.liftDefsToComponents(defSchema, components);
1638
+ let targetName = name;
1639
+ if (components[name] && JSON.stringify(components[name]) !== JSON.stringify(lifted)) {
1640
+ let suffix = 2;
1641
+ while (components[`${name}_${suffix}`])
1642
+ suffix++;
1643
+ targetName = `${name}_${suffix}`;
1644
+ }
1645
+ components[targetName] = lifted;
1646
+ }
1647
+ delete result.$defs;
1648
+ }
1649
+ return this.rewriteRefs(result);
1650
+ }
1651
+ rewriteRefs(schema) {
1652
+ const result = { ...schema };
1653
+ if (result.$ref?.startsWith("#/$defs/")) {
1654
+ result.$ref = result.$ref.replace("#/$defs/", "#/components/schemas/");
1655
+ }
1656
+ if (result.properties) {
1657
+ const newProps = {};
1658
+ for (const [key, value] of Object.entries(result.properties)) {
1659
+ newProps[key] = this.rewriteRefs(value);
1660
+ }
1661
+ result.properties = newProps;
1662
+ }
1663
+ if (result.items) {
1664
+ result.items = this.rewriteRefs(result.items);
1665
+ }
1666
+ for (const key of ["oneOf", "allOf", "anyOf"]) {
1667
+ if (result[key]) {
1668
+ result[key] = result[key].map((s) => this.rewriteRefs(s));
1669
+ }
1670
+ }
1671
+ if (result.additionalProperties && typeof result.additionalProperties === "object") {
1672
+ result.additionalProperties = this.rewriteRefs(result.additionalProperties);
1673
+ }
1674
+ return result;
1675
+ }
1676
+ collectTags(ir) {
1677
+ const tagNames = new Set;
1678
+ for (const mod of ir.modules) {
1679
+ for (const router of mod.routers) {
1680
+ for (const route of router.routes) {
1681
+ for (const tag of route.tags) {
1682
+ tagNames.add(tag);
1683
+ }
1684
+ }
1685
+ }
1686
+ }
1687
+ return [...tagNames].sort().map((name) => ({ name }));
1688
+ }
1689
+ }
1690
+
1691
+ // src/generators/route-table-generator.ts
1692
+ import { writeFile as writeFile4 } from "node:fs/promises";
1693
+ function schemaRefName(ref) {
1694
+ if (!ref || ref.kind !== "named")
1695
+ return;
1696
+ return ref.schemaName;
1697
+ }
1698
+ function buildRouteTable(ir) {
1699
+ const routes = [];
1700
+ for (const mod of ir.modules) {
1701
+ for (const router of mod.routers) {
1702
+ for (const route of router.routes) {
1703
+ routes.push({
1704
+ method: route.method,
1705
+ path: route.fullPath,
1706
+ operationId: route.operationId,
1707
+ moduleName: mod.name,
1708
+ routerName: router.name,
1709
+ middleware: route.middleware.map((m) => m.name),
1710
+ schemas: {
1711
+ params: schemaRefName(route.params),
1712
+ query: schemaRefName(route.query),
1713
+ body: schemaRefName(route.body),
1714
+ headers: schemaRefName(route.headers),
1715
+ response: schemaRefName(route.response)
1716
+ }
1717
+ });
1718
+ }
1719
+ }
1720
+ }
1721
+ routes.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
1722
+ return { routes };
1723
+ }
1724
+ function renderRouteTableFile(manifest) {
1725
+ const lines = [
1726
+ "// Auto-generated by @vertz/compiler — do not edit",
1727
+ "import type { HttpMethod } from '@vertz/compiler';",
1728
+ "",
1729
+ "export interface RouteTableEntry {",
1730
+ " method: HttpMethod;",
1731
+ " path: string;",
1732
+ " operationId: string;",
1733
+ " moduleName: string;",
1734
+ " routerName: string;",
1735
+ " middleware: string[];",
1736
+ " schemas: {",
1737
+ " params?: string;",
1738
+ " query?: string;",
1739
+ " body?: string;",
1740
+ " headers?: string;",
1741
+ " response?: string;",
1742
+ " };",
1743
+ "}",
1744
+ "",
1745
+ "export const routeTable: RouteTableEntry[] = ["
1746
+ ];
1747
+ for (const route of manifest.routes) {
1748
+ lines.push(" {");
1749
+ lines.push(` method: '${route.method}',`);
1750
+ lines.push(` path: '${route.path}',`);
1751
+ lines.push(` operationId: '${route.operationId}',`);
1752
+ lines.push(` moduleName: '${route.moduleName}',`);
1753
+ lines.push(` routerName: '${route.routerName}',`);
1754
+ lines.push(` middleware: [${route.middleware.map((m) => `'${m}'`).join(", ")}],`);
1755
+ lines.push(" schemas: {");
1756
+ for (const [key, value] of Object.entries(route.schemas)) {
1757
+ if (value !== undefined) {
1758
+ lines.push(` ${key}: '${value}',`);
1759
+ }
1760
+ }
1761
+ lines.push(" },");
1762
+ lines.push(" },");
1763
+ }
1764
+ lines.push("];");
1765
+ lines.push("");
1766
+ return lines.join(`
1767
+ `);
1768
+ }
1769
+
1770
+ class RouteTableGenerator extends BaseGenerator {
1771
+ name = "route-table";
1772
+ async generate(ir, outputDir) {
1773
+ const manifest = buildRouteTable(ir);
1774
+ const content = renderRouteTableFile(manifest);
1775
+ const outputPath = this.resolveOutputPath(outputDir, "routes.ts");
1776
+ await writeFile4(outputPath, content);
1777
+ }
1778
+ }
1779
+
1780
+ // src/generators/schema-registry-generator.ts
1781
+ import { writeFile as writeFile5 } from "node:fs/promises";
1782
+ function buildSchemaRegistry(ir) {
1783
+ const schemas = ir.schemas.filter((s) => s.isNamed).map((s) => ({
1784
+ name: s.name,
1785
+ ...s.id && { id: s.id },
1786
+ importPath: s.sourceFile,
1787
+ variableName: s.name,
1788
+ ...s.jsonSchema && { jsonSchema: s.jsonSchema }
1789
+ })).sort((a, b) => a.name.localeCompare(b.name));
1790
+ return { schemas };
1791
+ }
1792
+ function indentJson(value, indent) {
1793
+ return JSON.stringify(value, null, 2).split(`
1794
+ `).map((line, i) => i === 0 ? line : `${indent}${line}`).join(`
1795
+ `);
1796
+ }
1797
+ function renderSchemaRegistryFile(manifest, outputDir) {
1798
+ const lines = ["// Auto-generated by @vertz/compiler — do not edit", ""];
1799
+ const importsByFile = new Map;
1800
+ for (const schema of manifest.schemas) {
1801
+ const resolvedPath = resolveImportPath(outputDir, schema.importPath);
1802
+ const existing = importsByFile.get(resolvedPath) ?? [];
1803
+ existing.push(schema.variableName);
1804
+ importsByFile.set(resolvedPath, existing);
1805
+ }
1806
+ for (const [path, names] of importsByFile) {
1807
+ lines.push(`import { ${names.join(", ")} } from '${path}';`);
1808
+ }
1809
+ if (manifest.schemas.length > 0) {
1810
+ lines.push("");
1811
+ }
1812
+ lines.push("export const schemaRegistry = {");
1813
+ for (const schema of manifest.schemas) {
1814
+ lines.push(` ${schema.variableName},`);
1815
+ }
1816
+ lines.push("} as const;");
1817
+ lines.push("");
1818
+ lines.push("export const jsonSchemas = {");
1819
+ for (const schema of manifest.schemas) {
1820
+ if (schema.jsonSchema) {
1821
+ lines.push(` ${schema.name}: ${indentJson(schema.jsonSchema, " ")},`);
1822
+ }
1823
+ }
1824
+ lines.push("} as const;");
1825
+ lines.push("");
1826
+ return lines.join(`
1827
+ `);
1828
+ }
1829
+
1830
+ class SchemaRegistryGenerator extends BaseGenerator {
1831
+ name = "schema-registry";
1832
+ async generate(ir, outputDir) {
1833
+ const manifest = buildSchemaRegistry(ir);
1834
+ const content = renderSchemaRegistryFile(manifest, outputDir);
1835
+ const outputPath = this.resolveOutputPath(outputDir, "schemas.ts");
1836
+ await writeFile5(outputPath, content);
1837
+ }
1838
+ }
1839
+
1840
+ // src/ir/builder.ts
1841
+ function createEmptyDependencyGraph() {
1842
+ return {
1843
+ nodes: [],
1844
+ edges: [],
1845
+ initializationOrder: [],
1846
+ circularDependencies: []
1847
+ };
1848
+ }
1849
+ function createEmptyAppIR() {
1850
+ return {
1851
+ app: {
1852
+ basePath: "",
1853
+ globalMiddleware: [],
1854
+ moduleRegistrations: [],
1855
+ sourceFile: "",
1856
+ sourceLine: 0,
1857
+ sourceColumn: 0
1858
+ },
1859
+ modules: [],
1860
+ middleware: [],
1861
+ schemas: [],
1862
+ dependencyGraph: createEmptyDependencyGraph(),
1863
+ diagnostics: []
1864
+ };
1865
+ }
1866
+ function addDiagnosticsToIR(ir, diagnostics) {
1867
+ return {
1868
+ ...ir,
1869
+ diagnostics: [...ir.diagnostics, ...diagnostics]
1870
+ };
1871
+ }
1872
+
1873
+ // src/validators/completeness-validator.ts
1874
+ var METHODS_WITHOUT_RESPONSE = new Set(["DELETE", "HEAD", "OPTIONS"]);
1875
+ var RESERVED_CTX_KEYS = new Set([
1876
+ "params",
1877
+ "body",
1878
+ "query",
1879
+ "headers",
1880
+ "raw",
1881
+ "state",
1882
+ "options",
1883
+ "env"
1884
+ ]);
1885
+
1886
+ class CompletenessValidator {
1887
+ async validate(ir) {
1888
+ const diagnostics = [];
1889
+ this.checkResponseSchemas(ir, diagnostics);
1890
+ this.checkUnusedServices(ir, diagnostics);
1891
+ this.checkUnreferencedSchemas(ir, diagnostics);
1892
+ this.checkDIWiring(ir, diagnostics);
1893
+ this.checkMiddlewareChains(ir, diagnostics);
1894
+ this.checkCtxKeyCollisions(ir, diagnostics);
1895
+ this.checkDuplicateRoutes(ir, diagnostics);
1896
+ this.checkPathParamMatch(ir, diagnostics);
1897
+ this.checkModuleOptions(ir, diagnostics);
1898
+ this.checkRoutePathFormat(ir, diagnostics);
1899
+ return diagnostics;
1900
+ }
1901
+ checkResponseSchemas(ir, diagnostics) {
1902
+ for (const route of allRoutes(ir)) {
1903
+ if (!route.response && !METHODS_WITHOUT_RESPONSE.has(route.method)) {
1904
+ diagnostics.push(createDiagnosticFromLocation(route, {
1905
+ severity: "error",
1906
+ code: "VERTZ_ROUTE_MISSING_RESPONSE",
1907
+ message: `Route ${route.method} ${route.fullPath} has no response schema.`,
1908
+ suggestion: "Add a 'response' property to the route config."
1909
+ }));
1910
+ }
1911
+ }
1912
+ }
1913
+ checkUnusedServices(ir, diagnostics) {
1914
+ const referenced = collectAllInjectedTokens(ir);
1915
+ for (const mod of ir.modules) {
1916
+ for (const exp of mod.exports) {
1917
+ referenced.add(exp);
1918
+ }
1919
+ for (const svc of mod.services) {
1920
+ if (!referenced.has(svc.name)) {
1921
+ diagnostics.push(createDiagnosticFromLocation(svc, {
1922
+ severity: "warning",
1923
+ code: "VERTZ_DEAD_CODE",
1924
+ message: `Service '${svc.name}' in module '${mod.name}' is never injected or exported.`
1925
+ }));
1926
+ }
1927
+ }
1928
+ }
1929
+ }
1930
+ checkUnreferencedSchemas(ir, diagnostics) {
1931
+ const referenced = new Set;
1932
+ function addRef(ref) {
1933
+ if (ref?.kind === "named")
1934
+ referenced.add(ref.schemaName);
1935
+ }
1936
+ for (const mod of ir.modules) {
1937
+ addRef(mod.options);
1938
+ for (const route of allModuleRoutes(mod.routers)) {
1939
+ addRef(route.params);
1940
+ addRef(route.query);
1941
+ addRef(route.body);
1942
+ addRef(route.headers);
1943
+ addRef(route.response);
1944
+ }
1945
+ }
1946
+ for (const mw of ir.middleware) {
1947
+ addRef(mw.headers);
1948
+ addRef(mw.params);
1949
+ addRef(mw.query);
1950
+ addRef(mw.body);
1951
+ addRef(mw.requires);
1952
+ addRef(mw.provides);
1953
+ }
1954
+ for (const schema of ir.schemas) {
1955
+ if (!schema.isNamed)
1956
+ continue;
1957
+ if (referenced.has(schema.name))
1958
+ continue;
1959
+ diagnostics.push(createDiagnosticFromLocation(schema, {
1960
+ severity: "warning",
1961
+ code: "VERTZ_DEAD_CODE",
1962
+ message: `Schema '${schema.name}' is not referenced by any route or middleware.`
1963
+ }));
1964
+ }
1965
+ }
1966
+ checkDIWiring(ir, diagnostics) {
1967
+ const moduleExports = new Map;
1968
+ for (const mod of ir.modules) {
1969
+ moduleExports.set(mod.name, new Set(mod.exports));
1970
+ }
1971
+ for (const mod of ir.modules) {
1972
+ const available = new Set(mod.services.map((s) => s.name));
1973
+ for (const imp of mod.imports) {
1974
+ if (imp.isEnvImport || !imp.sourceModule)
1975
+ continue;
1976
+ const exports = moduleExports.get(imp.sourceModule);
1977
+ if (exports) {
1978
+ for (const exp of exports) {
1979
+ available.add(exp);
1980
+ }
1981
+ }
1982
+ }
1983
+ for (const svc of mod.services) {
1984
+ this.checkInjectTokens(svc.name, "Service", svc.inject, available, svc, diagnostics);
1985
+ }
1986
+ for (const router of mod.routers) {
1987
+ this.checkInjectTokens(router.name, "Router", router.inject, available, router, diagnostics);
1988
+ }
1989
+ }
1990
+ }
1991
+ checkInjectTokens(ownerName, ownerKind, inject, available, location, diagnostics) {
1992
+ for (const inj of inject) {
1993
+ if (available.has(inj.resolvedToken))
1994
+ continue;
1995
+ diagnostics.push(createDiagnosticFromLocation(location, {
1996
+ severity: "error",
1997
+ code: "VERTZ_SERVICE_INJECT_MISSING",
1998
+ message: `${ownerKind} '${ownerName}' injects '${inj.resolvedToken}' which cannot be resolved.`
1999
+ }));
2000
+ }
2001
+ }
2002
+ checkMiddlewareChains(ir, diagnostics) {
2003
+ const mwMap = new Map;
2004
+ for (const mw of ir.middleware) {
2005
+ mwMap.set(mw.name, mw);
2006
+ }
2007
+ const providedKeys = new Set;
2008
+ for (const mwRef of ir.app.globalMiddleware) {
2009
+ const mw = mwMap.get(mwRef.name);
2010
+ if (!mw)
2011
+ continue;
2012
+ for (const key of extractSchemaPropertyKeys(mw.requires)) {
2013
+ if (!providedKeys.has(key)) {
2014
+ diagnostics.push(createDiagnosticFromLocation(mw, {
2015
+ severity: "error",
2016
+ code: "VERTZ_MW_REQUIRES_UNSATISFIED",
2017
+ message: `Middleware '${mw.name}' requires '${key}' but no preceding middleware provides it.`
2018
+ }));
2019
+ }
2020
+ }
2021
+ for (const key of extractSchemaPropertyKeys(mw.provides)) {
2022
+ providedKeys.add(key);
2023
+ }
2024
+ }
2025
+ }
2026
+ checkModuleOptions(ir, diagnostics) {
2027
+ const moduleMap = new Map(ir.modules.map((m) => [m.name, m]));
2028
+ for (const reg of ir.app.moduleRegistrations) {
2029
+ const mod = moduleMap.get(reg.moduleName);
2030
+ if (!mod)
2031
+ continue;
2032
+ if (reg.options && !mod.options) {
2033
+ diagnostics.push(createDiagnostic({
2034
+ severity: "warning",
2035
+ code: "VERTZ_MODULE_OPTIONS_INVALID",
2036
+ message: `Module '${reg.moduleName}' received options but does not define an options schema.`
2037
+ }));
2038
+ }
2039
+ if (!reg.options && mod.options) {
2040
+ diagnostics.push(createDiagnostic({
2041
+ severity: "error",
2042
+ code: "VERTZ_MODULE_OPTIONS_INVALID",
2043
+ message: `Module '${reg.moduleName}' requires options but none were provided in .register().`
2044
+ }));
2045
+ }
2046
+ }
2047
+ }
2048
+ checkRoutePathFormat(ir, diagnostics) {
2049
+ for (const route of allRoutes(ir)) {
2050
+ if (!route.path.startsWith("/")) {
2051
+ diagnostics.push(createDiagnosticFromLocation(route, {
2052
+ severity: "error",
2053
+ code: "VERTZ_RT_INVALID_PATH",
2054
+ message: `Route path '${route.path}' must start with '/'.`,
2055
+ suggestion: `Change the path to '/${route.path}'.`
2056
+ }));
2057
+ }
2058
+ }
2059
+ }
2060
+ checkPathParamMatch(ir, diagnostics) {
2061
+ for (const route of allRoutes(ir)) {
2062
+ if (!route.params)
2063
+ continue;
2064
+ const pathParams = new Set(extractPathParams(route.fullPath));
2065
+ const schemaParams = new Set(extractSchemaPropertyKeys(route.params));
2066
+ for (const param of pathParams) {
2067
+ if (!schemaParams.has(param)) {
2068
+ diagnostics.push(createDiagnosticFromLocation(route, {
2069
+ severity: "error",
2070
+ code: "VERTZ_ROUTE_PARAM_MISMATCH",
2071
+ message: `Route ${route.method} ${route.fullPath} has path parameter ':${param}' not defined in params schema.`
2072
+ }));
2073
+ }
2074
+ }
2075
+ for (const param of schemaParams) {
2076
+ if (!pathParams.has(param)) {
2077
+ diagnostics.push(createDiagnosticFromLocation(route, {
2078
+ severity: "warning",
2079
+ code: "VERTZ_ROUTE_PARAM_MISMATCH",
2080
+ message: `Route ${route.method} ${route.fullPath} params schema defines '${param}' which is not a path parameter.`
2081
+ }));
2082
+ }
2083
+ }
2084
+ }
2085
+ }
2086
+ checkDuplicateRoutes(ir, diagnostics) {
2087
+ const seen = new Map;
2088
+ for (const mod of ir.modules) {
2089
+ for (const router of mod.routers) {
2090
+ for (const route of router.routes) {
2091
+ const key = `${route.method} ${route.fullPath}`;
2092
+ const existing = seen.get(key);
2093
+ if (existing) {
2094
+ diagnostics.push(createDiagnosticFromLocation(route, {
2095
+ severity: "error",
2096
+ code: "VERTZ_ROUTE_DUPLICATE",
2097
+ message: `Duplicate route: ${key} defined in ${existing} and ${router.name}.`
2098
+ }));
2099
+ } else {
2100
+ seen.set(key, router.name);
2101
+ }
2102
+ }
2103
+ }
2104
+ }
2105
+ }
2106
+ checkCtxKeyCollisions(ir, diagnostics) {
2107
+ const keyProviders = new Map;
2108
+ for (const mw of ir.middleware) {
2109
+ const providedKeys = extractSchemaPropertyKeys(mw.provides);
2110
+ for (const key of providedKeys) {
2111
+ if (RESERVED_CTX_KEYS.has(key)) {
2112
+ diagnostics.push(createDiagnosticFromLocation(mw, {
2113
+ severity: "error",
2114
+ code: "VERTZ_CTX_COLLISION",
2115
+ message: `Context key '${key}' provided by middleware '${mw.name}' is a reserved ctx property.`
2116
+ }));
2117
+ continue;
2118
+ }
2119
+ const existing = keyProviders.get(key);
2120
+ if (existing) {
2121
+ diagnostics.push(createDiagnosticFromLocation(mw, {
2122
+ severity: "error",
2123
+ code: "VERTZ_CTX_COLLISION",
2124
+ message: `Context key '${key}' is provided by both '${existing}' and '${mw.name}'.`
2125
+ }));
2126
+ } else {
2127
+ keyProviders.set(key, mw.name);
2128
+ }
2129
+ }
2130
+ }
2131
+ const injectedNames = new Set;
2132
+ for (const mod of ir.modules) {
2133
+ for (const router of mod.routers) {
2134
+ for (const inj of router.inject) {
2135
+ injectedNames.add(inj.resolvedToken);
2136
+ }
2137
+ }
2138
+ }
2139
+ for (const mw of ir.middleware) {
2140
+ for (const key of extractSchemaPropertyKeys(mw.provides)) {
2141
+ if (injectedNames.has(key)) {
2142
+ diagnostics.push(createDiagnosticFromLocation(mw, {
2143
+ severity: "error",
2144
+ code: "VERTZ_CTX_COLLISION",
2145
+ message: `Context key '${key}' provided by middleware '${mw.name}' collides with injected service name.`
2146
+ }));
2147
+ }
2148
+ }
2149
+ }
2150
+ }
2151
+ }
2152
+ function extractSchemaPropertyKeys(ref) {
2153
+ if (!ref)
2154
+ return [];
2155
+ const props = ref.jsonSchema?.properties;
2156
+ if (!props || typeof props !== "object")
2157
+ return [];
2158
+ return Object.keys(props);
2159
+ }
2160
+ function extractPathParams(path) {
2161
+ return path.split("/").filter((s) => s.startsWith(":")).map((s) => s.slice(1));
2162
+ }
2163
+ function* allModuleRoutes(routers) {
2164
+ for (const router of routers) {
2165
+ yield* router.routes;
2166
+ }
2167
+ }
2168
+ function* allRoutes(ir) {
2169
+ for (const mod of ir.modules) {
2170
+ yield* allModuleRoutes(mod.routers);
2171
+ }
2172
+ }
2173
+ function collectAllInjectedTokens(ir) {
2174
+ const tokens = new Set;
2175
+ for (const mod of ir.modules) {
2176
+ for (const router of mod.routers) {
2177
+ for (const inj of router.inject) {
2178
+ tokens.add(inj.resolvedToken);
2179
+ }
2180
+ }
2181
+ for (const svc of mod.services) {
2182
+ for (const inj of svc.inject) {
2183
+ tokens.add(inj.resolvedToken);
2184
+ }
2185
+ }
2186
+ }
2187
+ for (const mw of ir.middleware) {
2188
+ for (const inj of mw.inject) {
2189
+ tokens.add(inj.resolvedToken);
2190
+ }
2191
+ }
2192
+ return tokens;
2193
+ }
2194
+
2195
+ // src/validators/module-validator.ts
2196
+ class ModuleValidator {
2197
+ async validate(ir) {
2198
+ const diagnostics = [];
2199
+ for (const mod of ir.modules) {
2200
+ this.checkExports(mod, diagnostics);
2201
+ this.checkOwnership(mod, diagnostics);
2202
+ }
2203
+ this.checkCircularDependencies(ir, diagnostics);
2204
+ return diagnostics;
2205
+ }
2206
+ checkExports(mod, diagnostics) {
2207
+ const serviceNames = new Set(mod.services.map((s) => s.name));
2208
+ for (const exp of mod.exports) {
2209
+ if (!serviceNames.has(exp)) {
2210
+ diagnostics.push(createDiagnostic({
2211
+ severity: "error",
2212
+ code: "VERTZ_MODULE_EXPORT_INVALID",
2213
+ message: `Module '${mod.name}' exports '${exp}' which is not one of its services.`,
2214
+ suggestion: `Either add '${exp}' as a service or remove it from exports.`
2215
+ }));
2216
+ }
2217
+ }
2218
+ }
2219
+ checkOwnership(mod, diagnostics) {
2220
+ for (const svc of mod.services) {
2221
+ if (svc.moduleName !== mod.name) {
2222
+ diagnostics.push(createDiagnostic({
2223
+ severity: "error",
2224
+ code: "VERTZ_MODULE_WRONG_OWNERSHIP",
2225
+ message: `Service '${svc.name}' declares module '${svc.moduleName}' but is listed under module '${mod.name}'.`
2226
+ }));
2227
+ }
2228
+ }
2229
+ }
2230
+ checkCircularDependencies(ir, diagnostics) {
2231
+ for (const cycle of ir.dependencyGraph.circularDependencies) {
2232
+ const path = [...cycle, cycle.at(0)].join(" -> ");
2233
+ diagnostics.push(createDiagnostic({
2234
+ severity: "error",
2235
+ code: "VERTZ_MODULE_CIRCULAR",
2236
+ message: `Circular dependency detected: ${path}.`,
2237
+ suggestion: "Break the cycle by extracting shared code into a separate module that both can import."
2238
+ }));
2239
+ }
2240
+ }
2241
+ }
2242
+
2243
+ // src/validators/naming-validator.ts
2244
+ var VALID_OPERATIONS2 = ["create", "read", "update", "list", "delete"];
2245
+ var VALID_PARTS2 = ["Body", "Response", "Query", "Params", "Headers"];
2246
+ var NULL_PARSED = { operation: null, entity: null, part: null };
2247
+ function isUpperCase(char) {
2248
+ return char >= "A" && char <= "Z";
2249
+ }
2250
+ function isFullyParsed(parsed) {
2251
+ return parsed.operation !== null && parsed.entity !== null && parsed.part !== null;
2252
+ }
2253
+
2254
+ class NamingValidator {
2255
+ async validate(ir) {
2256
+ const diagnostics = [];
2257
+ for (const schema of ir.schemas) {
2258
+ if (!schema.isNamed)
2259
+ continue;
2260
+ if (isFullyParsed(this.parseSchemaName(schema.name)))
2261
+ continue;
2262
+ diagnostics.push(createDiagnosticFromLocation(schema, {
2263
+ severity: "warning",
2264
+ code: "VERTZ_SCHEMA_NAMING",
2265
+ message: `Schema '${schema.name}' does not follow the {operation}{Entity}{Part} naming convention.`,
2266
+ suggestion: this.suggestFix(schema.name)
2267
+ }));
2268
+ }
2269
+ return diagnostics;
2270
+ }
2271
+ parseSchemaName(name) {
2272
+ if (!name)
2273
+ return NULL_PARSED;
2274
+ for (const op of VALID_OPERATIONS2) {
2275
+ if (!name.startsWith(op))
2276
+ continue;
2277
+ const rest = name.slice(op.length);
2278
+ if (!rest || !isUpperCase(rest[0]))
2279
+ continue;
2280
+ for (const part of VALID_PARTS2) {
2281
+ if (!rest.endsWith(part))
2282
+ continue;
2283
+ const entity = rest.slice(0, -part.length);
2284
+ if (!entity)
2285
+ continue;
2286
+ return { operation: op, entity, part };
2287
+ }
2288
+ return { operation: op, entity: null, part: null };
2289
+ }
2290
+ return NULL_PARSED;
2291
+ }
2292
+ suggestFix(name) {
2293
+ const lowerName = name[0].toLowerCase() + name.slice(1);
2294
+ if (isFullyParsed(this.parseSchemaName(lowerName))) {
2295
+ return `Use lowercase operation: '${lowerName}'`;
2296
+ }
2297
+ for (const op of VALID_OPERATIONS2) {
2298
+ if (!name.startsWith(op))
2299
+ continue;
2300
+ const rest = name.slice(op.length);
2301
+ if (!rest)
2302
+ continue;
2303
+ const fixed = op + rest[0].toUpperCase() + rest.slice(1);
2304
+ if (isFullyParsed(this.parseSchemaName(fixed))) {
2305
+ return `Use PascalCase entity: '${fixed}'`;
2306
+ }
2307
+ }
2308
+ return;
2309
+ }
2310
+ }
2311
+
2312
+ // src/validators/placement-validator.ts
2313
+ class PlacementValidator {
2314
+ async validate(ir) {
2315
+ const diagnostics = [];
2316
+ for (const schema of ir.schemas) {
2317
+ this.checkFileLocation(schema, diagnostics);
2318
+ }
2319
+ this.checkMixedExports(ir.schemas, diagnostics);
2320
+ return diagnostics;
2321
+ }
2322
+ checkFileLocation(schema, diagnostics) {
2323
+ const file = schema.sourceFile;
2324
+ const inSchemasDir = file.includes("/schemas/") || file.includes("\\schemas\\");
2325
+ if (!inSchemasDir) {
2326
+ diagnostics.push(createDiagnosticFromLocation(schema, {
2327
+ severity: "warning",
2328
+ code: "VERTZ_SCHEMA_PLACEMENT",
2329
+ message: `Schema '${schema.name}' is not in a schemas/ directory.`,
2330
+ suggestion: `Move schema file to a 'schemas/' directory.`
2331
+ }));
2332
+ return;
2333
+ }
2334
+ if (!file.endsWith(".schema.ts")) {
2335
+ diagnostics.push(createDiagnosticFromLocation(schema, {
2336
+ severity: "warning",
2337
+ code: "VERTZ_SCHEMA_PLACEMENT",
2338
+ message: `Schema file '${file}' does not use the .schema.ts suffix.`,
2339
+ suggestion: `Rename file to use '.schema.ts' suffix.`
2340
+ }));
2341
+ }
2342
+ }
2343
+ checkMixedExports(schemas, diagnostics) {
2344
+ const byFile = groupBy(schemas, (s) => s.sourceFile);
2345
+ for (const [file, fileSchemas] of byFile) {
2346
+ const withConvention = fileSchemas.filter((s) => s.namingConvention.operation && s.namingConvention.entity);
2347
+ if (withConvention.length < 2)
2348
+ continue;
2349
+ const operations = new Set(withConvention.map((s) => s.namingConvention.operation));
2350
+ if (operations.size > 1) {
2351
+ diagnostics.push(createDiagnostic({
2352
+ severity: "warning",
2353
+ code: "VERTZ_SCHEMA_PLACEMENT",
2354
+ message: `Schema file '${file}' exports schemas with mixed operations: ${[...operations].join(", ")}.`,
2355
+ file
2356
+ }));
2357
+ }
2358
+ const entities = new Set(withConvention.map((s) => s.namingConvention.entity));
2359
+ if (entities.size > 1) {
2360
+ diagnostics.push(createDiagnostic({
2361
+ severity: "warning",
2362
+ code: "VERTZ_SCHEMA_PLACEMENT",
2363
+ message: `Schema file '${file}' exports schemas with mixed entities: ${[...entities].join(", ")}.`,
2364
+ file
2365
+ }));
2366
+ }
2367
+ }
2368
+ }
2369
+ }
2370
+ function groupBy(items, keyFn) {
2371
+ const map = new Map;
2372
+ for (const item of items) {
2373
+ const key = keyFn(item);
2374
+ const group = map.get(key);
2375
+ if (group) {
2376
+ group.push(item);
2377
+ } else {
2378
+ map.set(key, [item]);
2379
+ }
2380
+ }
2381
+ return map;
2382
+ }
2383
+
2384
+ // src/compiler.ts
2385
+ class Compiler {
2386
+ config;
2387
+ deps;
2388
+ constructor(config, dependencies) {
2389
+ this.config = config;
2390
+ this.deps = dependencies;
2391
+ }
2392
+ getConfig() {
2393
+ return this.config;
2394
+ }
2395
+ async analyze() {
2396
+ const ir = createEmptyAppIR();
2397
+ const { analyzers } = this.deps;
2398
+ const envResult = await analyzers.env.analyze();
2399
+ const schemaResult = await analyzers.schema.analyze();
2400
+ const moduleResult = await analyzers.module.analyze();
2401
+ const middlewareResult = await analyzers.middleware.analyze();
2402
+ const appResult = await analyzers.app.analyze();
2403
+ const depGraphResult = await analyzers.dependencyGraph.analyze();
2404
+ ir.env = envResult.env;
2405
+ ir.schemas = schemaResult.schemas;
2406
+ ir.modules = moduleResult.modules;
2407
+ ir.middleware = middlewareResult.middleware;
2408
+ ir.app = appResult.app;
2409
+ ir.dependencyGraph = depGraphResult.graph;
2410
+ return ir;
2411
+ }
2412
+ async validate(ir) {
2413
+ const allDiagnostics = [];
2414
+ for (const validator of this.deps.validators) {
2415
+ const diagnostics = await validator.validate(ir);
2416
+ allDiagnostics.push(...diagnostics);
2417
+ }
2418
+ return allDiagnostics;
2419
+ }
2420
+ async generate(ir) {
2421
+ const outputDir = this.config.compiler.outputDir;
2422
+ await Promise.all(this.deps.generators.map((g) => g.generate(ir, outputDir)));
2423
+ }
2424
+ async compile() {
2425
+ const ir = await this.analyze();
2426
+ const diagnostics = await this.validate(ir);
2427
+ const hasErrorDiags = hasErrors(diagnostics);
2428
+ if (!hasErrorDiags || this.config.forceGenerate) {
2429
+ await this.generate(ir);
2430
+ }
2431
+ return {
2432
+ success: !hasErrorDiags,
2433
+ ir: { ...ir, diagnostics },
2434
+ diagnostics
2435
+ };
2436
+ }
2437
+ }
2438
+ function createCompiler(config) {
2439
+ const resolved = resolveConfig(config);
2440
+ const project = new Project({ tsConfigFilePath: "tsconfig.json" });
2441
+ const deps = {
2442
+ analyzers: {
2443
+ env: new EnvAnalyzer(project, resolved),
2444
+ schema: new SchemaAnalyzer(project, resolved),
2445
+ middleware: new MiddlewareAnalyzer(project, resolved),
2446
+ module: new ModuleAnalyzer(project, resolved),
2447
+ app: new AppAnalyzer(project, resolved),
2448
+ dependencyGraph: new DependencyGraphAnalyzer(project, resolved)
2449
+ },
2450
+ validators: [
2451
+ new CompletenessValidator,
2452
+ new ModuleValidator,
2453
+ new NamingValidator,
2454
+ new PlacementValidator
2455
+ ],
2456
+ generators: [
2457
+ new BootGenerator(resolved),
2458
+ new RouteTableGenerator(resolved),
2459
+ new SchemaRegistryGenerator(resolved),
2460
+ new ManifestGenerator(resolved),
2461
+ new OpenAPIGenerator(resolved)
2462
+ ]
2463
+ };
2464
+ return new Compiler(resolved, deps);
2465
+ }
2466
+ // src/incremental.ts
2467
+ import { basename, dirname } from "node:path";
2468
+
2469
+ // src/ir/merge.ts
2470
+ function mergeByName(base, partial) {
2471
+ if (!partial)
2472
+ return base;
2473
+ const partialNames = new Set(partial.map((item) => item.name));
2474
+ const preserved = base.filter((item) => !partialNames.has(item.name));
2475
+ return [...preserved, ...partial];
2476
+ }
2477
+ function mergeIR(base, partial) {
2478
+ return {
2479
+ ...base,
2480
+ modules: mergeByName(base.modules, partial.modules),
2481
+ schemas: mergeByName(base.schemas, partial.schemas),
2482
+ middleware: mergeByName(base.middleware, partial.middleware),
2483
+ dependencyGraph: partial.dependencyGraph ?? base.dependencyGraph,
2484
+ diagnostics: []
2485
+ };
2486
+ }
2487
+
2488
+ // src/incremental.ts
2489
+ function categorize(path, options) {
2490
+ const file = basename(path);
2491
+ if (file.endsWith(".schema.ts"))
2492
+ return "schema";
2493
+ if (file.endsWith(".router.ts"))
2494
+ return "router";
2495
+ if (file.endsWith(".service.ts"))
2496
+ return "service";
2497
+ if (file.endsWith(".module.ts"))
2498
+ return "module";
2499
+ if (path.includes("middleware/"))
2500
+ return "middleware";
2501
+ if (options.entryFile && path === options.entryFile)
2502
+ return "app-entry";
2503
+ if (file.startsWith(".env"))
2504
+ return "env";
2505
+ if (file === "vertz.config.ts")
2506
+ return "config";
2507
+ return null;
2508
+ }
2509
+ function categorizeChanges(changes, options = {}) {
2510
+ const result = {
2511
+ schema: [],
2512
+ router: [],
2513
+ service: [],
2514
+ module: [],
2515
+ middleware: [],
2516
+ requiresFullRecompile: false,
2517
+ requiresReboot: false
2518
+ };
2519
+ for (const change of changes) {
2520
+ const category = categorize(change.path, options);
2521
+ switch (category) {
2522
+ case "schema":
2523
+ result.schema.push(change);
2524
+ break;
2525
+ case "router":
2526
+ result.router.push(change);
2527
+ break;
2528
+ case "service":
2529
+ result.service.push(change);
2530
+ break;
2531
+ case "module":
2532
+ result.module.push(change);
2533
+ break;
2534
+ case "middleware":
2535
+ result.middleware.push(change);
2536
+ break;
2537
+ case "app-entry":
2538
+ result.requiresFullRecompile = true;
2539
+ break;
2540
+ case "env":
2541
+ result.requiresReboot = true;
2542
+ result.rebootReason = "env";
2543
+ break;
2544
+ case "config":
2545
+ result.requiresReboot = true;
2546
+ result.rebootReason = "config";
2547
+ break;
2548
+ }
2549
+ }
2550
+ return result;
2551
+ }
2552
+ function findAffectedModules(categorized, ir) {
2553
+ const affected = new Set;
2554
+ for (const change of categorized.module) {
2555
+ const mod = ir.modules.find((m) => m.sourceFile === change.path);
2556
+ if (mod)
2557
+ affected.add(mod.name);
2558
+ }
2559
+ for (const change of categorized.service) {
2560
+ for (const mod of ir.modules) {
2561
+ if (mod.services.some((s) => s.sourceFile === change.path)) {
2562
+ affected.add(mod.name);
2563
+ }
2564
+ }
2565
+ }
2566
+ for (const change of categorized.router) {
2567
+ for (const mod of ir.modules) {
2568
+ if (mod.routers.some((r) => r.sourceFile === change.path)) {
2569
+ affected.add(mod.name);
2570
+ }
2571
+ }
2572
+ }
2573
+ for (const change of categorized.schema) {
2574
+ for (const mod of ir.modules) {
2575
+ const moduleDir = dirname(mod.sourceFile);
2576
+ if (change.path.startsWith(moduleDir)) {
2577
+ affected.add(mod.name);
2578
+ }
2579
+ }
2580
+ }
2581
+ return [...affected];
2582
+ }
2583
+
2584
+ class IncrementalCompiler {
2585
+ currentIR;
2586
+ compiler;
2587
+ constructor(compiler) {
2588
+ this.compiler = compiler;
2589
+ this.currentIR = createEmptyAppIR();
2590
+ }
2591
+ async initialCompile() {
2592
+ const result = await this.compiler.compile();
2593
+ this.currentIR = result.ir;
2594
+ return result;
2595
+ }
2596
+ async handleChanges(changes) {
2597
+ const categorized = categorizeChanges(changes, {
2598
+ entryFile: this.compiler.getConfig().compiler.entryFile
2599
+ });
2600
+ if (categorized.requiresReboot) {
2601
+ return { kind: "reboot", reason: categorized.rebootReason ?? "unknown" };
2602
+ }
2603
+ if (categorized.requiresFullRecompile) {
2604
+ const result = await this.compiler.compile();
2605
+ this.currentIR = result.ir;
2606
+ return { kind: "full-recompile" };
2607
+ }
2608
+ const partialIR = await this.compiler.analyze();
2609
+ this.currentIR = mergeIR(this.currentIR, partialIR);
2610
+ const diagnostics = await this.compiler.validate(this.currentIR);
2611
+ if (!hasErrors(diagnostics)) {
2612
+ await this.compiler.generate(this.currentIR);
2613
+ }
2614
+ const affectedModules = findAffectedModules(categorized, this.currentIR);
2615
+ return {
2616
+ kind: "incremental",
2617
+ affectedModules,
2618
+ diagnostics
2619
+ };
2620
+ }
2621
+ getCurrentIR() {
2622
+ return this.currentIR;
2623
+ }
2624
+ }
2625
+ // src/typecheck.ts
2626
+ import { execFile, spawn } from "node:child_process";
2627
+ function parseTscOutput(output) {
2628
+ const diagnostics = [];
2629
+ const pattern = /^(.+)\((\d+),(\d+)\): error TS(\d+): (.+)$/gm;
2630
+ let match = pattern.exec(output);
2631
+ while (match !== null) {
2632
+ diagnostics.push({
2633
+ file: match[1],
2634
+ line: Number.parseInt(match[2], 10),
2635
+ column: Number.parseInt(match[3], 10),
2636
+ code: Number.parseInt(match[4], 10),
2637
+ message: match[5]
2638
+ });
2639
+ match = pattern.exec(output);
2640
+ }
2641
+ return diagnostics;
2642
+ }
2643
+ function parseWatchBlock(block) {
2644
+ const diagnostics = parseTscOutput(block);
2645
+ const foundMatch = /Found (\d+) error/.exec(block);
2646
+ const errorCount = foundMatch ? Number.parseInt(foundMatch[1], 10) : diagnostics.length;
2647
+ return {
2648
+ success: errorCount === 0,
2649
+ diagnostics
2650
+ };
2651
+ }
2652
+ async function* typecheckWatch(options = {}) {
2653
+ const proc = options.spawner?.() ?? spawn("tsc", ["--noEmit", "--watch", ...options.tsconfigPath ? ["--project", options.tsconfigPath] : []], {
2654
+ cwd: process.cwd()
2655
+ });
2656
+ const completionMarker = /Found \d+ error/;
2657
+ let buffer = "";
2658
+ const results = [];
2659
+ let resolve = null;
2660
+ let done = false;
2661
+ const onData = (chunk) => {
2662
+ buffer += chunk.toString();
2663
+ if (completionMarker.test(buffer)) {
2664
+ const result = parseWatchBlock(buffer);
2665
+ buffer = "";
2666
+ results.push(result);
2667
+ resolve?.();
2668
+ }
2669
+ };
2670
+ proc.stdout?.on("data", onData);
2671
+ proc.stderr?.on("data", onData);
2672
+ proc.on("close", () => {
2673
+ done = true;
2674
+ resolve?.();
2675
+ });
2676
+ try {
2677
+ while (!done || results.length > 0) {
2678
+ const next = results.shift();
2679
+ if (next) {
2680
+ yield next;
2681
+ } else if (!done) {
2682
+ await new Promise((r) => {
2683
+ resolve = r;
2684
+ });
2685
+ }
2686
+ }
2687
+ } finally {
2688
+ proc.kill();
2689
+ }
2690
+ }
2691
+ async function typecheck(options = {}) {
2692
+ const args = ["--noEmit"];
2693
+ if (options.tsconfigPath) {
2694
+ args.push("--project", options.tsconfigPath);
2695
+ }
2696
+ return new Promise((resolve) => {
2697
+ execFile("tsc", args, { cwd: process.cwd() }, (error, stdout, stderr) => {
2698
+ const output = stdout + stderr;
2699
+ const diagnostics = parseTscOutput(output);
2700
+ resolve({
2701
+ success: !error,
2702
+ diagnostics
2703
+ });
2704
+ });
2705
+ });
2706
+ }
2707
+ // src/utils/schema-executor.ts
2708
+ function createSchemaExecutor(_rootDir) {
2709
+ const diagnostics = [];
2710
+ function addError(message, file) {
2711
+ diagnostics.push(createDiagnostic({
2712
+ severity: "error",
2713
+ code: "VERTZ_SCHEMA_EXECUTION",
2714
+ message,
2715
+ file
2716
+ }));
2717
+ return null;
2718
+ }
2719
+ return {
2720
+ async execute(schemaName, sourceFile) {
2721
+ try {
2722
+ const mod = await import(sourceFile);
2723
+ const schema = mod[schemaName];
2724
+ if (!schema) {
2725
+ return addError(`Export '${schemaName}' not found in '${sourceFile}'`, sourceFile);
2726
+ }
2727
+ if (typeof schema.toJSONSchema !== "function") {
2728
+ return addError(`Export '${schemaName}' in '${sourceFile}' does not have a toJSONSchema() method`, sourceFile);
2729
+ }
2730
+ return { jsonSchema: schema.toJSONSchema() };
2731
+ } catch (err) {
2732
+ const detail = err instanceof Error ? err.message : String(err);
2733
+ return addError(`Failed to execute schema '${schemaName}' from '${sourceFile}': ${detail}`, sourceFile);
2734
+ }
2735
+ },
2736
+ getDiagnostics() {
2737
+ return [...diagnostics];
2738
+ }
2739
+ };
2740
+ }
2741
+ export {
2742
+ typecheckWatch,
2743
+ typecheck,
2744
+ resolveImportPath,
2745
+ resolveIdentifier,
2746
+ resolveExport,
2747
+ resolveConfig,
2748
+ renderSchemaRegistryFile,
2749
+ renderRouteTableFile,
2750
+ renderBootFile,
2751
+ parseWatchBlock,
2752
+ parseTscOutput,
2753
+ parseSchemaName,
2754
+ parseInjectRefs,
2755
+ parseImports,
2756
+ mergeIR,
2757
+ mergeDiagnostics,
2758
+ isSchemaFile,
2759
+ isSchemaExpression,
2760
+ isFromImport,
2761
+ hasErrors,
2762
+ getVariableNameForCall,
2763
+ getStringValue,
2764
+ getSourceLocation,
2765
+ getPropertyValue,
2766
+ getProperties,
2767
+ getNumberValue,
2768
+ getBooleanValue,
2769
+ getArrayElements,
2770
+ findMethodCallsOnVariable,
2771
+ findCallExpressions,
2772
+ findAffectedModules,
2773
+ filterBySeverity,
2774
+ extractSchemaId,
2775
+ extractObjectLiteral,
2776
+ extractMethodSignatures,
2777
+ extractIdentifierNames,
2778
+ defineConfig,
2779
+ createSchemaExecutor,
2780
+ createNamedSchemaRef,
2781
+ createInlineSchemaRef,
2782
+ createEmptyDependencyGraph,
2783
+ createEmptyAppIR,
2784
+ createDiagnosticFromLocation,
2785
+ createDiagnostic,
2786
+ createCompiler,
2787
+ categorizeChanges,
2788
+ buildSchemaRegistry,
2789
+ buildRouteTable,
2790
+ buildManifest,
2791
+ buildBootManifest,
2792
+ addDiagnosticsToIR,
2793
+ ServiceAnalyzer,
2794
+ SchemaRegistryGenerator,
2795
+ SchemaAnalyzer,
2796
+ RouteTableGenerator,
2797
+ RouteAnalyzer,
2798
+ PlacementValidator,
2799
+ OpenAPIGenerator,
2800
+ NamingValidator,
2801
+ ModuleValidator,
2802
+ ModuleAnalyzer,
2803
+ MiddlewareAnalyzer,
2804
+ ManifestGenerator,
2805
+ IncrementalCompiler,
2806
+ EnvAnalyzer,
2807
+ DependencyGraphAnalyzer,
2808
+ CompletenessValidator,
2809
+ Compiler,
2810
+ BootGenerator,
2811
+ BaseGenerator,
2812
+ BaseAnalyzer,
2813
+ AppAnalyzer
2814
+ };