@vertz/compiler 0.1.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # @vertz/compiler
2
+
3
+ > **Internal package** — You don't use this directly. It powers `vertz build`, `vertz dev`, and `vertz codegen` behind the scenes.
4
+
5
+ Static analysis and code generation for Vertz applications. Analyzes TypeScript source code to extract application structure (routes, schemas, modules, middleware), validates conventions, and generates runtime artifacts like boot files, route tables, and OpenAPI specs.
6
+
7
+ ## Who uses this
8
+
9
+ - **`@vertz/cli`** — All CLI commands (`vertz build`, `vertz dev`, `vertz check`) invoke the compiler.
10
+ - **`@vertz/codegen`** — Consumes the compiler's intermediate representation (IR) to generate SDKs and CLIs.
11
+ - **Framework contributors** — See [INTERNALS.md](./INTERNALS.md) for architecture, pipeline stages, and extension points.
12
+
13
+ ## Related Packages
14
+
15
+ - [`@vertz/cli`](../cli) — The CLI that invokes the compiler
16
+ - [`@vertz/codegen`](../codegen) — Code generation from compiler IR
17
+
18
+ ## License
19
+
20
+ MIT
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  type DiagnosticSeverity = "error" | "warning" | "info";
2
- type DiagnosticCode = "VERTZ_SCHEMA_NAMING" | "VERTZ_SCHEMA_PLACEMENT" | "VERTZ_SCHEMA_EXECUTION" | "VERTZ_SCHEMA_MISSING_ID" | "VERTZ_SCHEMA_DYNAMIC_NAME" | "VERTZ_MODULE_CIRCULAR" | "VERTZ_MODULE_EXPORT_INVALID" | "VERTZ_MODULE_IMPORT_MISSING" | "VERTZ_MODULE_DUPLICATE_NAME" | "VERTZ_MODULE_DYNAMIC_NAME" | "VERTZ_MODULE_OPTIONS_INVALID" | "VERTZ_MODULE_WRONG_OWNERSHIP" | "VERTZ_SERVICE_INJECT_MISSING" | "VERTZ_SERVICE_UNUSED" | "VERTZ_SERVICE_DYNAMIC_NAME" | "VERTZ_ENV_MISSING_DEFAULT" | "VERTZ_ENV_DUPLICATE" | "VERTZ_ENV_DYNAMIC_CONFIG" | "VERTZ_MW_MISSING_NAME" | "VERTZ_MW_MISSING_HANDLER" | "VERTZ_MW_DYNAMIC_NAME" | "VERTZ_MW_NON_OBJECT_CONFIG" | "VERTZ_MW_REQUIRES_UNSATISFIED" | "VERTZ_MW_PROVIDES_COLLISION" | "VERTZ_MW_ORDER_INVALID" | "VERTZ_RT_UNKNOWN_MODULE_DEF" | "VERTZ_RT_DYNAMIC_PATH" | "VERTZ_RT_MISSING_HANDLER" | "VERTZ_RT_MISSING_PREFIX" | "VERTZ_RT_DYNAMIC_CONFIG" | "VERTZ_RT_INVALID_PATH" | "VERTZ_ROUTE_DUPLICATE" | "VERTZ_ROUTE_PARAM_MISMATCH" | "VERTZ_ROUTE_MISSING_RESPONSE" | "VERTZ_APP_MISSING" | "VERTZ_APP_NOT_FOUND" | "VERTZ_APP_DUPLICATE" | "VERTZ_APP_BASEPATH_FORMAT" | "VERTZ_APP_INLINE_MODULE" | "VERTZ_DEP_CYCLE" | "VERTZ_DEP_CIRCULAR" | "VERTZ_DEP_UNRESOLVED_INJECT" | "VERTZ_DEP_INIT_ORDER" | "VERTZ_CTX_COLLISION" | "VERTZ_DEAD_CODE";
2
+ type DiagnosticCode = "VERTZ_SCHEMA_NAMING" | "VERTZ_SCHEMA_PLACEMENT" | "VERTZ_SCHEMA_EXECUTION" | "VERTZ_SCHEMA_MISSING_ID" | "VERTZ_SCHEMA_DYNAMIC_NAME" | "VERTZ_MODULE_CIRCULAR" | "VERTZ_MODULE_EXPORT_INVALID" | "VERTZ_MODULE_IMPORT_MISSING" | "VERTZ_MODULE_DUPLICATE_NAME" | "VERTZ_MODULE_DYNAMIC_NAME" | "VERTZ_MODULE_OPTIONS_INVALID" | "VERTZ_MODULE_WRONG_OWNERSHIP" | "VERTZ_SERVICE_INJECT_MISSING" | "VERTZ_SERVICE_UNUSED" | "VERTZ_SERVICE_DYNAMIC_NAME" | "VERTZ_ENV_MISSING_DEFAULT" | "VERTZ_ENV_DUPLICATE" | "VERTZ_ENV_DYNAMIC_CONFIG" | "VERTZ_MW_MISSING_NAME" | "VERTZ_MW_MISSING_HANDLER" | "VERTZ_MW_DYNAMIC_NAME" | "VERTZ_MW_NON_OBJECT_CONFIG" | "VERTZ_MW_REQUIRES_UNSATISFIED" | "VERTZ_MW_PROVIDES_COLLISION" | "VERTZ_MW_ORDER_INVALID" | "VERTZ_RT_UNKNOWN_MODULE_DEF" | "VERTZ_RT_DYNAMIC_PATH" | "VERTZ_RT_MISSING_HANDLER" | "VERTZ_RT_MISSING_PREFIX" | "VERTZ_RT_DYNAMIC_CONFIG" | "VERTZ_RT_INVALID_PATH" | "VERTZ_ROUTE_DUPLICATE" | "VERTZ_ROUTE_PARAM_MISMATCH" | "VERTZ_ROUTE_MISSING_RESPONSE" | "VERTZ_APP_MISSING" | "VERTZ_APP_NOT_FOUND" | "VERTZ_APP_DUPLICATE" | "VERTZ_APP_BASEPATH_FORMAT" | "VERTZ_APP_INLINE_MODULE" | "VERTZ_DEP_CYCLE" | "VERTZ_DEP_CIRCULAR" | "VERTZ_DEP_UNRESOLVED_INJECT" | "VERTZ_DEP_INIT_ORDER" | "VERTZ_CTX_COLLISION" | "VERTZ_DEAD_CODE" | "ENTITY_MISSING_ARGS" | "ENTITY_NON_LITERAL_NAME" | "ENTITY_INVALID_NAME" | "ENTITY_DUPLICATE_NAME" | "ENTITY_CONFIG_NOT_OBJECT" | "ENTITY_MISSING_MODEL" | "ENTITY_MODEL_UNRESOLVABLE" | "ENTITY_ACTION_NAME_COLLISION" | "ENTITY_ACTION_MISSING_SCHEMA" | "ENTITY_ACTION_INVALID_METHOD" | "ENTITY_UNKNOWN_ACCESS_OP" | "ENTITY_UNRESOLVED_IMPORT" | "ENTITY_ROUTE_COLLISION" | "ENTITY_NO_ROUTES";
3
3
  interface SourceContext {
4
4
  lines: {
5
5
  number: number;
@@ -37,6 +37,7 @@ interface AppIR {
37
37
  modules: ModuleIR[];
38
38
  middleware: MiddlewareIR[];
39
39
  schemas: SchemaIR[];
40
+ entities: EntityIR[];
40
41
  dependencyGraph: DependencyGraphIR;
41
42
  diagnostics: Diagnostic[];
42
43
  }
@@ -133,6 +134,7 @@ interface MiddlewareRef {
133
134
  interface SchemaIR extends SourceLocation {
134
135
  name: string;
135
136
  id?: string;
137
+ moduleName: string;
136
138
  namingConvention: SchemaNameParts;
137
139
  jsonSchema?: Record<string, unknown>;
138
140
  isNamed: boolean;
@@ -153,6 +155,60 @@ interface InlineSchemaRef {
153
155
  kind: "inline";
154
156
  sourceFile: string;
155
157
  jsonSchema?: Record<string, unknown>;
158
+ resolvedFields?: ResolvedField[];
159
+ }
160
+ /** Structured field info extracted from resolved TypeScript types. */
161
+ interface ResolvedField {
162
+ name: string;
163
+ tsType: "string" | "number" | "boolean" | "date" | "unknown";
164
+ optional: boolean;
165
+ }
166
+ interface EntityIR extends SourceLocation {
167
+ name: string;
168
+ modelRef: EntityModelRef;
169
+ access: EntityAccessIR;
170
+ hooks: EntityHooksIR;
171
+ actions: EntityActionIR[];
172
+ relations: EntityRelationIR[];
173
+ }
174
+ interface EntityModelRef {
175
+ variableName: string;
176
+ importSource?: string;
177
+ tableName?: string;
178
+ schemaRefs: EntityModelSchemaRefs;
179
+ }
180
+ interface EntityModelSchemaRefs {
181
+ response?: SchemaRef;
182
+ createInput?: SchemaRef;
183
+ updateInput?: SchemaRef;
184
+ resolved: boolean;
185
+ }
186
+ interface EntityAccessIR {
187
+ list: EntityAccessRuleKind;
188
+ get: EntityAccessRuleKind;
189
+ create: EntityAccessRuleKind;
190
+ update: EntityAccessRuleKind;
191
+ delete: EntityAccessRuleKind;
192
+ custom: Record<string, EntityAccessRuleKind>;
193
+ }
194
+ type EntityAccessRuleKind = "none" | "false" | "function";
195
+ interface EntityHooksIR {
196
+ before: ("create" | "update")[];
197
+ after: ("create" | "update" | "delete")[];
198
+ }
199
+ interface EntityActionIR extends SourceLocation {
200
+ name: string;
201
+ method: HttpMethod;
202
+ path?: string;
203
+ params?: SchemaRef;
204
+ query?: SchemaRef;
205
+ headers?: SchemaRef;
206
+ body?: SchemaRef;
207
+ response?: SchemaRef;
208
+ }
209
+ interface EntityRelationIR {
210
+ name: string;
211
+ selection: "all" | string[];
156
212
  }
157
213
  interface ModuleDefContext {
158
214
  moduleDefVariables: Map<string, string>;
@@ -215,6 +271,7 @@ declare function defineConfig(config: VertzConfig): VertzConfig;
215
271
  declare function resolveConfig(config?: VertzConfig): ResolvedConfig;
216
272
  interface Analyzer<T> {
217
273
  analyze(): Promise<T>;
274
+ getDiagnostics(): Diagnostic[];
218
275
  }
219
276
  declare abstract class BaseAnalyzer<T> implements Analyzer<T> {
220
277
  protected readonly project: Project;
@@ -292,6 +349,49 @@ declare class RouteAnalyzer extends BaseAnalyzer<RouteAnalyzerResult> {
292
349
  private extractMiddlewareRefs;
293
350
  private generateOperationId;
294
351
  }
352
+ interface EntityAnalyzerResult {
353
+ entities: EntityIR[];
354
+ }
355
+ declare class EntityAnalyzer extends BaseAnalyzer<EntityAnalyzerResult> {
356
+ private debug;
357
+ analyze(): Promise<EntityAnalyzerResult>;
358
+ private findEntityCalls;
359
+ private extractEntity;
360
+ private extractModelRef;
361
+ private findImportForIdentifier;
362
+ private resolveModelSchemas;
363
+ private extractSchemaType;
364
+ /**
365
+ * Build JSON Schema from resolved fields.
366
+ * Maps tsType ('string' | 'number' | 'boolean' | 'date' | 'unknown') to JSON Schema types.
367
+ */
368
+ private buildJsonSchema;
369
+ /**
370
+ * Map tsType to JSON Schema type.
371
+ * Handles column types like text → string, boolean → boolean, uuid → string with format,
372
+ * timestamp with time zone → string with date-time format, integer → integer, real/float → number.
373
+ */
374
+ private tsTypeToJsonSchema;
375
+ /**
376
+ * Navigate through SchemaLike<T> to extract T's field info.
377
+ * SchemaLike<T>.parse() returns { ok: true; data: T } | { ok: false; error: Error }.
378
+ * We unwrap the Result union to get T from the success branch's `data` property.
379
+ */
380
+ private resolveFieldsFromSchemaType;
381
+ /**
382
+ * Unwrap a Result type to extract T from the success branch.
383
+ * Handles: { ok: true; data: T } | { ok: false; error: Error } → T
384
+ * Falls back to the type itself if it's not a Result union (backward compat).
385
+ */
386
+ private unwrapResultType;
387
+ private mapTsType;
388
+ private extractAccess;
389
+ private classifyAccessRule;
390
+ private extractHooks;
391
+ private extractActions;
392
+ private resolveSchemaFromExpression;
393
+ private extractRelations;
394
+ }
295
395
  import { Expression as Expression2, SourceFile } from "ts-morph";
296
396
  interface SchemaAnalyzerResult {
297
397
  schemas: SchemaIR[];
@@ -338,6 +438,7 @@ interface CompilerDependencies {
338
438
  middleware: Analyzer<MiddlewareAnalyzerResult>;
339
439
  module: Analyzer<ModuleAnalyzerResult>;
340
440
  app: Analyzer<AppAnalyzerResult>;
441
+ entity: Analyzer<EntityAnalyzerResult>;
341
442
  dependencyGraph: Analyzer<DependencyGraphResult>;
342
443
  };
343
444
  validators: Validator[];
@@ -629,6 +730,8 @@ declare class IncrementalCompiler {
629
730
  declare function createEmptyDependencyGraph(): DependencyGraphIR;
630
731
  declare function createEmptyAppIR(): AppIR;
631
732
  declare function addDiagnosticsToIR(ir: AppIR, diagnostics: readonly Diagnostic[]): AppIR;
733
+ declare function injectEntityRoutes(ir: AppIR): void;
734
+ declare function detectRouteCollisions(ir: AppIR): Diagnostic[];
632
735
  declare function mergeIR(base: AppIR, partial: Partial<AppIR>): AppIR;
633
736
  import { ChildProcess } from "node:child_process";
634
737
  interface TypecheckDiagnostic {
@@ -721,4 +824,4 @@ declare class PlacementValidator implements Validator {
721
824
  private checkFileLocation;
722
825
  private checkMixedExports;
723
826
  }
724
- export { typecheckWatch, typecheck, resolveImportPath, resolveIdentifier, resolveExport, resolveConfig, renderSchemaRegistryFile, renderRouteTableFile, renderBootFile, parseWatchBlock, parseTscOutput, parseSchemaName2 as parseSchemaName, parseInjectRefs, parseImports, mergeIR, mergeDiagnostics, isSchemaFile, isSchemaExpression, isFromImport, hasErrors, getVariableNameForCall, getStringValue, getSourceLocation, getPropertyValue, getProperties, getNumberValue, getBooleanValue, getArrayElements, findMethodCallsOnVariable, findCallExpressions, findAffectedModules, filterBySeverity, extractSchemaId, extractObjectLiteral, extractMethodSignatures, extractIdentifierNames, defineConfig, createSchemaExecutor, createNamedSchemaRef, createInlineSchemaRef, createEmptyDependencyGraph, createEmptyAppIR, createDiagnosticFromLocation, createDiagnostic, createCompiler, categorizeChanges, buildSchemaRegistry, buildRouteTable, buildManifest, buildBootManifest, addDiagnosticsToIR, VertzConfig, Validator, ValidationConfig, ValidPart, ValidOperation, TypecheckWatchOptions, TypecheckResult, TypecheckOptions, TypecheckDiagnostic, SourceLocation, SourceContext, ServiceMethodParam, ServiceMethodIR, ServiceIR, ServiceAnalyzerResult, ServiceAnalyzer, SchemaRegistryManifest, SchemaRegistryGenerator, SchemaRegistryEntry, SchemaRef, SchemaNameParts, SchemaIR, SchemaExecutor, SchemaExecutionResult, SchemaConfig, SchemaAnalyzerResult, SchemaAnalyzer, RouterIR, RouteTableSchemas, RouteTableManifest, RouteTableGenerator, RouteTableEntry, RouteIR, RouteAnalyzerResult, RouteAnalyzer, ResolvedImport, ResolvedConfig, PlacementValidator, ParsedSchemaName, OpenAPITag, OpenAPIServer, OpenAPIResponse, OpenAPIRequestBody, OpenAPIPathItem, OpenAPIParameter, OpenAPIOperation, OpenAPIInfo, OpenAPIGenerator, OpenAPIDocument, OpenAPIConfig, NamingValidator, NamedSchemaRef, ModuleValidator, ModuleRegistration, ModuleIR, ModuleDefContext, ModuleAnalyzerResult, ModuleAnalyzer, MiddlewareRef, MiddlewareIR, MiddlewareAnalyzerResult, MiddlewareAnalyzer, ManifestRoute, ManifestModule, ManifestMiddleware, ManifestGenerator, ManifestDiagnostic, ManifestDependencyEdge, JSONSchemaObject, InlineSchemaRef, InjectRef, IncrementalResult, IncrementalCompiler, ImportRef, HttpMethod, Generator, FileChange, FileCategory, EnvVariableIR, EnvIR, EnvAnalyzerResult, EnvAnalyzer, DiagnosticSeverity, DiagnosticCode, Diagnostic, DependencyNodeKind, DependencyNode, DependencyGraphResult, DependencyGraphInput, DependencyGraphIR, DependencyGraphAnalyzer, DependencyEdgeKind, DependencyEdge, CreateDiagnosticOptions, CompletenessValidator, CompilerDependencies, CompilerConfig, Compiler, CompileResult, CategorizedChanges, CategorizeOptions, BootModuleEntry, BootMiddlewareEntry, BootManifest, BootGenerator, BaseGenerator, BaseAnalyzer, AppManifest, AppIR, AppDefinition, AppAnalyzerResult, AppAnalyzer, Analyzer };
827
+ export { typecheckWatch, typecheck, resolveImportPath, resolveIdentifier, resolveExport, resolveConfig, renderSchemaRegistryFile, renderRouteTableFile, renderBootFile, parseWatchBlock, parseTscOutput, parseSchemaName2 as parseSchemaName, parseInjectRefs, parseImports, mergeIR, mergeDiagnostics, isSchemaFile, isSchemaExpression, isFromImport, injectEntityRoutes, hasErrors, getVariableNameForCall, getStringValue, getSourceLocation, getPropertyValue, getProperties, getNumberValue, getBooleanValue, getArrayElements, findMethodCallsOnVariable, findCallExpressions, findAffectedModules, filterBySeverity, extractSchemaId, extractObjectLiteral, extractMethodSignatures, extractIdentifierNames, detectRouteCollisions, defineConfig, createSchemaExecutor, createNamedSchemaRef, createInlineSchemaRef, createEmptyDependencyGraph, createEmptyAppIR, createDiagnosticFromLocation, createDiagnostic, createCompiler, categorizeChanges, buildSchemaRegistry, buildRouteTable, buildManifest, buildBootManifest, addDiagnosticsToIR, VertzConfig, Validator, ValidationConfig, ValidPart, ValidOperation, TypecheckWatchOptions, TypecheckResult, TypecheckOptions, TypecheckDiagnostic, SourceLocation, SourceContext, ServiceMethodParam, ServiceMethodIR, ServiceIR, ServiceAnalyzerResult, ServiceAnalyzer, SchemaRegistryManifest, SchemaRegistryGenerator, SchemaRegistryEntry, SchemaRef, SchemaNameParts, SchemaIR, SchemaExecutor, SchemaExecutionResult, SchemaConfig, SchemaAnalyzerResult, SchemaAnalyzer, RouterIR, RouteTableSchemas, RouteTableManifest, RouteTableGenerator, RouteTableEntry, RouteIR, RouteAnalyzerResult, RouteAnalyzer, ResolvedImport, ResolvedConfig, PlacementValidator, ParsedSchemaName, OpenAPITag, OpenAPIServer, OpenAPIResponse, OpenAPIRequestBody, OpenAPIPathItem, OpenAPIParameter, OpenAPIOperation, OpenAPIInfo, OpenAPIGenerator, OpenAPIDocument, OpenAPIConfig, NamingValidator, NamedSchemaRef, ModuleValidator, ModuleRegistration, ModuleIR, ModuleDefContext, ModuleAnalyzerResult, ModuleAnalyzer, MiddlewareRef, MiddlewareIR, MiddlewareAnalyzerResult, MiddlewareAnalyzer, ManifestRoute, ManifestModule, ManifestMiddleware, ManifestGenerator, ManifestDiagnostic, ManifestDependencyEdge, JSONSchemaObject, InlineSchemaRef, InjectRef, IncrementalResult, IncrementalCompiler, ImportRef, HttpMethod, Generator, FileChange, FileCategory, EnvVariableIR, EnvIR, EnvAnalyzerResult, EnvAnalyzer, EntityRelationIR, EntityModelSchemaRefs, EntityModelRef, EntityIR, EntityHooksIR, EntityAnalyzerResult, EntityAnalyzer, EntityActionIR, EntityAccessRuleKind, EntityAccessIR, DiagnosticSeverity, DiagnosticCode, Diagnostic, DependencyNodeKind, DependencyNode, DependencyGraphResult, DependencyGraphInput, DependencyGraphIR, DependencyGraphAnalyzer, DependencyEdgeKind, DependencyEdge, CreateDiagnosticOptions, CompletenessValidator, CompilerDependencies, CompilerConfig, Compiler, CompileResult, CategorizedChanges, CategorizeOptions, BootModuleEntry, BootMiddlewareEntry, BootManifest, BootGenerator, BaseGenerator, BaseAnalyzer, AppManifest, AppIR, AppDefinition, AppAnalyzerResult, AppAnalyzer, Analyzer };
package/dist/index.js CHANGED
@@ -211,7 +211,10 @@ class AppAnalyzer extends BaseAnalyzer {
211
211
  async analyze() {
212
212
  const allAppCalls = [];
213
213
  for (const file2 of this.project.getSourceFiles()) {
214
- const appCalls = findCallExpressions(file2, "vertz", "app");
214
+ const appCalls = [
215
+ ...findCallExpressions(file2, "vertz", "app"),
216
+ ...findCallExpressions(file2, "vertz", "server")
217
+ ];
215
218
  for (const call2 of appCalls) {
216
219
  allAppCalls.push({ call: call2, file: file2 });
217
220
  }
@@ -628,6 +631,7 @@ class SchemaAnalyzer extends BaseAnalyzer {
628
631
  name,
629
632
  ...loc,
630
633
  id: id ?? undefined,
634
+ moduleName: "",
631
635
  namingConvention: parseSchemaName(name),
632
636
  isNamed: id !== null
633
637
  });
@@ -1268,7 +1272,483 @@ function joinPaths(prefix, path) {
1268
1272
  function sanitizePath(path) {
1269
1273
  return path.replace(/^\//, "").replace(/[/:.]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "") || "root";
1270
1274
  }
1275
+ // src/analyzers/entity-analyzer.ts
1276
+ import { SyntaxKind as SyntaxKind9 } from "ts-morph";
1277
+ var ENTITY_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
1278
+ var CRUD_OPS = ["list", "get", "create", "update", "delete"];
1279
+
1280
+ class EntityAnalyzer extends BaseAnalyzer {
1281
+ debug(msg) {
1282
+ if (process.env["VERTZ_DEBUG"]?.includes("entities")) {
1283
+ console.log(`[entity-analyzer] ${msg}`);
1284
+ }
1285
+ }
1286
+ async analyze() {
1287
+ const entities = [];
1288
+ const seenNames = new Map;
1289
+ const files = this.project.getSourceFiles();
1290
+ this.debug(`Scanning ${files.length} source files...`);
1291
+ for (const file of files) {
1292
+ const calls = this.findEntityCalls(file);
1293
+ for (const call of calls) {
1294
+ const entity = this.extractEntity(file, call);
1295
+ if (!entity)
1296
+ continue;
1297
+ const existing = seenNames.get(entity.name);
1298
+ if (existing) {
1299
+ this.addDiagnostic({
1300
+ code: "ENTITY_DUPLICATE_NAME",
1301
+ severity: "error",
1302
+ message: `Entity "${entity.name}" is already defined at ${existing.sourceFile}:${existing.sourceLine}`,
1303
+ ...getSourceLocation(call)
1304
+ });
1305
+ continue;
1306
+ }
1307
+ seenNames.set(entity.name, entity);
1308
+ entities.push(entity);
1309
+ this.debug(`Detected entity: "${entity.name}" at ${entity.sourceFile}:${entity.sourceLine}`);
1310
+ this.debug(` model: ${entity.modelRef.variableName} (resolved: ${entity.modelRef.schemaRefs.resolved ? "✅" : "❌"})`);
1311
+ const accessStatus = CRUD_OPS.map((op) => {
1312
+ const kind = entity.access[op];
1313
+ return `${op} ${kind === "false" ? "✗" : "✓"}`;
1314
+ }).join(", ");
1315
+ this.debug(` access: ${accessStatus}`);
1316
+ if (entity.hooks.before.length > 0 || entity.hooks.after.length > 0) {
1317
+ this.debug(` hooks: before[${entity.hooks.before.join(",")}], after[${entity.hooks.after.join(",")}]`);
1318
+ }
1319
+ if (entity.actions.length > 0) {
1320
+ this.debug(` actions: ${entity.actions.map((a) => a.name).join(", ")}`);
1321
+ }
1322
+ }
1323
+ }
1324
+ return { entities };
1325
+ }
1326
+ findEntityCalls(file) {
1327
+ const validCalls = [];
1328
+ for (const call of file.getDescendantsOfKind(SyntaxKind9.CallExpression)) {
1329
+ const expr = call.getExpression();
1330
+ if (expr.isKind(SyntaxKind9.Identifier)) {
1331
+ const isValid = isFromImport(expr, "@vertz/server");
1332
+ if (!isValid && expr.getText() === "entity") {
1333
+ this.addDiagnostic({
1334
+ code: "ENTITY_UNRESOLVED_IMPORT",
1335
+ severity: "error",
1336
+ message: "entity() call does not resolve to @vertz/server",
1337
+ ...getSourceLocation(call)
1338
+ });
1339
+ continue;
1340
+ }
1341
+ if (isValid) {
1342
+ validCalls.push(call);
1343
+ }
1344
+ continue;
1345
+ }
1346
+ if (expr.isKind(SyntaxKind9.PropertyAccessExpression)) {
1347
+ const propName = expr.getName();
1348
+ if (propName !== "entity")
1349
+ continue;
1350
+ const obj = expr.getExpression();
1351
+ if (!obj.isKind(SyntaxKind9.Identifier))
1352
+ continue;
1353
+ const sourceFile = obj.getSourceFile();
1354
+ const importDecl = sourceFile.getImportDeclarations().find((d) => d.getModuleSpecifierValue() === "@vertz/server" && d.getNamespaceImport()?.getText() === obj.getText());
1355
+ if (importDecl) {
1356
+ validCalls.push(call);
1357
+ }
1358
+ }
1359
+ }
1360
+ return validCalls;
1361
+ }
1362
+ extractEntity(_file, call) {
1363
+ const args = call.getArguments();
1364
+ const loc = getSourceLocation(call);
1365
+ if (args.length < 2) {
1366
+ this.addDiagnostic({
1367
+ code: "ENTITY_MISSING_ARGS",
1368
+ severity: "error",
1369
+ message: "entity() requires two arguments: name and config",
1370
+ ...loc
1371
+ });
1372
+ return null;
1373
+ }
1374
+ const name = getStringValue(args[0]);
1375
+ if (name === null) {
1376
+ this.addDiagnostic({
1377
+ code: "ENTITY_NON_LITERAL_NAME",
1378
+ severity: "error",
1379
+ message: "entity() name must be a string literal",
1380
+ ...loc
1381
+ });
1382
+ return null;
1383
+ }
1384
+ if (!ENTITY_NAME_PATTERN.test(name)) {
1385
+ this.addDiagnostic({
1386
+ code: "ENTITY_INVALID_NAME",
1387
+ severity: "error",
1388
+ message: `Entity name must match /^[a-z][a-z0-9-]*$/. Got: "${name}"`,
1389
+ ...loc
1390
+ });
1391
+ return null;
1392
+ }
1393
+ const configObj = extractObjectLiteral(call, 1);
1394
+ if (!configObj) {
1395
+ this.addDiagnostic({
1396
+ code: "ENTITY_CONFIG_NOT_OBJECT",
1397
+ severity: "warning",
1398
+ message: "entity() config must be an object literal for static analysis",
1399
+ ...loc
1400
+ });
1401
+ return null;
1402
+ }
1403
+ const modelRef = this.extractModelRef(configObj, loc);
1404
+ if (!modelRef)
1405
+ return null;
1406
+ const access = this.extractAccess(configObj);
1407
+ const hooks = this.extractHooks(configObj);
1408
+ const actions = this.extractActions(configObj);
1409
+ const relations = this.extractRelations(configObj);
1410
+ for (const action of actions) {
1411
+ if (CRUD_OPS.includes(action.name)) {
1412
+ this.addDiagnostic({
1413
+ code: "ENTITY_ACTION_NAME_COLLISION",
1414
+ severity: "error",
1415
+ message: `Custom action "${action.name}" collides with built-in CRUD operation`,
1416
+ ...action
1417
+ });
1418
+ }
1419
+ }
1420
+ for (const customOp of Object.keys(access.custom)) {
1421
+ if (!actions.some((a) => a.name === customOp)) {
1422
+ this.addDiagnostic({
1423
+ code: "ENTITY_UNKNOWN_ACCESS_OP",
1424
+ severity: "warning",
1425
+ message: `Unknown access operation "${customOp}" — not a CRUD op or custom action`,
1426
+ ...loc
1427
+ });
1428
+ }
1429
+ }
1430
+ return { name, modelRef, access, hooks, actions, relations, ...loc };
1431
+ }
1432
+ extractModelRef(configObj, loc) {
1433
+ const modelExpr = getPropertyValue(configObj, "model");
1434
+ if (!modelExpr) {
1435
+ this.addDiagnostic({
1436
+ code: "ENTITY_MISSING_MODEL",
1437
+ severity: "error",
1438
+ message: "entity() requires a model property",
1439
+ ...loc
1440
+ });
1441
+ return null;
1442
+ }
1443
+ const variableName = modelExpr.isKind(SyntaxKind9.Identifier) ? modelExpr.getText() : modelExpr.getText();
1444
+ let importSource;
1445
+ if (modelExpr.isKind(SyntaxKind9.Identifier)) {
1446
+ const importInfo = this.findImportForIdentifier(modelExpr);
1447
+ if (importInfo) {
1448
+ importSource = importInfo.importDecl.getModuleSpecifierValue();
1449
+ }
1450
+ }
1451
+ const schemaRefs = this.resolveModelSchemas(modelExpr);
1452
+ return { variableName, importSource, schemaRefs };
1453
+ }
1454
+ findImportForIdentifier(identifier) {
1455
+ const sourceFile = identifier.getSourceFile();
1456
+ const name = identifier.getText();
1457
+ for (const importDecl of sourceFile.getImportDeclarations()) {
1458
+ for (const specifier of importDecl.getNamedImports()) {
1459
+ const localName = specifier.getAliasNode()?.getText() ?? specifier.getName();
1460
+ if (localName === name) {
1461
+ return { importDecl, originalName: specifier.getName() };
1462
+ }
1463
+ }
1464
+ const nsImport = importDecl.getNamespaceImport();
1465
+ if (nsImport && nsImport.getText() === name) {
1466
+ return { importDecl, originalName: "*" };
1467
+ }
1468
+ }
1469
+ return null;
1470
+ }
1471
+ resolveModelSchemas(modelExpr) {
1472
+ try {
1473
+ const modelType = modelExpr.getType();
1474
+ const schemasProp = modelType.getProperty("schemas");
1475
+ if (!schemasProp)
1476
+ return { resolved: false };
1477
+ const schemasType = schemasProp.getTypeAtLocation(modelExpr);
1478
+ const response = this.extractSchemaType(schemasType, "response", modelExpr);
1479
+ const createInput = this.extractSchemaType(schemasType, "createInput", modelExpr);
1480
+ const updateInput = this.extractSchemaType(schemasType, "updateInput", modelExpr);
1481
+ return {
1482
+ response,
1483
+ createInput,
1484
+ updateInput,
1485
+ resolved: response !== undefined || createInput !== undefined || updateInput !== undefined
1486
+ };
1487
+ } catch {
1488
+ return { resolved: false };
1489
+ }
1490
+ }
1491
+ extractSchemaType(parentType, propertyName, location) {
1492
+ const prop = parentType.getProperty(propertyName);
1493
+ if (!prop)
1494
+ return;
1495
+ const propType = prop.getTypeAtLocation(location);
1496
+ const resolvedFields = this.resolveFieldsFromSchemaType(propType, location);
1497
+ const jsonSchema = this.buildJsonSchema(resolvedFields);
1498
+ return {
1499
+ kind: "inline",
1500
+ sourceFile: location.getSourceFile().getFilePath(),
1501
+ jsonSchema,
1502
+ resolvedFields
1503
+ };
1504
+ }
1505
+ buildJsonSchema(resolvedFields) {
1506
+ if (!resolvedFields || resolvedFields.length === 0) {
1507
+ return {};
1508
+ }
1509
+ const properties = {};
1510
+ const required = [];
1511
+ for (const field of resolvedFields) {
1512
+ const fieldSchema = this.tsTypeToJsonSchema(field.tsType);
1513
+ properties[field.name] = fieldSchema;
1514
+ if (!field.optional) {
1515
+ required.push(field.name);
1516
+ }
1517
+ }
1518
+ return {
1519
+ type: "object",
1520
+ properties,
1521
+ ...required.length > 0 ? { required } : {}
1522
+ };
1523
+ }
1524
+ tsTypeToJsonSchema(tsType) {
1525
+ switch (tsType) {
1526
+ case "string":
1527
+ return { type: "string" };
1528
+ case "number":
1529
+ return { type: "number" };
1530
+ case "boolean":
1531
+ return { type: "boolean" };
1532
+ case "date":
1533
+ return { type: "string", format: "date-time" };
1534
+ case "unknown":
1535
+ default:
1536
+ return {};
1537
+ }
1538
+ }
1539
+ resolveFieldsFromSchemaType(schemaType, location) {
1540
+ try {
1541
+ const parseProp = schemaType.getProperty("parse");
1542
+ if (!parseProp)
1543
+ return;
1544
+ const parseType = parseProp.getTypeAtLocation(location);
1545
+ const callSignatures = parseType.getCallSignatures();
1546
+ if (callSignatures.length === 0)
1547
+ return;
1548
+ const returnType = callSignatures[0]?.getReturnType();
1549
+ if (!returnType)
1550
+ return;
1551
+ const dataType = this.unwrapResultType(returnType, location);
1552
+ if (!dataType)
1553
+ return;
1554
+ const properties = dataType.getProperties();
1555
+ if (properties.length === 0)
1556
+ return;
1557
+ const fields = [];
1558
+ for (const fieldProp of properties) {
1559
+ const name = fieldProp.getName();
1560
+ const fieldType = fieldProp.getTypeAtLocation(location);
1561
+ const optional = fieldProp.isOptional();
1562
+ const tsType = this.mapTsType(fieldType);
1563
+ fields.push({ name, tsType, optional });
1564
+ }
1565
+ return fields;
1566
+ } catch {
1567
+ return;
1568
+ }
1569
+ }
1570
+ unwrapResultType(type, location) {
1571
+ if (type.isUnion()) {
1572
+ for (const member of type.getUnionTypes()) {
1573
+ const dataProp2 = member.getProperty("data");
1574
+ if (dataProp2) {
1575
+ return dataProp2.getTypeAtLocation(location);
1576
+ }
1577
+ }
1578
+ return;
1579
+ }
1580
+ const dataProp = type.getProperty("data");
1581
+ if (dataProp) {
1582
+ return dataProp.getTypeAtLocation(location);
1583
+ }
1584
+ return type;
1585
+ }
1586
+ mapTsType(type) {
1587
+ const typeText = type.getText();
1588
+ if (type.isUnion()) {
1589
+ const nonUndefined = type.getUnionTypes().filter((t) => !t.isUndefined());
1590
+ if (nonUndefined.length === 1 && nonUndefined[0]) {
1591
+ return this.mapTsType(nonUndefined[0]);
1592
+ }
1593
+ }
1594
+ if (type.isString() || type.isStringLiteral())
1595
+ return "string";
1596
+ if (type.isNumber() || type.isNumberLiteral())
1597
+ return "number";
1598
+ if (type.isBoolean() || type.isBooleanLiteral())
1599
+ return "boolean";
1600
+ if (typeText === "Date")
1601
+ return "date";
1602
+ return "unknown";
1603
+ }
1604
+ extractAccess(configObj) {
1605
+ const defaults = {
1606
+ list: "none",
1607
+ get: "none",
1608
+ create: "none",
1609
+ update: "none",
1610
+ delete: "none",
1611
+ custom: {}
1612
+ };
1613
+ const accessExpr = getPropertyValue(configObj, "access");
1614
+ if (!accessExpr || !accessExpr.isKind(SyntaxKind9.ObjectLiteralExpression))
1615
+ return defaults;
1616
+ const result = { ...defaults };
1617
+ const knownOps = new Set([...CRUD_OPS]);
1618
+ for (const { name, value } of getProperties(accessExpr)) {
1619
+ const kind = this.classifyAccessRule(value);
1620
+ if (knownOps.has(name)) {
1621
+ result[name] = kind;
1622
+ } else {
1623
+ result.custom[name] = kind;
1624
+ }
1625
+ }
1626
+ return result;
1627
+ }
1628
+ classifyAccessRule(expr) {
1629
+ const boolVal = getBooleanValue(expr);
1630
+ if (boolVal === false)
1631
+ return "false";
1632
+ if (boolVal === true)
1633
+ return "none";
1634
+ return "function";
1635
+ }
1636
+ extractHooks(configObj) {
1637
+ const hooks = { before: [], after: [] };
1638
+ const beforeExpr = getPropertyValue(configObj, "before");
1639
+ if (beforeExpr?.isKind(SyntaxKind9.ObjectLiteralExpression)) {
1640
+ for (const { name } of getProperties(beforeExpr)) {
1641
+ if (name === "create" || name === "update")
1642
+ hooks.before.push(name);
1643
+ }
1644
+ }
1645
+ const afterExpr = getPropertyValue(configObj, "after");
1646
+ if (afterExpr?.isKind(SyntaxKind9.ObjectLiteralExpression)) {
1647
+ for (const { name } of getProperties(afterExpr)) {
1648
+ if (name === "create" || name === "update" || name === "delete")
1649
+ hooks.after.push(name);
1650
+ }
1651
+ }
1652
+ return hooks;
1653
+ }
1654
+ extractActions(configObj) {
1655
+ const actionsExpr = getPropertyValue(configObj, "actions");
1656
+ if (!actionsExpr?.isKind(SyntaxKind9.ObjectLiteralExpression))
1657
+ return [];
1658
+ return getProperties(actionsExpr).map(({ name, value }) => {
1659
+ const actionObj = value.isKind(SyntaxKind9.ObjectLiteralExpression) ? value : null;
1660
+ const loc = getSourceLocation(value);
1661
+ const bodyExpr = actionObj ? getPropertyValue(actionObj, "body") : null;
1662
+ const responseExpr = actionObj ? getPropertyValue(actionObj, "response") : null;
1663
+ if (!bodyExpr && !responseExpr) {
1664
+ this.addDiagnostic({
1665
+ code: "ENTITY_ACTION_MISSING_SCHEMA",
1666
+ severity: "warning",
1667
+ message: `Custom action "${name}" is missing body and response schema`,
1668
+ ...loc
1669
+ });
1670
+ }
1671
+ const body = bodyExpr ? this.resolveSchemaFromExpression(bodyExpr, loc) : undefined;
1672
+ const response = responseExpr ? this.resolveSchemaFromExpression(responseExpr, loc) : undefined;
1673
+ const methodExpr = actionObj ? getPropertyValue(actionObj, "method") : null;
1674
+ let method = "POST";
1675
+ if (methodExpr) {
1676
+ const methodStr = getStringValue(methodExpr);
1677
+ const validMethods = [
1678
+ "GET",
1679
+ "POST",
1680
+ "PUT",
1681
+ "DELETE",
1682
+ "PATCH",
1683
+ "HEAD",
1684
+ "OPTIONS"
1685
+ ];
1686
+ if (methodStr && validMethods.includes(methodStr)) {
1687
+ method = methodStr;
1688
+ } else {
1689
+ this.addDiagnostic({
1690
+ code: "ENTITY_ACTION_INVALID_METHOD",
1691
+ severity: "error",
1692
+ message: `Custom action "${name}" has invalid method "${methodStr ?? "(non-string)"}" — must be one of GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS`,
1693
+ ...loc
1694
+ });
1695
+ }
1696
+ }
1697
+ const pathExpr = actionObj ? getPropertyValue(actionObj, "path") : null;
1698
+ const path = pathExpr ? getStringValue(pathExpr) ?? undefined : undefined;
1699
+ const queryExpr = actionObj ? getPropertyValue(actionObj, "query") : null;
1700
+ const queryRef = queryExpr ? this.resolveSchemaFromExpression(queryExpr, loc) : undefined;
1701
+ const paramsExpr = actionObj ? getPropertyValue(actionObj, "params") : null;
1702
+ const paramsRef = paramsExpr ? this.resolveSchemaFromExpression(paramsExpr, loc) : undefined;
1703
+ const headersExpr = actionObj ? getPropertyValue(actionObj, "headers") : null;
1704
+ const headersRef = headersExpr ? this.resolveSchemaFromExpression(headersExpr, loc) : undefined;
1705
+ return {
1706
+ name,
1707
+ method,
1708
+ path,
1709
+ params: paramsRef,
1710
+ query: queryRef,
1711
+ headers: headersRef,
1712
+ body,
1713
+ response,
1714
+ ...loc
1715
+ };
1716
+ });
1717
+ }
1718
+ resolveSchemaFromExpression(expr, loc) {
1719
+ if (expr.isKind(SyntaxKind9.Identifier)) {
1720
+ const varName = expr.getText();
1721
+ return { kind: "named", schemaName: varName, sourceFile: loc.sourceFile };
1722
+ }
1723
+ try {
1724
+ const typeText = expr.getType().getText();
1725
+ return { kind: "inline", sourceFile: loc.sourceFile, jsonSchema: { __typeText: typeText } };
1726
+ } catch {
1727
+ return { kind: "inline", sourceFile: loc.sourceFile };
1728
+ }
1729
+ }
1730
+ extractRelations(configObj) {
1731
+ const relExpr = getPropertyValue(configObj, "relations");
1732
+ if (!relExpr?.isKind(SyntaxKind9.ObjectLiteralExpression))
1733
+ return [];
1734
+ return getProperties(relExpr).filter(({ value }) => {
1735
+ const boolVal = getBooleanValue(value);
1736
+ return boolVal !== false;
1737
+ }).map(({ name, value }) => {
1738
+ const boolVal = getBooleanValue(value);
1739
+ if (boolVal === true)
1740
+ return { name, selection: "all" };
1741
+ if (value.isKind(SyntaxKind9.ObjectLiteralExpression)) {
1742
+ const fields = getProperties(value).map((p) => p.name);
1743
+ return { name, selection: fields };
1744
+ }
1745
+ return { name, selection: "all" };
1746
+ });
1747
+ }
1748
+ }
1271
1749
  // src/compiler.ts
1750
+ import { mkdir } from "node:fs/promises";
1751
+ import { resolve } from "node:path";
1272
1752
  import { Project } from "ts-morph";
1273
1753
 
1274
1754
  // src/config.ts
@@ -1859,10 +2339,31 @@ function createEmptyAppIR() {
1859
2339
  modules: [],
1860
2340
  middleware: [],
1861
2341
  schemas: [],
2342
+ entities: [],
1862
2343
  dependencyGraph: createEmptyDependencyGraph(),
1863
2344
  diagnostics: []
1864
2345
  };
1865
2346
  }
2347
+ function enrichSchemasWithModuleNames(ir) {
2348
+ const schemaToModule = new Map;
2349
+ for (const mod of ir.modules) {
2350
+ for (const router of mod.routers) {
2351
+ for (const route of router.routes) {
2352
+ const refs = [route.body, route.query, route.params, route.headers, route.response];
2353
+ for (const ref of refs) {
2354
+ if (ref?.kind === "named") {
2355
+ schemaToModule.set(ref.schemaName, mod.name);
2356
+ }
2357
+ }
2358
+ }
2359
+ }
2360
+ }
2361
+ const schemas = ir.schemas.map((s) => ({
2362
+ ...s,
2363
+ moduleName: schemaToModule.get(s.name) ?? s.moduleName
2364
+ }));
2365
+ return { ...ir, schemas };
2366
+ }
1866
2367
  function addDiagnosticsToIR(ir, diagnostics) {
1867
2368
  return {
1868
2369
  ...ir,
@@ -1870,6 +2371,162 @@ function addDiagnosticsToIR(ir, diagnostics) {
1870
2371
  };
1871
2372
  }
1872
2373
 
2374
+ // src/ir/entity-route-injector.ts
2375
+ var SYNTHETIC_MODULE = "__entities";
2376
+ function toPascalCase(s) {
2377
+ return s.split("-").map((w) => w[0]?.toUpperCase() + w.slice(1)).join("");
2378
+ }
2379
+ function injectEntityRoutes(ir) {
2380
+ if (!ir.entities.length)
2381
+ return;
2382
+ const routes = [];
2383
+ for (const entity of ir.entities) {
2384
+ routes.push(...generateCrudRoutes(entity));
2385
+ routes.push(...generateActionRoutes(entity));
2386
+ }
2387
+ if (!routes.length)
2388
+ return;
2389
+ const router = {
2390
+ name: `${SYNTHETIC_MODULE}_router`,
2391
+ moduleName: SYNTHETIC_MODULE,
2392
+ prefix: "",
2393
+ inject: [],
2394
+ routes,
2395
+ sourceFile: "",
2396
+ sourceLine: 0,
2397
+ sourceColumn: 0
2398
+ };
2399
+ const module = {
2400
+ name: SYNTHETIC_MODULE,
2401
+ imports: [],
2402
+ services: [],
2403
+ routers: [router],
2404
+ exports: [],
2405
+ sourceFile: "",
2406
+ sourceLine: 0,
2407
+ sourceColumn: 0
2408
+ };
2409
+ ir.modules.push(module);
2410
+ }
2411
+ function generateCrudRoutes(entity) {
2412
+ const entityPascal = toPascalCase(entity.name);
2413
+ const basePath = `/${entity.name}`;
2414
+ const routes = [];
2415
+ const ops = [
2416
+ { op: "list", method: "GET", path: basePath, idParam: false },
2417
+ { op: "get", method: "GET", path: `${basePath}/:id`, idParam: true },
2418
+ { op: "create", method: "POST", path: basePath, idParam: false },
2419
+ { op: "update", method: "PATCH", path: `${basePath}/:id`, idParam: true },
2420
+ { op: "delete", method: "DELETE", path: `${basePath}/:id`, idParam: true }
2421
+ ];
2422
+ for (const { op, method, path } of ops) {
2423
+ const accessKind = entity.access[op];
2424
+ if (accessKind === "false")
2425
+ continue;
2426
+ const route = {
2427
+ method,
2428
+ path,
2429
+ fullPath: path,
2430
+ operationId: `${op}${entityPascal}`,
2431
+ middleware: [],
2432
+ tags: [entity.name],
2433
+ description: `${op} ${entity.name}`,
2434
+ ...entity
2435
+ };
2436
+ if (entity.modelRef.schemaRefs.resolved) {
2437
+ if (op === "create") {
2438
+ route.body = entity.modelRef.schemaRefs.createInput;
2439
+ route.response = entity.modelRef.schemaRefs.response;
2440
+ } else if (op === "update") {
2441
+ route.body = entity.modelRef.schemaRefs.updateInput;
2442
+ route.response = entity.modelRef.schemaRefs.response;
2443
+ } else if (op === "list") {
2444
+ route.response = wrapInPaginatedEnvelope(entity.modelRef.schemaRefs.response);
2445
+ } else {
2446
+ route.response = entity.modelRef.schemaRefs.response;
2447
+ }
2448
+ }
2449
+ routes.push(route);
2450
+ }
2451
+ return routes;
2452
+ }
2453
+ function wrapInPaginatedEnvelope(itemSchema) {
2454
+ if (!itemSchema)
2455
+ return;
2456
+ const itemJsonSchema = itemSchema.kind === "named" ? { $ref: `#/components/schemas/${itemSchema.schemaName}` } : itemSchema.jsonSchema ?? {};
2457
+ return {
2458
+ kind: "inline",
2459
+ sourceFile: itemSchema.sourceFile,
2460
+ jsonSchema: {
2461
+ type: "object",
2462
+ properties: {
2463
+ items: { type: "array", items: itemJsonSchema },
2464
+ total: { type: "number" },
2465
+ limit: { type: "number" },
2466
+ nextCursor: { type: ["string", "null"] },
2467
+ hasNextPage: { type: "boolean" }
2468
+ },
2469
+ required: ["items", "total", "limit", "nextCursor", "hasNextPage"]
2470
+ }
2471
+ };
2472
+ }
2473
+ function generateActionRoutes(entity) {
2474
+ const entityPascal = toPascalCase(entity.name);
2475
+ return entity.actions.filter((action) => entity.access.custom[action.name] !== "false").map((action) => {
2476
+ const method = action.method;
2477
+ const path = action.path ? `/${entity.name}/${action.path}` : `/${entity.name}/:id/${action.name}`;
2478
+ const fullPath = path;
2479
+ return {
2480
+ method,
2481
+ path,
2482
+ fullPath,
2483
+ operationId: `${action.name}${entityPascal}`,
2484
+ params: action.params,
2485
+ query: action.query,
2486
+ headers: action.headers,
2487
+ body: action.body,
2488
+ response: action.response,
2489
+ middleware: [],
2490
+ tags: [entity.name],
2491
+ description: `${action.name} on ${entity.name}`,
2492
+ sourceFile: action.sourceFile,
2493
+ sourceLine: action.sourceLine,
2494
+ sourceColumn: action.sourceColumn
2495
+ };
2496
+ });
2497
+ }
2498
+ function detectRouteCollisions(ir) {
2499
+ const diagnostics = [];
2500
+ const seen = new Map;
2501
+ for (const mod of ir.modules) {
2502
+ if (mod.name === SYNTHETIC_MODULE)
2503
+ continue;
2504
+ for (const router of mod.routers) {
2505
+ for (const route of router.routes) {
2506
+ seen.set(route.operationId, route);
2507
+ }
2508
+ }
2509
+ }
2510
+ const entityModule = ir.modules.find((m) => m.name === SYNTHETIC_MODULE);
2511
+ if (entityModule) {
2512
+ for (const router of entityModule.routers) {
2513
+ for (const route of router.routes) {
2514
+ const existing = seen.get(route.operationId);
2515
+ if (existing) {
2516
+ diagnostics.push({
2517
+ code: "ENTITY_ROUTE_COLLISION",
2518
+ severity: "error",
2519
+ message: `Entity-generated operationId "${route.operationId}" collides with existing route at ${existing.sourceFile}:${existing.sourceLine}`,
2520
+ ...route
2521
+ });
2522
+ }
2523
+ seen.set(route.operationId, route);
2524
+ }
2525
+ }
2526
+ }
2527
+ return diagnostics;
2528
+ }
2529
+
1873
2530
  // src/validators/completeness-validator.ts
1874
2531
  var METHODS_WITHOUT_RESPONSE = new Set(["DELETE", "HEAD", "OPTIONS"]);
1875
2532
  var RESERVED_CTX_KEYS = new Set([
@@ -2400,14 +3057,20 @@ class Compiler {
2400
3057
  const moduleResult = await analyzers.module.analyze();
2401
3058
  const middlewareResult = await analyzers.middleware.analyze();
2402
3059
  const appResult = await analyzers.app.analyze();
3060
+ const entityResult = await analyzers.entity.analyze();
2403
3061
  const depGraphResult = await analyzers.dependencyGraph.analyze();
2404
3062
  ir.env = envResult.env;
2405
3063
  ir.schemas = schemaResult.schemas;
2406
3064
  ir.modules = moduleResult.modules;
2407
3065
  ir.middleware = middlewareResult.middleware;
2408
3066
  ir.app = appResult.app;
3067
+ ir.entities = entityResult.entities;
2409
3068
  ir.dependencyGraph = depGraphResult.graph;
2410
- return ir;
3069
+ ir.diagnostics.push(...analyzers.env.getDiagnostics(), ...analyzers.schema.getDiagnostics(), ...analyzers.middleware.getDiagnostics(), ...analyzers.module.getDiagnostics(), ...analyzers.app.getDiagnostics(), ...analyzers.entity.getDiagnostics(), ...analyzers.dependencyGraph.getDiagnostics());
3070
+ injectEntityRoutes(ir);
3071
+ const collisionDiags = detectRouteCollisions(ir);
3072
+ ir.diagnostics.push(...collisionDiags);
3073
+ return enrichSchemasWithModuleNames(ir);
2411
3074
  }
2412
3075
  async validate(ir) {
2413
3076
  const allDiagnostics = [];
@@ -2419,6 +3082,7 @@ class Compiler {
2419
3082
  }
2420
3083
  async generate(ir) {
2421
3084
  const outputDir = this.config.compiler.outputDir;
3085
+ await mkdir(resolve(outputDir), { recursive: true });
2422
3086
  await Promise.all(this.deps.generators.map((g) => g.generate(ir, outputDir)));
2423
3087
  }
2424
3088
  async compile() {
@@ -2445,6 +3109,7 @@ function createCompiler(config) {
2445
3109
  middleware: new MiddlewareAnalyzer(project, resolved),
2446
3110
  module: new ModuleAnalyzer(project, resolved),
2447
3111
  app: new AppAnalyzer(project, resolved),
3112
+ entity: new EntityAnalyzer(project, resolved),
2448
3113
  dependencyGraph: new DependencyGraphAnalyzer(project, resolved)
2449
3114
  },
2450
3115
  validators: [
@@ -2656,7 +3321,7 @@ async function* typecheckWatch(options = {}) {
2656
3321
  const completionMarker = /Found \d+ error/;
2657
3322
  let buffer = "";
2658
3323
  const results = [];
2659
- let resolve = null;
3324
+ let resolve2 = null;
2660
3325
  let done = false;
2661
3326
  const onData = (chunk) => {
2662
3327
  buffer += chunk.toString();
@@ -2664,14 +3329,14 @@ async function* typecheckWatch(options = {}) {
2664
3329
  const result = parseWatchBlock(buffer);
2665
3330
  buffer = "";
2666
3331
  results.push(result);
2667
- resolve?.();
3332
+ resolve2?.();
2668
3333
  }
2669
3334
  };
2670
3335
  proc.stdout?.on("data", onData);
2671
3336
  proc.stderr?.on("data", onData);
2672
3337
  proc.on("close", () => {
2673
3338
  done = true;
2674
- resolve?.();
3339
+ resolve2?.();
2675
3340
  });
2676
3341
  try {
2677
3342
  while (!done || results.length > 0) {
@@ -2680,7 +3345,7 @@ async function* typecheckWatch(options = {}) {
2680
3345
  yield next;
2681
3346
  } else if (!done) {
2682
3347
  await new Promise((r) => {
2683
- resolve = r;
3348
+ resolve2 = r;
2684
3349
  });
2685
3350
  }
2686
3351
  }
@@ -2693,11 +3358,11 @@ async function typecheck(options = {}) {
2693
3358
  if (options.tsconfigPath) {
2694
3359
  args.push("--project", options.tsconfigPath);
2695
3360
  }
2696
- return new Promise((resolve) => {
3361
+ return new Promise((resolve2) => {
2697
3362
  execFile("tsc", args, { cwd: process.cwd() }, (error, stdout, stderr) => {
2698
3363
  const output = stdout + stderr;
2699
3364
  const diagnostics = parseTscOutput(output);
2700
- resolve({
3365
+ resolve2({
2701
3366
  success: !error,
2702
3367
  diagnostics
2703
3368
  });
@@ -2758,6 +3423,7 @@ export {
2758
3423
  isSchemaFile,
2759
3424
  isSchemaExpression,
2760
3425
  isFromImport,
3426
+ injectEntityRoutes,
2761
3427
  hasErrors,
2762
3428
  getVariableNameForCall,
2763
3429
  getStringValue,
@@ -2775,6 +3441,7 @@ export {
2775
3441
  extractObjectLiteral,
2776
3442
  extractMethodSignatures,
2777
3443
  extractIdentifierNames,
3444
+ detectRouteCollisions,
2778
3445
  defineConfig,
2779
3446
  createSchemaExecutor,
2780
3447
  createNamedSchemaRef,
@@ -2804,6 +3471,7 @@ export {
2804
3471
  ManifestGenerator,
2805
3472
  IncrementalCompiler,
2806
3473
  EnvAnalyzer,
3474
+ EntityAnalyzer,
2807
3475
  DependencyGraphAnalyzer,
2808
3476
  CompletenessValidator,
2809
3477
  Compiler,
package/package.json CHANGED
@@ -1,17 +1,14 @@
1
1
  {
2
2
  "name": "@vertz/compiler",
3
- "version": "0.1.0",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
+ "description": "Vertz compiler — internal, no stability guarantee",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "https://github.com/vertz-dev/vertz.git",
9
10
  "directory": "packages/compiler"
10
11
  },
11
- "publishConfig": {
12
- "access": "public",
13
- "provenance": true
14
- },
15
12
  "main": "dist/index.js",
16
13
  "types": "dist/index.d.ts",
17
14
  "exports": {
@@ -25,20 +22,26 @@
25
22
  ],
26
23
  "scripts": {
27
24
  "build": "bunup",
28
- "test": "vitest run",
25
+ "test": "bun test",
26
+ "test:type": "vitest typecheck",
29
27
  "test:watch": "vitest",
30
28
  "typecheck": "tsc --noEmit -p tsconfig.typecheck.json"
31
29
  },
32
30
  "dependencies": {
33
- "ts-morph": "^25.0.0"
31
+ "ts-morph": "^27.0.2"
34
32
  },
35
33
  "devDependencies": {
36
- "@types/node": "^25.2.1",
37
- "bunup": "latest",
34
+ "@types/node": "^25.3.1",
35
+ "@vitest/coverage-v8": "^4.0.18",
36
+ "bunup": "^0.16.31",
38
37
  "typescript": "^5.7.0",
39
- "vitest": "^3.0.0"
38
+ "vitest": "^4.0.18"
40
39
  },
41
40
  "engines": {
42
41
  "node": ">=22"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "provenance": true
43
46
  }
44
47
  }