micro-contracts 0.16.3 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/cli-contract.yaml +0 -32
  2. package/dist/micro-contracts.bundle.mjs +4682 -0
  3. package/dist/micro-contracts.bundle.mjs.map +7 -0
  4. package/docs/cli-reference.md +0 -28
  5. package/package.json +20 -23
  6. package/dist/agents/context-builder.d.ts +0 -5
  7. package/dist/agents/context-builder.d.ts.map +0 -1
  8. package/dist/agents/context-builder.js +0 -239
  9. package/dist/agents/context-builder.js.map +0 -1
  10. package/dist/agents/formatter.d.ts +0 -9
  11. package/dist/agents/formatter.d.ts.map +0 -1
  12. package/dist/agents/formatter.js +0 -124
  13. package/dist/agents/formatter.js.map +0 -1
  14. package/dist/agents/index.d.ts +0 -5
  15. package/dist/agents/index.d.ts.map +0 -1
  16. package/dist/agents/index.js +0 -3
  17. package/dist/agents/index.js.map +0 -1
  18. package/dist/agents/orchestrator.d.ts +0 -5
  19. package/dist/agents/orchestrator.d.ts.map +0 -1
  20. package/dist/agents/orchestrator.js +0 -105
  21. package/dist/agents/orchestrator.js.map +0 -1
  22. package/dist/agents/types.d.ts +0 -21
  23. package/dist/agents/types.d.ts.map +0 -1
  24. package/dist/agents/types.js +0 -2
  25. package/dist/agents/types.js.map +0 -1
  26. package/dist/cli/templates.d.ts +0 -22
  27. package/dist/cli/templates.d.ts.map +0 -1
  28. package/dist/cli/templates.js +0 -445
  29. package/dist/cli/templates.js.map +0 -1
  30. package/dist/cli.d.ts +0 -3
  31. package/dist/cli.d.ts.map +0 -1
  32. package/dist/cli.js +0 -1142
  33. package/dist/cli.js.map +0 -1
  34. package/dist/commands/audit-guardrails.d.ts +0 -13
  35. package/dist/commands/audit-guardrails.d.ts.map +0 -1
  36. package/dist/commands/audit-guardrails.js +0 -32
  37. package/dist/commands/audit-guardrails.js.map +0 -1
  38. package/dist/commands/audit-openapi.d.ts +0 -13
  39. package/dist/commands/audit-openapi.d.ts.map +0 -1
  40. package/dist/commands/audit-openapi.js +0 -32
  41. package/dist/commands/audit-openapi.js.map +0 -1
  42. package/dist/commands/propose-overlays.d.ts +0 -13
  43. package/dist/commands/propose-overlays.d.ts.map +0 -1
  44. package/dist/commands/propose-overlays.js +0 -32
  45. package/dist/commands/propose-overlays.js.map +0 -1
  46. package/dist/commands/review-published.d.ts +0 -13
  47. package/dist/commands/review-published.d.ts.map +0 -1
  48. package/dist/commands/review-published.js +0 -32
  49. package/dist/commands/review-published.js.map +0 -1
  50. package/dist/generated/commands.d.ts +0 -2
  51. package/dist/generated/commands.d.ts.map +0 -1
  52. package/dist/generated/commands.js +0 -2
  53. package/dist/generated/commands.js.map +0 -1
  54. package/dist/generated/dsl/agents.d.ts +0 -34
  55. package/dist/generated/dsl/agents.d.ts.map +0 -1
  56. package/dist/generated/dsl/agents.js +0 -98
  57. package/dist/generated/dsl/agents.js.map +0 -1
  58. package/dist/generated/dsl/handoffs.d.ts +0 -457
  59. package/dist/generated/dsl/handoffs.d.ts.map +0 -1
  60. package/dist/generated/dsl/handoffs.js +0 -205
  61. package/dist/generated/dsl/handoffs.js.map +0 -1
  62. package/dist/generated/dsl/index.d.ts +0 -13
  63. package/dist/generated/dsl/index.d.ts.map +0 -1
  64. package/dist/generated/dsl/index.js +0 -10
  65. package/dist/generated/dsl/index.js.map +0 -1
  66. package/dist/generated/dsl/tasks.d.ts +0 -26
  67. package/dist/generated/dsl/tasks.d.ts.map +0 -1
  68. package/dist/generated/dsl/tasks.js +0 -133
  69. package/dist/generated/dsl/tasks.js.map +0 -1
  70. package/dist/generated/dsl/workflows.d.ts +0 -34
  71. package/dist/generated/dsl/workflows.d.ts.map +0 -1
  72. package/dist/generated/dsl/workflows.js +0 -84
  73. package/dist/generated/dsl/workflows.js.map +0 -1
  74. package/dist/generated/index.d.ts +0 -7
  75. package/dist/generated/index.d.ts.map +0 -1
  76. package/dist/generated/index.js +0 -7
  77. package/dist/generated/index.js.map +0 -1
  78. package/dist/generated/policy-runtime.d.ts +0 -114
  79. package/dist/generated/policy-runtime.d.ts.map +0 -1
  80. package/dist/generated/policy-runtime.js +0 -195
  81. package/dist/generated/policy-runtime.js.map +0 -1
  82. package/dist/generated/policy.d.ts +0 -619
  83. package/dist/generated/policy.d.ts.map +0 -1
  84. package/dist/generated/policy.js +0 -802
  85. package/dist/generated/policy.js.map +0 -1
  86. package/dist/generated/program.d.ts +0 -112
  87. package/dist/generated/program.d.ts.map +0 -1
  88. package/dist/generated/program.js +0 -266
  89. package/dist/generated/program.js.map +0 -1
  90. package/dist/generated/schemas.d.ts +0 -778
  91. package/dist/generated/schemas.d.ts.map +0 -1
  92. package/dist/generated/schemas.js +0 -1005
  93. package/dist/generated/schemas.js.map +0 -1
  94. package/dist/generated/types.d.ts +0 -395
  95. package/dist/generated/types.d.ts.map +0 -1
  96. package/dist/generated/types.js +0 -3
  97. package/dist/generated/types.js.map +0 -1
  98. package/dist/generator/dependencyGenerator.d.ts +0 -43
  99. package/dist/generator/dependencyGenerator.d.ts.map +0 -1
  100. package/dist/generator/dependencyGenerator.js +0 -159
  101. package/dist/generator/dependencyGenerator.js.map +0 -1
  102. package/dist/generator/index.d.ts +0 -39
  103. package/dist/generator/index.d.ts.map +0 -1
  104. package/dist/generator/index.js +0 -720
  105. package/dist/generator/index.js.map +0 -1
  106. package/dist/generator/inputHash.d.ts +0 -23
  107. package/dist/generator/inputHash.d.ts.map +0 -1
  108. package/dist/generator/inputHash.js +0 -145
  109. package/dist/generator/inputHash.js.map +0 -1
  110. package/dist/generator/linter.d.ts +0 -26
  111. package/dist/generator/linter.d.ts.map +0 -1
  112. package/dist/generator/linter.js +0 -352
  113. package/dist/generator/linter.js.map +0 -1
  114. package/dist/generator/overlayProcessor.d.ts +0 -90
  115. package/dist/generator/overlayProcessor.d.ts.map +0 -1
  116. package/dist/generator/overlayProcessor.js +0 -541
  117. package/dist/generator/overlayProcessor.js.map +0 -1
  118. package/dist/generator/schemaGenerator.d.ts +0 -10
  119. package/dist/generator/schemaGenerator.d.ts.map +0 -1
  120. package/dist/generator/schemaGenerator.js +0 -299
  121. package/dist/generator/schemaGenerator.js.map +0 -1
  122. package/dist/generator/serviceGenerator.d.ts +0 -41
  123. package/dist/generator/serviceGenerator.d.ts.map +0 -1
  124. package/dist/generator/serviceGenerator.js +0 -296
  125. package/dist/generator/serviceGenerator.js.map +0 -1
  126. package/dist/generator/templateProcessor.d.ts +0 -245
  127. package/dist/generator/templateProcessor.d.ts.map +0 -1
  128. package/dist/generator/templateProcessor.js +0 -821
  129. package/dist/generator/templateProcessor.js.map +0 -1
  130. package/dist/generator/typeGenerator.d.ts +0 -9
  131. package/dist/generator/typeGenerator.d.ts.map +0 -1
  132. package/dist/generator/typeGenerator.js +0 -424
  133. package/dist/generator/typeGenerator.js.map +0 -1
  134. package/dist/generator/typeGenerator.test.d.ts +0 -2
  135. package/dist/generator/typeGenerator.test.d.ts.map +0 -1
  136. package/dist/generator/typeGenerator.test.js +0 -122
  137. package/dist/generator/typeGenerator.test.js.map +0 -1
  138. package/dist/guardrails/allowlist.d.ts +0 -45
  139. package/dist/guardrails/allowlist.d.ts.map +0 -1
  140. package/dist/guardrails/allowlist.js +0 -261
  141. package/dist/guardrails/allowlist.js.map +0 -1
  142. package/dist/guardrails/config.d.ts +0 -40
  143. package/dist/guardrails/config.d.ts.map +0 -1
  144. package/dist/guardrails/config.js +0 -174
  145. package/dist/guardrails/config.js.map +0 -1
  146. package/dist/guardrails/docs.d.ts +0 -24
  147. package/dist/guardrails/docs.d.ts.map +0 -1
  148. package/dist/guardrails/docs.js +0 -138
  149. package/dist/guardrails/docs.js.map +0 -1
  150. package/dist/guardrails/drift.d.ts +0 -23
  151. package/dist/guardrails/drift.d.ts.map +0 -1
  152. package/dist/guardrails/drift.js +0 -127
  153. package/dist/guardrails/drift.js.map +0 -1
  154. package/dist/guardrails/index.d.ts +0 -19
  155. package/dist/guardrails/index.d.ts.map +0 -1
  156. package/dist/guardrails/index.js +0 -23
  157. package/dist/guardrails/index.js.map +0 -1
  158. package/dist/guardrails/lint.d.ts +0 -20
  159. package/dist/guardrails/lint.d.ts.map +0 -1
  160. package/dist/guardrails/lint.js +0 -274
  161. package/dist/guardrails/lint.js.map +0 -1
  162. package/dist/guardrails/manifest.d.ts +0 -63
  163. package/dist/guardrails/manifest.d.ts.map +0 -1
  164. package/dist/guardrails/manifest.js +0 -299
  165. package/dist/guardrails/manifest.js.map +0 -1
  166. package/dist/guardrails/runner.d.ts +0 -43
  167. package/dist/guardrails/runner.d.ts.map +0 -1
  168. package/dist/guardrails/runner.js +0 -365
  169. package/dist/guardrails/runner.js.map +0 -1
  170. package/dist/guardrails/typecheck.d.ts +0 -15
  171. package/dist/guardrails/typecheck.d.ts.map +0 -1
  172. package/dist/guardrails/typecheck.js +0 -104
  173. package/dist/guardrails/typecheck.js.map +0 -1
  174. package/dist/guardrails/types.d.ts +0 -202
  175. package/dist/guardrails/types.d.ts.map +0 -1
  176. package/dist/guardrails/types.js +0 -8
  177. package/dist/guardrails/types.js.map +0 -1
  178. package/dist/index.d.ts.map +0 -1
  179. package/dist/index.js.map +0 -1
  180. package/dist/types.d.ts +0 -583
  181. package/dist/types.d.ts.map +0 -1
  182. package/dist/types.js +0 -305
  183. package/dist/types.js.map +0 -1
