micro-contracts 0.16.4 → 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 (181) hide show
  1. package/dist/micro-contracts.bundle.mjs +4682 -0
  2. package/dist/micro-contracts.bundle.mjs.map +7 -0
  3. package/package.json +20 -23
  4. package/dist/agents/context-builder.d.ts +0 -5
  5. package/dist/agents/context-builder.d.ts.map +0 -1
  6. package/dist/agents/context-builder.js +0 -239
  7. package/dist/agents/context-builder.js.map +0 -1
  8. package/dist/agents/formatter.d.ts +0 -9
  9. package/dist/agents/formatter.d.ts.map +0 -1
  10. package/dist/agents/formatter.js +0 -124
  11. package/dist/agents/formatter.js.map +0 -1
  12. package/dist/agents/index.d.ts +0 -5
  13. package/dist/agents/index.d.ts.map +0 -1
  14. package/dist/agents/index.js +0 -3
  15. package/dist/agents/index.js.map +0 -1
  16. package/dist/agents/orchestrator.d.ts +0 -5
  17. package/dist/agents/orchestrator.d.ts.map +0 -1
  18. package/dist/agents/orchestrator.js +0 -105
  19. package/dist/agents/orchestrator.js.map +0 -1
  20. package/dist/agents/types.d.ts +0 -21
  21. package/dist/agents/types.d.ts.map +0 -1
  22. package/dist/agents/types.js +0 -2
  23. package/dist/agents/types.js.map +0 -1
  24. package/dist/cli/templates.d.ts +0 -22
  25. package/dist/cli/templates.d.ts.map +0 -1
  26. package/dist/cli/templates.js +0 -445
  27. package/dist/cli/templates.js.map +0 -1
  28. package/dist/cli.d.ts +0 -3
  29. package/dist/cli.d.ts.map +0 -1
  30. package/dist/cli.js +0 -1142
  31. package/dist/cli.js.map +0 -1
  32. package/dist/commands/audit-guardrails.d.ts +0 -13
  33. package/dist/commands/audit-guardrails.d.ts.map +0 -1
  34. package/dist/commands/audit-guardrails.js +0 -32
  35. package/dist/commands/audit-guardrails.js.map +0 -1
  36. package/dist/commands/audit-openapi.d.ts +0 -13
  37. package/dist/commands/audit-openapi.d.ts.map +0 -1
  38. package/dist/commands/audit-openapi.js +0 -32
  39. package/dist/commands/audit-openapi.js.map +0 -1
  40. package/dist/commands/propose-overlays.d.ts +0 -13
  41. package/dist/commands/propose-overlays.d.ts.map +0 -1
  42. package/dist/commands/propose-overlays.js +0 -32
  43. package/dist/commands/propose-overlays.js.map +0 -1
  44. package/dist/commands/review-published.d.ts +0 -13
  45. package/dist/commands/review-published.d.ts.map +0 -1
  46. package/dist/commands/review-published.js +0 -32
  47. package/dist/commands/review-published.js.map +0 -1
  48. package/dist/generated/commands.d.ts +0 -2
  49. package/dist/generated/commands.d.ts.map +0 -1
  50. package/dist/generated/commands.js +0 -2
  51. package/dist/generated/commands.js.map +0 -1
  52. package/dist/generated/dsl/agents.d.ts +0 -34
  53. package/dist/generated/dsl/agents.d.ts.map +0 -1
  54. package/dist/generated/dsl/agents.js +0 -98
  55. package/dist/generated/dsl/agents.js.map +0 -1
  56. package/dist/generated/dsl/handoffs.d.ts +0 -457
  57. package/dist/generated/dsl/handoffs.d.ts.map +0 -1
  58. package/dist/generated/dsl/handoffs.js +0 -205
  59. package/dist/generated/dsl/handoffs.js.map +0 -1
  60. package/dist/generated/dsl/index.d.ts +0 -13
  61. package/dist/generated/dsl/index.d.ts.map +0 -1
  62. package/dist/generated/dsl/index.js +0 -10
  63. package/dist/generated/dsl/index.js.map +0 -1
  64. package/dist/generated/dsl/tasks.d.ts +0 -26
  65. package/dist/generated/dsl/tasks.d.ts.map +0 -1
  66. package/dist/generated/dsl/tasks.js +0 -133
  67. package/dist/generated/dsl/tasks.js.map +0 -1
  68. package/dist/generated/dsl/workflows.d.ts +0 -34
  69. package/dist/generated/dsl/workflows.d.ts.map +0 -1
  70. package/dist/generated/dsl/workflows.js +0 -84
  71. package/dist/generated/dsl/workflows.js.map +0 -1
  72. package/dist/generated/index.d.ts +0 -7
  73. package/dist/generated/index.d.ts.map +0 -1
  74. package/dist/generated/index.js +0 -7
  75. package/dist/generated/index.js.map +0 -1
  76. package/dist/generated/policy-runtime.d.ts +0 -114
  77. package/dist/generated/policy-runtime.d.ts.map +0 -1
  78. package/dist/generated/policy-runtime.js +0 -195
  79. package/dist/generated/policy-runtime.js.map +0 -1
  80. package/dist/generated/policy.d.ts +0 -619
  81. package/dist/generated/policy.d.ts.map +0 -1
  82. package/dist/generated/policy.js +0 -802
  83. package/dist/generated/policy.js.map +0 -1
  84. package/dist/generated/program.d.ts +0 -112
  85. package/dist/generated/program.d.ts.map +0 -1
  86. package/dist/generated/program.js +0 -266
  87. package/dist/generated/program.js.map +0 -1
  88. package/dist/generated/schemas.d.ts +0 -778
  89. package/dist/generated/schemas.d.ts.map +0 -1
  90. package/dist/generated/schemas.js +0 -1005
  91. package/dist/generated/schemas.js.map +0 -1
  92. package/dist/generated/types.d.ts +0 -395
  93. package/dist/generated/types.d.ts.map +0 -1
  94. package/dist/generated/types.js +0 -3
  95. package/dist/generated/types.js.map +0 -1
  96. package/dist/generator/dependencyGenerator.d.ts +0 -43
  97. package/dist/generator/dependencyGenerator.d.ts.map +0 -1
  98. package/dist/generator/dependencyGenerator.js +0 -159
  99. package/dist/generator/dependencyGenerator.js.map +0 -1
  100. package/dist/generator/index.d.ts +0 -39
  101. package/dist/generator/index.d.ts.map +0 -1
  102. package/dist/generator/index.js +0 -720
  103. package/dist/generator/index.js.map +0 -1
  104. package/dist/generator/inputHash.d.ts +0 -23
  105. package/dist/generator/inputHash.d.ts.map +0 -1
  106. package/dist/generator/inputHash.js +0 -145
  107. package/dist/generator/inputHash.js.map +0 -1
  108. package/dist/generator/linter.d.ts +0 -26
  109. package/dist/generator/linter.d.ts.map +0 -1
  110. package/dist/generator/linter.js +0 -352
  111. package/dist/generator/linter.js.map +0 -1
  112. package/dist/generator/overlayProcessor.d.ts +0 -90
  113. package/dist/generator/overlayProcessor.d.ts.map +0 -1
  114. package/dist/generator/overlayProcessor.js +0 -541
  115. package/dist/generator/overlayProcessor.js.map +0 -1
  116. package/dist/generator/schemaGenerator.d.ts +0 -10
  117. package/dist/generator/schemaGenerator.d.ts.map +0 -1
  118. package/dist/generator/schemaGenerator.js +0 -299
  119. package/dist/generator/schemaGenerator.js.map +0 -1
  120. package/dist/generator/serviceGenerator.d.ts +0 -41
  121. package/dist/generator/serviceGenerator.d.ts.map +0 -1
  122. package/dist/generator/serviceGenerator.js +0 -296
  123. package/dist/generator/serviceGenerator.js.map +0 -1
  124. package/dist/generator/templateProcessor.d.ts +0 -245
  125. package/dist/generator/templateProcessor.d.ts.map +0 -1
  126. package/dist/generator/templateProcessor.js +0 -821
  127. package/dist/generator/templateProcessor.js.map +0 -1
  128. package/dist/generator/typeGenerator.d.ts +0 -9
  129. package/dist/generator/typeGenerator.d.ts.map +0 -1
  130. package/dist/generator/typeGenerator.js +0 -424
  131. package/dist/generator/typeGenerator.js.map +0 -1
  132. package/dist/generator/typeGenerator.test.d.ts +0 -2
  133. package/dist/generator/typeGenerator.test.d.ts.map +0 -1
  134. package/dist/generator/typeGenerator.test.js +0 -122
  135. package/dist/generator/typeGenerator.test.js.map +0 -1
  136. package/dist/guardrails/allowlist.d.ts +0 -45
  137. package/dist/guardrails/allowlist.d.ts.map +0 -1
  138. package/dist/guardrails/allowlist.js +0 -261
  139. package/dist/guardrails/allowlist.js.map +0 -1
  140. package/dist/guardrails/config.d.ts +0 -40
  141. package/dist/guardrails/config.d.ts.map +0 -1
  142. package/dist/guardrails/config.js +0 -174
  143. package/dist/guardrails/config.js.map +0 -1
  144. package/dist/guardrails/docs.d.ts +0 -24
  145. package/dist/guardrails/docs.d.ts.map +0 -1
  146. package/dist/guardrails/docs.js +0 -138
  147. package/dist/guardrails/docs.js.map +0 -1
  148. package/dist/guardrails/drift.d.ts +0 -23
  149. package/dist/guardrails/drift.d.ts.map +0 -1
  150. package/dist/guardrails/drift.js +0 -127
  151. package/dist/guardrails/drift.js.map +0 -1
  152. package/dist/guardrails/index.d.ts +0 -19
  153. package/dist/guardrails/index.d.ts.map +0 -1
  154. package/dist/guardrails/index.js +0 -23
  155. package/dist/guardrails/index.js.map +0 -1
  156. package/dist/guardrails/lint.d.ts +0 -20
  157. package/dist/guardrails/lint.d.ts.map +0 -1
  158. package/dist/guardrails/lint.js +0 -274
  159. package/dist/guardrails/lint.js.map +0 -1
  160. package/dist/guardrails/manifest.d.ts +0 -63
  161. package/dist/guardrails/manifest.d.ts.map +0 -1
  162. package/dist/guardrails/manifest.js +0 -299
  163. package/dist/guardrails/manifest.js.map +0 -1
  164. package/dist/guardrails/runner.d.ts +0 -43
  165. package/dist/guardrails/runner.d.ts.map +0 -1
  166. package/dist/guardrails/runner.js +0 -365
  167. package/dist/guardrails/runner.js.map +0 -1
  168. package/dist/guardrails/typecheck.d.ts +0 -15
  169. package/dist/guardrails/typecheck.d.ts.map +0 -1
  170. package/dist/guardrails/typecheck.js +0 -104
  171. package/dist/guardrails/typecheck.js.map +0 -1
  172. package/dist/guardrails/types.d.ts +0 -202
  173. package/dist/guardrails/types.d.ts.map +0 -1
  174. package/dist/guardrails/types.js +0 -8
  175. package/dist/guardrails/types.js.map +0 -1
  176. package/dist/index.d.ts.map +0 -1
  177. package/dist/index.js.map +0 -1
  178. package/dist/types.d.ts +0 -583
  179. package/dist/types.d.ts.map +0 -1
  180. package/dist/types.js +0 -305
  181. 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