@@ -1,821 +0,0 @@
1
- /**
2
- * Template Processor
3
- *
4
- * Provides Handlebars template loading and processing for code generation.
5
- * Supports custom templates for server routes, frontend clients, etc.
6
- */
7
- import fs from 'fs';
8
- import path from 'path';
9
- import Handlebars from 'handlebars';
10
- import { isReference, getRefName } from '../types.js';
11
- // =============================================================================
12
- // Template Processor
13
- // =============================================================================
14
- /**
15
- * Load and compile a template
16
- */
17
- export function loadTemplate(templatePath) {
18
- if (!fs.existsSync(templatePath)) {
19
- throw new Error(`Template not found: ${templatePath}`);
20
- }
21
- const content = fs.readFileSync(templatePath, 'utf-8');
22
- return Handlebars.compile(content);
23
- }
24
- /**
25
- * Resolve template path following priority order:
26
- * 1. spec/{module}/templates/{templateName}
27
- * 2. spec/default/templates/{templateName}
28
- *
29
- * Returns null if not found (no built-in fallback)
30
- */
31
- export function resolveTemplatePath(options) {
32
- const { specDir, moduleName, templateName } = options;
33
- // Priority 1: Module-specific template
34
- const moduleTemplatePath = path.join(specDir, moduleName, 'templates', templateName);
35
- if (fs.existsSync(moduleTemplatePath)) {
36
- return moduleTemplatePath;
37
- }
38
- // Priority 2: Default templates
39
- const defaultTemplatePath = path.join(specDir, 'default', 'templates', templateName);
40
- if (fs.existsSync(defaultTemplatePath)) {
41
- return defaultTemplatePath;
42
- }
43
- // No built-in fallback
44
- return null;
45
- }
46
- /**
47
- * Load template with automatic resolution
48
- */
49
- export function loadTemplateWithResolution(options) {
50
- const templatePath = resolveTemplatePath(options);
51
- if (!templatePath) {
52
- // Try absolute/relative path as fallback
53
- const absolutePath = path.resolve(options.templateName);
54
- if (fs.existsSync(absolutePath)) {
55
- return loadTemplate(absolutePath);
56
- }
57
- throw new Error(`Template not found: ${options.templateName}\n` +
58
- `Searched in:\n` +
59
- ` - ${path.join(options.specDir, options.moduleName, 'templates', options.templateName)}\n` +
60
- ` - ${path.join(options.specDir, 'default', 'templates', options.templateName)}\n` +
61
- ` - ${absolutePath}\n` +
62
- `\nRun 'micro-contracts init' to create starter templates.`);
63
- }
64
- return loadTemplate(templatePath);
65
- }
66
- /**
67
- * Get default template content (built-in - for backward compatibility only)
68
- * @deprecated Use loadTemplateWithResolution instead
69
- */
70
- export function getDefaultTemplate(type) {
71
- switch (type) {
72
- case 'server':
73
- return DEFAULT_SERVER_TEMPLATE;
74
- case 'frontend':
75
- return DEFAULT_FRONTEND_TEMPLATE;
76
- case 'services':
77
- return DEFAULT_SERVICES_TEMPLATE;
78
- }
79
- }
80
- /**
81
- * Build template context from spec and overlay result
82
- */
83
- export function buildTemplateContext(spec, moduleName, options = {}) {
84
- const servicesPath = options.servicesPath || `fastify.services.${moduleName}`;
85
- const contractPackage = options.contractPackage || `@project/contract/${moduleName}`;
86
- const extensionInfo = options.extensionInfo
87
- ? Array.from(options.extensionInfo.values())
88
- : [];
89
- const appliedOverlays = options.appliedOverlays || [];
90
- const routes = extractRoutes(spec, extensionInfo);
91
- const services = extractServices(routes);
92
- const schemaNames = Object.keys(spec.components?.schemas || {});
93
- // Extract base URL from OpenAPI servers
94
- const baseUrl = extractBaseUrl(spec);
95
- // Extract service types for imports (e.g., 'UserServiceApi')
96
- const serviceTypes = services.map(s => `${s.name}ServiceApi`);
97
- // Extract schema types needed for imports
98
- const schemaTypes = extractSchemaTypes(routes);
99
- // Extract unique overlays with their parameters (deduplicated)
100
- const uniqueOverlays = extractUniqueOverlays(routes);
101
- // Extract screen contexts when screen mode is enabled
102
- const screens = options.screen ? extractScreens(spec) : [];
103
- return {
104
- moduleName,
105
- spec,
106
- title: spec.info.title,
107
- version: spec.info.version,
108
- baseUrl,
109
- extensionInfo,
110
- appliedOverlays,
111
- servicesPath,
112
- contractPackage,
113
- routes,
114
- services,
115
- serviceTypes,
116
- schemaTypes,
117
- schemaNames,
118
- uniqueOverlays,
119
- screens,
120
- };
121
- }
122
- /**
123
- * Extract unique overlays from routes (deduplicated by overlay name)
124
- */
125
- function extractUniqueOverlays(routes) {
126
- const overlayMap = new Map();
127
- for (const route of routes) {
128
- for (const ext of route.extensions) {
129
- if (!overlayMap.has(ext.value)) {
130
- // Find params for this overlay
131
- const params = route.extensionParams.filter(p => p.extensionName === ext.value);
132
- overlayMap.set(ext.value, params);
133
- }
134
- }
135
- }
136
- return Array.from(overlayMap.entries()).map(([name, params]) => ({ name, params }));
137
- }
138
- /**
139
- * Extract base URL from OpenAPI servers field
140
- * Returns path portion only (e.g., '/api' from 'http://localhost:3000/api')
141
- */
142
- function extractBaseUrl(spec) {
143
- const servers = spec.servers;
144
- if (!servers || servers.length === 0) {
145
- return '';
146
- }
147
- const serverUrl = servers[0].url;
148
- try {
149
- // Try to parse as full URL
150
- const url = new URL(serverUrl);
151
- // Return pathname, removing trailing slash
152
- return url.pathname.replace(/\/$/, '');
153
- }
154
- catch {
155
- // If not a valid URL, assume it's already a path
156
- return serverUrl.replace(/\/$/, '');
157
- }
158
- }
159
- /**
160
- * Extract all schema types needed for client imports
161
- */
162
- function extractSchemaTypes(routes) {
163
- const types = new Set();
164
- types.add('ProblemDetails'); // Always needed for error handling
165
- for (const route of routes) {
166
- if (route.requestBody?.schemaName) {
167
- types.add(route.requestBody.schemaName);
168
- }
169
- for (const resp of route.responses) {
170
- if (resp.schemaName) {
171
- types.add(resp.schemaName);
172
- }
173
- }
174
- // Add params type (path + query parameters) for templates that need it
175
- if (route.pathParams.length > 0 || route.queryParams.length > 0) {
176
- types.add(`${route.typeNameBase}Params`);
177
- }
178
- // Add unified input type (for service-aligned templates)
179
- types.add(`${route.typeNameBase}Input`);
180
- }
181
- return Array.from(types).sort();
182
- }
183
- /**
184
- * Process template with context
185
- */
186
- export function processTemplate(template, context) {
187
- return template(context);
188
- }
189
- /**
190
- * Generate code using template
191
- */
192
- export function generateWithTemplate(templatePath, defaultType, context) {
193
- let template;
194
- if (templatePath && fs.existsSync(templatePath)) {
195
- template = loadTemplate(templatePath);
196
- }
197
- else {
198
- template = Handlebars.compile(getDefaultTemplate(defaultType));
199
- }
200
- return processTemplate(template, context);
201
- }
202
- // =============================================================================
203
- // Route Extraction
204
- // =============================================================================
205
- function extractRoutes(spec, extensionInfo) {
206
- const routes = [];
207
- const methods = ['get', 'post', 'put', 'patch', 'delete'];
208
- for (const [apiPath, pathItem] of Object.entries(spec.paths)) {
209
- for (const method of methods) {
210
- const operation = pathItem[method];
211
- if (!operation)
212
- continue;
213
- // Canonical extension names only
214
- const service = operation['x-micro-contracts-service'];
215
- const serviceMethod = operation['x-micro-contracts-method'];
216
- if (!service || !serviceMethod)
217
- continue;
218
- const operationId = operation.operationId || `${method}${apiPath.replace(/[^a-zA-Z0-9]/g, '')}`;
219
- const typeNameBase = `${service}_${serviceMethod}`;
220
- const fastifyPath = apiPath.replace(/\{([^}]+)\}/g, ':$1');
221
- const serviceKey = service.charAt(0).toLowerCase() + service.slice(1);
222
- // Extract extensions and their parameters
223
- const extensions = [];
224
- const extensionParams = [];
225
- for (const info of extensionInfo) {
226
- const extValue = operation[info.marker];
227
- if (extValue) {
228
- const matches = Array.isArray(extValue)
229
- ? extValue.includes(info.name)
230
- : extValue === info.name;
231
- if (matches) {
232
- const registryName = markerToRegistryName(info.marker);
233
- extensions.push({
234
- marker: info.marker,
235
- value: info.name,
236
- registryKey: `${registryName}.${info.name}`,
237
- });
238
- // Add parameters injected by this extension
239
- for (const param of info.injectedParameters) {
240
- // Map OpenAPI 'in' values to HTTP request property names
241
- const locationMap = {
242
- header: 'headers',
243
- query: 'query',
244
- path: 'params',
245
- };
246
- extensionParams.push({
247
- extensionName: info.name,
248
- name: param.name,
249
- location: locationMap[param.in] || 'headers',
250
- tsType: parameterToTsType(param),
251
- required: param.required || false,
252
- });
253
- }
254
- }
255
- }
256
- }
257
- // Extract parameters
258
- const allParams = operation.parameters || [];
259
- const queryParams = allParams
260
- .filter((p) => !isReference(p) && p.in === 'query')
261
- .map(p => ({ name: p.name, required: p.required || false }));
262
- const pathParams = allParams
263
- .filter((p) => !isReference(p) && p.in === 'path')
264
- .map(p => ({ name: p.name, required: true }));
265
- // Extract request body
266
- let requestBody;
267
- if (operation.requestBody) {
268
- const reqBody = isReference(operation.requestBody)
269
- ? null // Simplified - would need to resolve
270
- : operation.requestBody;
271
- if (reqBody?.content?.['application/json']?.schema) {
272
- const schema = reqBody.content['application/json'].schema;
273
- const schemaName = isReference(schema)
274
- ? getRefName(schema.$ref)
275
- : operationId + 'Body';
276
- requestBody = { schemaName, required: reqBody.required || false };
277
- }
278
- }
279
- // Extract responses
280
- const responses = [];
281
- for (const [statusCode, response] of Object.entries(operation.responses)) {
282
- const resp = isReference(response) ? null : response;
283
- if (resp?.content?.['application/json']?.schema) {
284
- const schema = resp.content['application/json'].schema;
285
- const schemaName = isReference(schema) ? getRefName(schema.$ref) : undefined;
286
- responses.push({ statusCode, schemaName });
287
- }
288
- else {
289
- responses.push({ statusCode });
290
- }
291
- }
292
- // Client URL pattern: /users/{id} -> /users/${params.id}
293
- const clientUrlPattern = apiPath.replace(/\{([^}]+)\}/g, '${params.$1}');
294
- // Client URL pattern using input object: /users/{id} -> /users/${input.id}
295
- const clientUrlPatternInput = apiPath.replace(/\{([^}]+)\}/g, '${input.$1}');
296
- // Calculate type names
297
- const responseType = responses.length > 0 && responses[0].schemaName
298
- ? responses[0].schemaName
299
- : 'void';
300
- const requestType = requestBody?.schemaName;
301
- // Params type combines both path and query parameters
302
- const paramsType = (pathParams.length > 0 || queryParams.length > 0) ? `${typeNameBase}Params` : undefined;
303
- // Unified input type name (matches contract service API)
304
- const inputType = `${typeNameBase}Input`;
305
- routes.push({
306
- path: apiPath,
307
- fastifyPath,
308
- clientUrlPattern,
309
- clientUrlPatternInput,
310
- method,
311
- httpMethod: method.toUpperCase(),
312
- operationId,
313
- service,
314
- serviceKey,
315
- serviceMethod,
316
- summary: operation.summary,
317
- tags: operation.tags || [],
318
- isPublished: operation['x-micro-contracts-published'] === true,
319
- extensions,
320
- extensionParams,
321
- queryParams,
322
- pathParams,
323
- requestBody,
324
- responses,
325
- typeNameBase,
326
- responseType,
327
- requestType,
328
- paramsType,
329
- inputType,
330
- });
331
- }
332
- }
333
- return routes;
334
- }
335
- function extractServices(routes) {
336
- const serviceMap = new Map();
337
- for (const route of routes) {
338
- if (!serviceMap.has(route.service)) {
339
- serviceMap.set(route.service, {
340
- name: route.service,
341
- key: route.serviceKey,
342
- methods: [],
343
- });
344
- }
345
- const service = serviceMap.get(route.service);
346
- if (!service.methods.includes(route.serviceMethod)) {
347
- service.methods.push(route.serviceMethod);
348
- }
349
- }
350
- return Array.from(serviceMap.values()).sort((a, b) => a.name.localeCompare(b.name));
351
- }
352
- function markerToRegistryName(marker) {
353
- const name = marker.replace(/^x-/, '');
354
- return name.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) + 'Registry';
355
- }
356
- /**
357
- * Convert OpenAPI parameter to TypeScript type
358
- */
359
- function parameterToTsType(param) {
360
- if (!param.schema)
361
- return 'unknown';
362
- const schema = param.schema;
363
- switch (schema.type) {
364
- case 'string':
365
- return 'string';
366
- case 'integer':
367
- case 'number':
368
- return 'number';
369
- case 'boolean':
370
- return 'boolean';
371
- case 'array':
372
- return 'unknown[]';
373
- default:
374
- return 'unknown';
375
- }
376
- }
377
- // =============================================================================
378
- // Screen Extraction
379
- // =============================================================================
380
- /**
381
- * Build an operationId → route path lookup from spec paths
382
- */
383
- function buildOperationRouteMap(spec) {
384
- const map = new Map();
385
- for (const [apiPath, pathItem] of Object.entries(spec.paths)) {
386
- for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
387
- const operation = pathItem[method];
388
- if (operation?.operationId) {
389
- map.set(operation.operationId, apiPath);
390
- }
391
- }
392
- }
393
- return map;
394
- }
395
- /**
396
- * Extract {param} segments from a route path.
397
- */
398
- function extractPathParams(route) {
399
- const matches = route.matchAll(/\{(\w+)\}/g);
400
- return [...matches].map(m => m[1]);
401
- }
402
- /**
403
- * Resolve raw x-event (string | object | $ref) into InlineEventDefinition.
404
- */
405
- function resolveInlineEvent(raw, eventDefs, defaultType) {
406
- if (raw == null)
407
- return undefined;
408
- if (typeof raw === 'string') {
409
- return { name: raw, type: defaultType };
410
- }
411
- if (typeof raw === 'object' && '$ref' in raw && typeof raw.$ref === 'string') {
412
- const ref = raw.$ref;
413
- if (!ref.startsWith('#/components/x-event-defs/')) {
414
- return { name: ref, type: defaultType };
415
- }
416
- const defName = ref.split('/').pop();
417
- const resolved = eventDefs[defName];
418
- if (!resolved)
419
- return { name: defName, type: defaultType };
420
- return {
421
- name: resolved.name ?? defName,
422
- type: resolved.type ?? defaultType,
423
- params: resolved.params,
424
- };
425
- }
426
- return {
427
- name: raw.name ?? '',
428
- type: raw.type ?? defaultType,
429
- params: raw.params,
430
- };
431
- }
432
- /**
433
- * Auto-derive params from path parameters when no explicit params are set.
434
- * Only applies to 'get' (screen path params) and 'link' (target route params).
435
- */
436
- function deriveEventParams(event, placement, context) {
437
- if (event.params)
438
- return event;
439
- const route = placement === 'get' ? context.routePath : context.targetRoute;
440
- if (route) {
441
- const params = extractPathParams(route);
442
- if (params.length > 0) {
443
- event.params = Object.fromEntries(params.map(p => [p, 'string']));
444
- }
445
- }
446
- return event;
447
- }
448
- /**
449
- * Extract schema $ref name from a mutation operation's request body.
450
- */
451
- function extractSchemaRef(operation) {
452
- if (!operation.requestBody)
453
- return '';
454
- if (isReference(operation.requestBody)) {
455
- return getRefName(operation.requestBody.$ref);
456
- }
457
- const jsonContent = operation.requestBody.content?.['application/json'];
458
- if (jsonContent?.schema && isReference(jsonContent.schema)) {
459
- return getRefName(jsonContent.schema.$ref);
460
- }
461
- return '';
462
- }
463
- /**
464
- * Extract screen contexts from OpenAPI spec.
465
- * Parses GET operations that have x-screen-id into ScreenContext objects.
466
- */
467
- function extractScreens(spec) {
468
- const screens = [];
469
- const operationRouteMap = buildOperationRouteMap(spec);
470
- const components = (spec.components ?? {});
471
- const eventDefs = (components['x-event-defs'] ?? {});
472
- for (const [apiPath, pathItem] of Object.entries(spec.paths)) {
473
- const operation = pathItem.get;
474
- if (!operation)
475
- continue;
476
- const screenId = operation['x-screen-id'];
477
- if (!screenId)
478
- continue;
479
- const screenConst = operation['x-screen-const'] || '';
480
- const screenName = operation['x-screen-name'] || '';
481
- const operationId = operation.operationId || '';
482
- const supportsBack = operation['x-back-navigation'] === true;
483
- // Legacy x-events (backward compat)
484
- const legacyEvents = operation['x-events'] || [];
485
- // Inline x-event on GET (screen_view)
486
- let screenEvent = resolveInlineEvent(operation['x-event'], eventDefs, 'screen_view');
487
- if (screenEvent) {
488
- deriveEventParams(screenEvent, 'get', { routePath: apiPath });
489
- }
490
- const requiresAuth = Array.isArray(operation.security)
491
- ? operation.security.length > 0 && operation.security.some(s => Object.keys(s).length > 0)
492
- : false;
493
- let viewModelSchema = operation['x-view-model'] || '';
494
- if (!viewModelSchema) {
495
- const response200 = operation.responses?.['200'];
496
- if (response200 && !isReference(response200)) {
497
- const jsonContent = response200.content?.['application/json'];
498
- if (jsonContent?.schema && isReference(jsonContent.schema)) {
499
- viewModelSchema = getRefName(jsonContent.schema.$ref);
500
- }
501
- }
502
- }
503
- // Navigation links from 200 response (with optional inline x-event)
504
- const links = [];
505
- const response200 = operation.responses?.['200'];
506
- if (response200 && !isReference(response200)) {
507
- const responseLinks = response200.links;
508
- if (responseLinks) {
509
- for (const [linkName, linkObj] of Object.entries(responseLinks)) {
510
- if (linkObj.operationId) {
511
- const targetRoute = operationRouteMap.get(linkObj.operationId) || '';
512
- let linkEvent = resolveInlineEvent(linkObj['x-event'], eventDefs, 'user_action');
513
- if (linkEvent) {
514
- deriveEventParams(linkEvent, 'link', { routePath: apiPath, targetRoute });
515
- }
516
- links.push({
517
- name: linkName,
518
- targetRoute,
519
- targetOperationId: linkObj.operationId,
520
- event: linkEvent,
521
- });
522
- }
523
- }
524
- }
525
- }
526
- // Mutation operations (post/put/patch/delete)
527
- const actions = [];
528
- const mutationMethods = ['post', 'put', 'patch', 'delete'];
529
- for (const method of mutationMethods) {
530
- const mutationOp = pathItem[method];
531
- if (!mutationOp)
532
- continue;
533
- const actionEvent = resolveInlineEvent(mutationOp['x-event'], eventDefs, 'user_action');
534
- actions.push({
535
- method,
536
- operationId: mutationOp.operationId ?? '',
537
- summary: mutationOp.summary ?? '',
538
- schemaRef: extractSchemaRef(mutationOp),
539
- event: actionEvent,
540
- });
541
- }
542
- // x-interactions
543
- const rawInteractions = (operation['x-interactions'] ?? []);
544
- const interactions = rawInteractions.map(i => {
545
- const { name, description, 'x-event': rawEvent, ...extras } = i;
546
- return {
547
- name: name ?? '',
548
- description: description ?? '',
549
- event: resolveInlineEvent(rawEvent, eventDefs, 'user_action'),
550
- extras,
551
- };
552
- });
553
- screens.push({
554
- route: apiPath,
555
- screenConst,
556
- screenId,
557
- screenName,
558
- operationId,
559
- supportsBack,
560
- viewModelSchema,
561
- links,
562
- events: legacyEvents,
563
- screenEvent,
564
- actions,
565
- interactions,
566
- pathParams: extractPathParams(apiPath),
567
- requiresAuth,
568
- });
569
- }
570
- return screens;
571
- }
572
- // =============================================================================
573
- // Register Handlebars Helpers
574
- // =============================================================================
575
- // Comparison helpers
576
- Handlebars.registerHelper('eq', (a, b) => a === b);
577
- Handlebars.registerHelper('ne', (a, b) => a !== b);
578
- Handlebars.registerHelper('gt', (a, b) => a > b);
579
- Handlebars.registerHelper('gte', (a, b) => a >= b);
580
- Handlebars.registerHelper('lt', (a, b) => a < b);
581
- Handlebars.registerHelper('lte', (a, b) => a <= b);
582
- Handlebars.registerHelper('and', function (...args) {
583
- const options = args.pop();
584
- return args.every(Boolean);
585
- });
586
- Handlebars.registerHelper('or', function (...args) {
587
- const options = args.pop();
588
- return args.some(Boolean);
589
- });
590
- // Array/Object helpers
591
- Handlebars.registerHelper('length', (arr) => arr?.length || 0);
592
- Handlebars.registerHelper('first', (arr) => arr?.[0]);
593
- Handlebars.registerHelper('last', (arr) => arr?.[arr?.length - 1]);
594
- Handlebars.registerHelper('join', (arr, sep) => arr?.join(sep) || '');
595
- Handlebars.registerHelper('includes', (arr, val) => arr?.includes(val));
596
- Handlebars.registerHelper('keys', (obj) => Object.keys(obj || {}));
597
- Handlebars.registerHelper('values', (obj) => Object.values(obj || {}));
598
- // String helpers
599
- Handlebars.registerHelper('uppercase', (str) => str?.toUpperCase());
600
- Handlebars.registerHelper('lowercase', (str) => str?.toLowerCase());
601
- Handlebars.registerHelper('capitalize', (str) => str?.charAt(0).toUpperCase() + str?.slice(1));
602
- Handlebars.registerHelper('camelCase', (str) => {
603
- return str?.replace(/[-_]([a-z])/g, (_match, c) => c.toUpperCase());
604
- });
605
- Handlebars.registerHelper('pascalCase', (str) => {
606
- const camel = str?.replace(/[-_]([a-z])/g, (_match, c) => c.toUpperCase());
607
- return camel?.charAt(0).toUpperCase() + camel?.slice(1);
608
- });
609
- Handlebars.registerHelper('kebabCase', (str) => {
610
- return str?.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
611
- });
612
- Handlebars.registerHelper('snakeCase', (str) => {
613
- return str?.replace(/([a-z])([A-Z])/g, '$1_$2').replace(/-/g, '_').toLowerCase();
614
- });
615
- // Ref helper
616
- Handlebars.registerHelper('refName', (ref) => {
617
- return typeof ref === 'string' ? ref.split('/').pop() : '';
618
- });
619
- // JSON helper
620
- Handlebars.registerHelper('json', (obj) => JSON.stringify(obj, null, 2));
621
- // Screen event helpers
622
- Handlebars.registerHelper('hasEvent', (ctx) => !!(ctx?.screenEvent || ctx?.links?.some((l) => l.event) ||
623
- ctx?.actions?.some((a) => a.event) ||
624
- ctx?.interactions?.some((i) => i.event)));
625
- Handlebars.registerHelper('eventParamsSignature', (params) => {
626
- if (!params)
627
- return '';
628
- return Object.entries(params)
629
- .map(([k, v]) => `${k}: ${v === 'integer' ? 'number' : v}`)
630
- .join(', ');
631
- });
632
- // Conditional block helpers
633
- Handlebars.registerHelper('ifCond', function (v1, operator, v2, options) {
634
- switch (operator) {
635
- case '==': return v1 == v2 ? options.fn(this) : options.inverse(this);
636
- case '===': return v1 === v2 ? options.fn(this) : options.inverse(this);
637
- case '!=': return v1 != v2 ? options.fn(this) : options.inverse(this);
638
- case '!==': return v1 !== v2 ? options.fn(this) : options.inverse(this);
639
- case '<': return v1 < v2 ? options.fn(this) : options.inverse(this);
640
- case '<=': return v1 <= v2 ? options.fn(this) : options.inverse(this);
641
- case '>': return v1 > v2 ? options.fn(this) : options.inverse(this);
642
- case '>=': return v1 >= v2 ? options.fn(this) : options.inverse(this);
643
- default: return options.inverse(this);
644
- }
645
- });
646
- // =============================================================================
647
- // Default Templates
648
- // =============================================================================
649
- const DEFAULT_SERVER_TEMPLATE = `/**
650
- * Auto-generated Fastify routes from OpenAPI specification
651
- * Generated from: {{spec.info.title}} v{{spec.info.version}}
652
- * DO NOT EDIT MANUALLY
653
- */
654
-
655
- import type { FastifyInstance } from 'fastify';
656
- import { allSchemas } from '{{contractPackage}}/schemas';
657
- import * as types from '{{contractPackage}}/schemas';
658
-
659
- export async function registerRoutes(fastify: FastifyInstance): Promise<void> {
660
- // Register all schemas from contract package
661
- for (const schema of allSchemas) {
662
- fastify.addSchema(schema);
663
- }
664
-
665
- // Service references from {{servicesPath}}
666
- const { {{#each services}}{{key}}{{#unless @last}}, {{/unless}}{{/each}} } = {{servicesPath}};
667
- {{#if extensionInfo.length}}
668
-
669
- // Extension registries
670
- {{#each extensionInfo}}
671
- const {{camelCase marker}}Registry = fastify.{{camelCase marker}}Registry;
672
- {{/each}}
673
- {{/if}}
674
-
675
- {{#each routes}}
676
- // {{summary}}
677
- // {{uppercase method}} {{path}}{{#if isPublished}} (published){{/if}}
678
- fastify.{{method}}('{{fastifyPath}}', {
679
- {{#if queryParams.length}}
680
- schema: {
681
- querystring: { $ref: '{{typeNameBase}}Query#' },
682
- {{/if}}
683
- {{#if pathParams.length}}
684
- {{#unless queryParams.length}}
685
- schema: {
686
- {{/unless}}
687
- params: { $ref: '{{typeNameBase}}Params#' },
688
- {{/if}}
689
- {{#if requestBody}}
690
- {{#unless (or queryParams.length pathParams.length)}}
691
- schema: {
692
- {{/unless}}
693
- body: { $ref: '{{requestBody.schemaName}}#' },
694
- {{/if}}
695
- {{#if responses.length}}
696
- {{#unless (or queryParams.length pathParams.length requestBody)}}
697
- schema: {
698
- {{/unless}}
699
- response: {
700
- {{#each responses}}
701
- {{#if schemaName}}
702
- {{statusCode}}: { $ref: '{{schemaName}}#' },
703
- {{/if}}
704
- {{/each}}
705
- },
706
- {{/if}}
707
- {{#if (or queryParams.length pathParams.length requestBody responses.length)}}
708
- },
709
- {{/if}}
710
- {{#if extensions.length}}
711
- preHandler: [
712
- {{#each extensions}}
713
- {{registryKey}},
714
- {{/each}}
715
- ],
716
- {{/if}}
717
- }, async (req, reply) => {
718
- return {{serviceKey}}.{{serviceMethod}}({{#if pathParams.length}}req.params as types.{{typeNameBase}}Params{{/if}}{{#if queryParams.length}}{{#if pathParams.length}}, {{/if}}req.query as types.{{typeNameBase}}Query{{/if}}{{#if requestBody}}{{#if (or pathParams.length queryParams.length)}}, {{/if}}req.body as types.{{requestBody.schemaName}}{{/if}});
719
- });
720
-
721
- {{/each}}
722
- }
723
- `;
724
- const DEFAULT_FRONTEND_TEMPLATE = `/**
725
- * Auto-generated API client from OpenAPI specification
726
- * Generated from: {{spec.info.title}} v{{spec.info.version}}
727
- * DO NOT EDIT MANUALLY
728
- */
729
-
730
- import type * as types from '{{contractPackage}}/schemas';
731
-
732
- const BASE_URL = '';
733
-
734
- async function fetchApi<T>(
735
- url: string,
736
- options: RequestInit = {}
737
- ): Promise<T> {
738
- const response = await fetch(BASE_URL + url, {
739
- ...options,
740
- headers: {
741
- 'Content-Type': 'application/json',
742
- ...options.headers,
743
- },
744
- });
745
-
746
- if (!response.ok) {
747
- throw new Error(\`API error: \${response.status}\`);
748
- }
749
-
750
- if (response.status === 204) {
751
- return undefined as T;
752
- }
753
-
754
- return response.json();
755
- }
756
-
757
- {{#each routes}}
758
- /**
759
- * {{summary}}
760
- * {{uppercase method}} {{path}}
761
- */
762
- export async function {{operationId}}(
763
- {{#if pathParams.length}}
764
- params: types.{{typeNameBase}}Params,
765
- {{/if}}
766
- {{#if queryParams.length}}
767
- query?: Partial<types.{{typeNameBase}}Query>,
768
- {{/if}}
769
- {{#if requestBody}}
770
- body: types.{{requestBody.schemaName}},
771
- {{/if}}
772
- ): Promise<{{#with (first responses)}}{{#if schemaName}}types.{{schemaName}}{{else}}void{{/if}}{{/with}}> {
773
- {{#if pathParams.length}}
774
- let url = '{{path}}'.replace(/\\{([^}]+)\\}/g, (_, key) => String((params as Record<string, unknown>)[key]));
775
- {{else}}
776
- let url = '{{path}}';
777
- {{/if}}
778
- {{#if queryParams.length}}
779
- if (query) {
780
- const searchParams = new URLSearchParams();
781
- for (const [key, value] of Object.entries(query)) {
782
- if (value !== undefined) {
783
- searchParams.set(key, String(value));
784
- }
785
- }
786
- const qs = searchParams.toString();
787
- if (qs) url += '?' + qs;
788
- }
789
- {{/if}}
790
- return fetchApi<{{#with (first responses)}}{{#if schemaName}}types.{{schemaName}}{{else}}void{{/if}}{{/with}}>(url, {
791
- method: '{{uppercase method}}',
792
- {{#if requestBody}}
793
- body: JSON.stringify(body),
794
- {{/if}}
795
- });
796
- }
797
-
798
- {{/each}}
799
- `;
800
- const DEFAULT_SERVICES_TEMPLATE = `/**
801
- * Service interfaces
802
- * Auto-generated from OpenAPI specification
803
- * DO NOT EDIT MANUALLY
804
- */
805
-
806
- import type * as types from '../schemas/types.js';
807
-
808
- {{#each services}}
809
- /**
810
- * {{name}} interface
811
- */
812
- export interface {{name}}ServiceApi {
813
- {{#each methods}}
814
- {{this}}(...args: unknown[]): Promise<unknown>;
815
- {{/each}}
816
- }
817
-
818
- {{/each}}
819
- `;
820
- export { DEFAULT_SERVER_TEMPLATE, DEFAULT_FRONTEND_TEMPLATE, DEFAULT_SERVICES_TEMPLATE };
821
- //# sourceMappingURL=templateProcessor.js.map