micro-contracts 0.9.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 (99) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +351 -0
  3. package/dist/cli/templates.d.ts +16 -0
  4. package/dist/cli/templates.d.ts.map +1 -0
  5. package/dist/cli/templates.js +377 -0
  6. package/dist/cli/templates.js.map +1 -0
  7. package/dist/cli.d.ts +9 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +978 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/generator/dependencyGenerator.d.ts +43 -0
  12. package/dist/generator/dependencyGenerator.d.ts.map +1 -0
  13. package/dist/generator/dependencyGenerator.js +159 -0
  14. package/dist/generator/dependencyGenerator.js.map +1 -0
  15. package/dist/generator/domainGenerator.d.ts +16 -0
  16. package/dist/generator/domainGenerator.d.ts.map +1 -0
  17. package/dist/generator/domainGenerator.js +212 -0
  18. package/dist/generator/domainGenerator.js.map +1 -0
  19. package/dist/generator/index.d.ts +37 -0
  20. package/dist/generator/index.d.ts.map +1 -0
  21. package/dist/generator/index.js +747 -0
  22. package/dist/generator/index.js.map +1 -0
  23. package/dist/generator/linter.d.ts +24 -0
  24. package/dist/generator/linter.d.ts.map +1 -0
  25. package/dist/generator/linter.js +202 -0
  26. package/dist/generator/linter.js.map +1 -0
  27. package/dist/generator/overlayProcessor.d.ts +90 -0
  28. package/dist/generator/overlayProcessor.d.ts.map +1 -0
  29. package/dist/generator/overlayProcessor.js +532 -0
  30. package/dist/generator/overlayProcessor.js.map +1 -0
  31. package/dist/generator/schemaGenerator.d.ts +10 -0
  32. package/dist/generator/schemaGenerator.d.ts.map +1 -0
  33. package/dist/generator/schemaGenerator.js +299 -0
  34. package/dist/generator/schemaGenerator.js.map +1 -0
  35. package/dist/generator/templateProcessor.d.ts +178 -0
  36. package/dist/generator/templateProcessor.d.ts.map +1 -0
  37. package/dist/generator/templateProcessor.js +607 -0
  38. package/dist/generator/templateProcessor.js.map +1 -0
  39. package/dist/generator/typeGenerator.d.ts +9 -0
  40. package/dist/generator/typeGenerator.d.ts.map +1 -0
  41. package/dist/generator/typeGenerator.js +395 -0
  42. package/dist/generator/typeGenerator.js.map +1 -0
  43. package/dist/guardrails/allowlist.d.ts +45 -0
  44. package/dist/guardrails/allowlist.d.ts.map +1 -0
  45. package/dist/guardrails/allowlist.js +261 -0
  46. package/dist/guardrails/allowlist.js.map +1 -0
  47. package/dist/guardrails/config.d.ts +40 -0
  48. package/dist/guardrails/config.d.ts.map +1 -0
  49. package/dist/guardrails/config.js +174 -0
  50. package/dist/guardrails/config.js.map +1 -0
  51. package/dist/guardrails/docs.d.ts +24 -0
  52. package/dist/guardrails/docs.d.ts.map +1 -0
  53. package/dist/guardrails/docs.js +138 -0
  54. package/dist/guardrails/docs.js.map +1 -0
  55. package/dist/guardrails/drift.d.ts +23 -0
  56. package/dist/guardrails/drift.d.ts.map +1 -0
  57. package/dist/guardrails/drift.js +127 -0
  58. package/dist/guardrails/drift.js.map +1 -0
  59. package/dist/guardrails/index.d.ts +19 -0
  60. package/dist/guardrails/index.d.ts.map +1 -0
  61. package/dist/guardrails/index.js +25 -0
  62. package/dist/guardrails/index.js.map +1 -0
  63. package/dist/guardrails/lint.d.ts +20 -0
  64. package/dist/guardrails/lint.d.ts.map +1 -0
  65. package/dist/guardrails/lint.js +274 -0
  66. package/dist/guardrails/lint.js.map +1 -0
  67. package/dist/guardrails/manifest.d.ts +43 -0
  68. package/dist/guardrails/manifest.d.ts.map +1 -0
  69. package/dist/guardrails/manifest.js +231 -0
  70. package/dist/guardrails/manifest.js.map +1 -0
  71. package/dist/guardrails/runner.d.ts +31 -0
  72. package/dist/guardrails/runner.d.ts.map +1 -0
  73. package/dist/guardrails/runner.js +268 -0
  74. package/dist/guardrails/runner.js.map +1 -0
  75. package/dist/guardrails/security.d.ts +31 -0
  76. package/dist/guardrails/security.d.ts.map +1 -0
  77. package/dist/guardrails/security.js +181 -0
  78. package/dist/guardrails/security.js.map +1 -0
  79. package/dist/guardrails/typecheck.d.ts +15 -0
  80. package/dist/guardrails/typecheck.d.ts.map +1 -0
  81. package/dist/guardrails/typecheck.js +104 -0
  82. package/dist/guardrails/typecheck.js.map +1 -0
  83. package/dist/guardrails/types.d.ts +196 -0
  84. package/dist/guardrails/types.d.ts.map +1 -0
  85. package/dist/guardrails/types.js +8 -0
  86. package/dist/guardrails/types.js.map +1 -0
  87. package/dist/index.d.ts +7 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +7 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/types.d.ts +489 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +297 -0
  94. package/dist/types.js.map +1 -0
  95. package/docs/architecture.svg +226 -0
  96. package/docs/development-guardrails.md +541 -0
  97. package/docs/guardrails-concept.svg +252 -0
  98. package/docs/overlays-deep-dive.md +298 -0
  99. package/package.json +66 -0
@@ -0,0 +1,747 @@
1
+ /**
2
+ * micro-contracts Generator
3
+ */
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import yaml from 'js-yaml';
7
+ import { isMultiModuleConfig, resolveModuleConfig, } from '../types.js';
8
+ import { generateTypes } from './typeGenerator.js';
9
+ import { generateSchemas } from './schemaGenerator.js';
10
+ import { generateDomainInterfaces } from './domainGenerator.js';
11
+ import { lintSpec, formatLintResults } from './linter.js';
12
+ import { processOverlays, generateExtensionInterfaces, formatOverlayLog, rebaseRefs, } from './overlayProcessor.js';
13
+ import { buildTemplateContext, generateWithTemplate, loadTemplateWithResolution, resolveTemplatePath, } from './templateProcessor.js';
14
+ import { validateDependsOn, } from './dependencyGenerator.js';
15
+ import { extractDependencies } from '../types.js';
16
+ export { generateTypes } from './typeGenerator.js';
17
+ export { generateSchemas } from './schemaGenerator.js';
18
+ export { generateDomainInterfaces } from './domainGenerator.js';
19
+ export { lintSpec, formatLintResults } from './linter.js';
20
+ export { processOverlays, generateExtensionInterfaces } from './overlayProcessor.js';
21
+ export { buildTemplateContext, generateWithTemplate } from './templateProcessor.js';
22
+ /**
23
+ * Write file only if content has changed (ignoring timestamp in header).
24
+ * This prevents unnecessary git diffs when only the timestamp changes.
25
+ * Returns true if file was written, false if content was unchanged.
26
+ */
27
+ function writeFileIfChanged(filePath, newContent) {
28
+ // Resolve to absolute path for consistency
29
+ const absolutePath = path.resolve(filePath);
30
+ if (fs.existsSync(absolutePath)) {
31
+ const existingContent = fs.readFileSync(absolutePath, 'utf-8');
32
+ if (existingContent === newContent) {
33
+ return false; // No change, skip writing
34
+ }
35
+ }
36
+ fs.writeFileSync(absolutePath, newContent);
37
+ return true;
38
+ }
39
+ /**
40
+ * Write file and log result. Uses writeFileIfChanged to avoid unnecessary updates.
41
+ */
42
+ function writeAndLog(filePath, content, indent = ' ') {
43
+ const written = writeFileIfChanged(filePath, content);
44
+ if (written) {
45
+ console.log(`${indent}Written: ${filePath}`);
46
+ }
47
+ else {
48
+ console.log(`${indent}Unchanged: ${filePath}`);
49
+ }
50
+ }
51
+ /**
52
+ * Load OpenAPI spec from file
53
+ */
54
+ export function loadOpenAPISpec(filePath) {
55
+ const content = fs.readFileSync(filePath, 'utf-8');
56
+ if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) {
57
+ return yaml.load(content);
58
+ }
59
+ else if (filePath.endsWith('.json')) {
60
+ return JSON.parse(content);
61
+ }
62
+ else {
63
+ throw new Error(`Unsupported file format: ${filePath}`);
64
+ }
65
+ }
66
+ /**
67
+ * Load config from file (supports both legacy and multi-module formats)
68
+ */
69
+ export function loadConfig(configPath) {
70
+ const content = fs.readFileSync(configPath, 'utf-8');
71
+ return yaml.load(content);
72
+ }
73
+ /**
74
+ * Parse module filter from options
75
+ */
76
+ function parseModuleFilter(modules) {
77
+ if (!modules)
78
+ return null;
79
+ if (Array.isArray(modules))
80
+ return modules;
81
+ return modules.split(',').map(m => m.trim()).filter(Boolean);
82
+ }
83
+ /**
84
+ * Generate all files from config
85
+ */
86
+ export async function generate(config, options = {}) {
87
+ // Handle multi-module config
88
+ if (isMultiModuleConfig(config)) {
89
+ await generateMultiModule(config, options);
90
+ return;
91
+ }
92
+ // Legacy single-module config is no longer supported
93
+ throw new Error('Legacy single-module configuration format is no longer supported. ' +
94
+ 'Please migrate to the multi-module format with a "modules:" section. ' +
95
+ 'See README.md for configuration examples.');
96
+ }
97
+ /**
98
+ * Generate for multi-module config
99
+ */
100
+ async function generateMultiModule(config, options) {
101
+ const moduleFilter = parseModuleFilter(options.modules);
102
+ const moduleNames = Object.keys(config.modules);
103
+ // Filter modules if specified
104
+ const targetModules = moduleFilter
105
+ ? moduleNames.filter(m => moduleFilter.includes(m))
106
+ : moduleNames;
107
+ if (targetModules.length === 0) {
108
+ if (moduleFilter) {
109
+ console.error(`No matching modules found. Available: ${moduleNames.join(', ')}`);
110
+ process.exit(1);
111
+ }
112
+ console.log('No modules defined in config.');
113
+ return;
114
+ }
115
+ console.log(`Generating for modules: ${targetModules.join(', ')}`);
116
+ // Generate each module
117
+ for (const moduleName of targetModules) {
118
+ const moduleConfig = config.modules[moduleName];
119
+ const resolved = resolveModuleConfig(moduleName, moduleConfig, config.defaults);
120
+ console.log(`\n${'='.repeat(60)}`);
121
+ console.log(`Module: ${moduleName}`);
122
+ console.log(`${'='.repeat(60)}`);
123
+ await generateModule(resolved, options);
124
+ }
125
+ console.log('\nGeneration complete!');
126
+ }
127
+ /**
128
+ * Generate for a single resolved module
129
+ */
130
+ async function generateModule(config, options) {
131
+ // Load OpenAPI spec
132
+ const openapiPath = path.resolve(config.openapi);
133
+ console.log(`Loading OpenAPI spec from: ${openapiPath}`);
134
+ if (!fs.existsSync(openapiPath)) {
135
+ throw new Error(`OpenAPI spec not found: ${openapiPath}`);
136
+ }
137
+ let spec = loadOpenAPISpec(openapiPath);
138
+ console.log(` Title: ${spec.info.title}`);
139
+ console.log(` Version: ${spec.info.version}`);
140
+ // Run linting first (unless skipped)
141
+ if (!options.skipLint) {
142
+ console.log('\nLinting OpenAPI spec...');
143
+ const lintResult = lintSpec(spec);
144
+ console.log(formatLintResults(lintResult));
145
+ if (!lintResult.valid) {
146
+ throw new Error('Lint failed. Fix errors before generating.');
147
+ }
148
+ }
149
+ // Apply overlays if configured
150
+ let overlayResult = null;
151
+ if (config.overlays.length > 0) {
152
+ console.log('\nApplying overlays...');
153
+ overlayResult = processOverlays(spec, {
154
+ collision: config.overlayCollision,
155
+ files: config.overlays,
156
+ }, process.cwd(), openapiPath // Pass spec path for $ref rebasing
157
+ );
158
+ spec = overlayResult.spec;
159
+ console.log(formatOverlayLog(overlayResult));
160
+ // Note: Transformed spec is written to packages/contract/*/docs/openapi.generated.yaml
161
+ }
162
+ const generateAll = !options.contractsOnly && !options.serverOnly &&
163
+ !options.frontendOnly && !options.docsOnly;
164
+ // Validate and generate dependencies
165
+ const dependencies = extractDependencies(spec);
166
+ if (config.dependsOn) {
167
+ const validation = validateDependsOn(dependencies.allDeps.map(d => d.raw), config.dependsOn, config.name);
168
+ if (!validation.valid) {
169
+ for (const err of validation.errors) {
170
+ console.error(`ERROR: ${err}`);
171
+ }
172
+ throw new Error('Dependency validation failed');
173
+ }
174
+ }
175
+ // Generate contract package
176
+ if (generateAll || options.contractsOnly) {
177
+ await generateContractPackage(spec, config, false, overlayResult);
178
+ // Generate public contract if there are public endpoints
179
+ if (hasPublicEndpoints(spec)) {
180
+ await generateContractPackage(spec, config, true, overlayResult);
181
+ }
182
+ // Generate deps/ re-exports if module has dependencies
183
+ if (dependencies.allDeps.length > 0) {
184
+ await generateDepsReExports(config, dependencies);
185
+ }
186
+ }
187
+ // Generate using new outputs system if configured
188
+ if (config.outputs.length > 0) {
189
+ await generateFromOutputs(spec, config, overlayResult, options);
190
+ }
191
+ else {
192
+ // Fallback to legacy server/frontend generation
193
+ // Generate server routes
194
+ if ((generateAll || options.serverOnly) && config.server) {
195
+ await generateServerRoutes(spec, config, overlayResult);
196
+ }
197
+ // Generate frontend clients
198
+ if ((generateAll || options.frontendOnly) && config.frontend) {
199
+ await generateFrontendClient(spec, config, overlayResult);
200
+ }
201
+ }
202
+ // Generate documentation
203
+ if ((generateAll || options.docsOnly) && config.docs.enabled) {
204
+ const docsDir = path.join(config.contractOutput, 'docs');
205
+ const openapiFile = path.join(docsDir, 'openapi.generated.yaml');
206
+ await generateDocumentation(openapiFile, docsDir);
207
+ // Also generate for public contract if exists
208
+ if (hasPublicEndpoints(spec)) {
209
+ const publicDocsDir = path.join(config.contractPublicOutput, 'docs');
210
+ const publicOpenapiFile = path.join(publicDocsDir, 'openapi.generated.yaml');
211
+ if (fs.existsSync(publicOpenapiFile)) {
212
+ await generateDocumentation(publicOpenapiFile, publicDocsDir);
213
+ }
214
+ }
215
+ }
216
+ }
217
+ /**
218
+ * Check if spec has any public endpoints
219
+ */
220
+ function hasPublicEndpoints(spec) {
221
+ for (const pathItem of Object.values(spec.paths)) {
222
+ for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
223
+ const operation = pathItem[method];
224
+ if (operation && operation['x-micro-contracts-published'] === true) {
225
+ return true;
226
+ }
227
+ }
228
+ }
229
+ return false;
230
+ }
231
+ /**
232
+ * Generate contract package
233
+ */
234
+ async function generateContractPackage(spec, config, publicOnly, overlayResult = null) {
235
+ const outputDir = publicOnly ? config.contractPublicOutput : config.contractOutput;
236
+ const label = publicOnly ? 'public contract' : 'contract';
237
+ console.log(`\nGenerating ${label} package...`);
238
+ // For public contract, use filtered spec
239
+ const targetSpec = publicOnly ? filterPublicSpec(spec) : spec;
240
+ // Create directories
241
+ const dirs = [
242
+ outputDir,
243
+ path.join(outputDir, 'domains'),
244
+ path.join(outputDir, 'schemas'),
245
+ path.join(outputDir, 'errors'),
246
+ path.join(outputDir, 'docs'),
247
+ ];
248
+ for (const dir of dirs) {
249
+ fs.mkdirSync(dir, { recursive: true });
250
+ }
251
+ // Note: We no longer delete files before generating to enable change detection.
252
+ // Orphaned files from removed domains/schemas should be manually cleaned up.
253
+ // Generate domain interfaces
254
+ console.log(` Generating domain interfaces...`);
255
+ const domainInterfaces = generateDomainInterfaces(targetSpec, { publicOnly });
256
+ for (const [name, content] of domainInterfaces) {
257
+ const fileName = name === 'index' ? 'index.ts' : `${name}Api.ts`;
258
+ const filePath = path.join(outputDir, 'domains', fileName);
259
+ writeAndLog(filePath, content);
260
+ }
261
+ // Generate types (use filtered spec for public)
262
+ console.log(` Generating schema types...`);
263
+ const typesContent = generateTypes(targetSpec);
264
+ const typesPath = path.join(outputDir, 'schemas', 'types.ts');
265
+ writeAndLog(typesPath, typesContent);
266
+ // Generate validators (JSON Schemas) - use filtered spec for public
267
+ console.log(` Generating validators...`);
268
+ const validatorsContent = generateSchemas(targetSpec);
269
+ const validatorsPath = path.join(outputDir, 'schemas', 'validators.ts');
270
+ writeAndLog(validatorsPath, validatorsContent);
271
+ // Generate schemas index
272
+ const schemasIndex = `/**
273
+ * Schema exports
274
+ * Auto-generated - DO NOT EDIT
275
+ */
276
+
277
+ export * from './types.js';
278
+ export { allSchemas } from './validators.js';
279
+ `;
280
+ writeAndLog(path.join(outputDir, 'schemas', 'index.ts'), schemasIndex);
281
+ // Generate errors
282
+ const hasEndpoints = Object.keys(targetSpec.paths).length > 0;
283
+ console.log(` Generating error types...`);
284
+ const errorsContent = hasEndpoints ? generateErrors() : generateEmptyErrors();
285
+ writeAndLog(path.join(outputDir, 'errors', 'index.ts'), errorsContent);
286
+ // Generate overlay interfaces if overlays were applied
287
+ if (overlayResult && overlayResult.extensionInfo.size > 0 && !publicOnly) {
288
+ console.log(` Generating overlay interfaces...`);
289
+ const overlaysDir = path.join(outputDir, 'overlays');
290
+ fs.mkdirSync(overlaysDir, { recursive: true });
291
+ const overlayContent = generateExtensionInterfaces(overlayResult.extensionInfo);
292
+ const overlayPath = path.join(overlaysDir, 'index.ts');
293
+ writeAndLog(overlayPath, overlayContent);
294
+ }
295
+ // Generate package index
296
+ const hasOverlays = overlayResult && overlayResult.extensionInfo.size > 0 && !publicOnly;
297
+ const indexContent = `/**
298
+ * ${publicOnly ? 'Public ' : ''}Contract Package
299
+ * Auto-generated - DO NOT EDIT
300
+ */
301
+
302
+ export * from './domains/index.js';
303
+ export * from './schemas/index.js';
304
+ export * from './errors/index.js';
305
+ ${hasOverlays ? "export * from './overlays/index.js';" : ''}
306
+ `;
307
+ writeAndLog(path.join(outputDir, 'index.ts'), indexContent);
308
+ // Copy OpenAPI spec to docs with source info header
309
+ // Rebase $ref paths from source directory to output directory
310
+ const sourceDir = path.dirname(config.openapi);
311
+ const docsDir = path.join(outputDir, 'docs');
312
+ const rebasedSpec = rebaseRefs(targetSpec, sourceDir, docsDir);
313
+ const specHeader = `# Auto-generated OpenAPI specification
314
+ # DO NOT EDIT MANUALLY
315
+ #
316
+ # Source: ${config.openapi}
317
+ # Regenerate: micro-contracts generate
318
+ ${publicOnly ? '# Filtered for public endpoints only\n' : ''}
319
+ `;
320
+ const yamlContent = specHeader + yaml.dump(rebasedSpec, { lineWidth: -1 });
321
+ writeAndLog(path.join(docsDir, 'openapi.generated.yaml'), yamlContent);
322
+ }
323
+ /**
324
+ * Generate using flexible outputs configuration
325
+ */
326
+ async function generateFromOutputs(spec, config, overlayResult, options) {
327
+ const generateAll = !options.contractsOnly && !options.serverOnly &&
328
+ !options.frontendOnly && !options.docsOnly;
329
+ console.log(`\nGenerating from outputs configuration...`);
330
+ const hasPublic = hasPublicEndpoints(spec);
331
+ for (const output of config.outputs) {
332
+ // Skip disabled outputs
333
+ if (!output.enabled)
334
+ continue;
335
+ // Check conditions
336
+ if (output.condition === 'hasPublicEndpoints' && !hasPublic) {
337
+ console.log(` Skipping ${output.id} (no public endpoints)`);
338
+ continue;
339
+ }
340
+ const hasOverlays = overlayResult && overlayResult.extensionInfo.size > 0;
341
+ if (output.condition === 'hasOverlays' && !hasOverlays) {
342
+ console.log(` Skipping ${output.id} (no overlays)`);
343
+ continue;
344
+ }
345
+ // Filter by generation type
346
+ const isServerOutput = output.id.includes('server');
347
+ const isFrontendOutput = output.id.includes('frontend') || output.id.includes('client');
348
+ if (options.serverOnly && !isServerOutput)
349
+ continue;
350
+ if (options.frontendOnly && !isFrontendOutput)
351
+ continue;
352
+ if (!generateAll && !options.serverOnly && !options.frontendOnly)
353
+ continue;
354
+ // Check if file exists and overwrite is disabled
355
+ if (!output.overwrite && fs.existsSync(output.output)) {
356
+ console.log(` Skipping ${output.id} (file exists, overwrite=false)`);
357
+ continue;
358
+ }
359
+ console.log(` Generating ${output.id}...`);
360
+ try {
361
+ // Build template context with output-specific config
362
+ // Expand {module} placeholders in config values
363
+ const expandPlaceholder = (val, fallback) => (val?.replace(/{module}/g, config.name) ?? fallback);
364
+ const templateContext = buildTemplateContext(spec, config.name, {
365
+ domainsPath: expandPlaceholder(output.config?.domainsPath, `fastify.domains.${config.name}`),
366
+ contractPackage: expandPlaceholder(output.config?.contractPackage, `@project/contract/${config.name}`),
367
+ extensionInfo: overlayResult?.extensionInfo,
368
+ appliedOverlays: overlayResult?.appliedOverlays,
369
+ });
370
+ // Add output-specific config to context
371
+ const extendedContext = {
372
+ ...templateContext,
373
+ outputConfig: output.config || {},
374
+ };
375
+ // Resolve and load template
376
+ const specDir = path.dirname(config.openapi).replace(/\/openapi$/, '').replace(`/${config.name}`, '');
377
+ const templatePath = resolveTemplatePath({
378
+ specDir,
379
+ moduleName: config.name,
380
+ templateName: path.basename(output.template),
381
+ }) || output.template;
382
+ if (!fs.existsSync(templatePath)) {
383
+ console.warn(` Warning: Template not found: ${output.template}`);
384
+ continue;
385
+ }
386
+ const template = loadTemplateWithResolution({
387
+ specDir,
388
+ moduleName: config.name,
389
+ templateName: path.basename(output.template),
390
+ });
391
+ const content = template(extendedContext);
392
+ // Ensure output directory exists
393
+ const outputDir = path.dirname(output.output);
394
+ if (!fs.existsSync(outputDir)) {
395
+ fs.mkdirSync(outputDir, { recursive: true });
396
+ }
397
+ // Write output file (only if content changed)
398
+ writeAndLog(output.output, content);
399
+ }
400
+ catch (error) {
401
+ console.error(` Error generating ${output.id}:`, error instanceof Error ? error.message : error);
402
+ }
403
+ }
404
+ }
405
+ /**
406
+ * Generate deps/ re-exports for cross-module dependencies
407
+ */
408
+ async function generateDepsReExports(config, dependencies) {
409
+ if (dependencies.allDeps.length === 0) {
410
+ console.log(`\n No dependencies declared, skipping deps/ generation`);
411
+ return;
412
+ }
413
+ console.log(`\nGenerating deps/ re-exports...`);
414
+ // Build contract-published paths map
415
+ const contractPublicPaths = new Map();
416
+ // Group deps by module
417
+ const moduleNames = new Set();
418
+ for (const dep of dependencies.allDeps) {
419
+ moduleNames.add(dep.module);
420
+ }
421
+ for (const moduleName of moduleNames) {
422
+ // Assume contract-published follows same pattern as config
423
+ contractPublicPaths.set(moduleName, `@project/contract-published/${moduleName}`);
424
+ }
425
+ // Generate deps files directly from already-extracted dependencies
426
+ const generatedFiles = generateSimpleDepsFiles(config.name, dependencies, contractPublicPaths);
427
+ for (const file of generatedFiles) {
428
+ const dir = path.dirname(file.path);
429
+ if (!fs.existsSync(dir)) {
430
+ fs.mkdirSync(dir, { recursive: true });
431
+ }
432
+ writeAndLog(file.path, file.content, ' ');
433
+ }
434
+ }
435
+ /**
436
+ * Generate simple deps files without complex type resolution
437
+ */
438
+ function generateSimpleDepsFiles(moduleName, dependencies, _contractPublicPaths) {
439
+ const files = [];
440
+ // Group deps by target module
441
+ const depsByModule = new Map();
442
+ for (const dep of dependencies.allDeps) {
443
+ if (!depsByModule.has(dep.module)) {
444
+ depsByModule.set(dep.module, []);
445
+ }
446
+ depsByModule.get(dep.module).push({
447
+ domain: dep.domain,
448
+ method: dep.method,
449
+ raw: dep.raw,
450
+ });
451
+ }
452
+ // Generate file for each target module
453
+ for (const [targetModule, deps] of depsByModule) {
454
+ // Use relative path from packages/contract/{module}/deps/ to packages/contract-published/{target}/
455
+ // From packages/contract/{module}/deps/ to packages/contract-published/{target}/
456
+ // ../../../contract-published/{target}/
457
+ const relativePathPrefix = `../../../contract-published/${targetModule}`;
458
+ const content = `/**
459
+ * Auto-generated from x-micro-contracts-depend-on - DO NOT EDIT
460
+ * Source module: ${moduleName}
461
+ * Target module: ${targetModule}
462
+ * Dependencies: ${deps.map(d => d.raw).join(', ')}
463
+ */
464
+
465
+ // Re-exported types from ${targetModule} (contract-published)
466
+ export type * from '${relativePathPrefix}/schemas/types.js';
467
+ export type * from '${relativePathPrefix}/domains/index.js';
468
+ `;
469
+ files.push({
470
+ path: `packages/contract/${moduleName}/deps/${targetModule}.ts`,
471
+ content,
472
+ });
473
+ }
474
+ // Generate index file
475
+ if (files.length > 0) {
476
+ const indexContent = `/**
477
+ * Auto-generated deps index - DO NOT EDIT
478
+ */
479
+
480
+ ${Array.from(depsByModule.keys()).map(m => `export * from './${m}.js';`).join('\n')}
481
+ `;
482
+ files.push({
483
+ path: `packages/contract/${moduleName}/deps/index.ts`,
484
+ content: indexContent,
485
+ });
486
+ }
487
+ return files;
488
+ }
489
+ /**
490
+ * Generate server routes
491
+ */
492
+ async function generateServerRoutes(spec, config, overlayResult = null) {
493
+ if (!config.server)
494
+ return;
495
+ const outputDir = path.resolve(config.server.output);
496
+ const routesFile = config.server.routes;
497
+ console.log(`\nGenerating server routes...`);
498
+ fs.mkdirSync(outputDir, { recursive: true });
499
+ // Template is required for server routes generation
500
+ if (!config.server.template) {
501
+ throw new Error('Server template is required. Please specify server.template in your config.');
502
+ }
503
+ const templateContext = buildTemplateContext(spec, config.name, {
504
+ domainsPath: config.server.domainsPath,
505
+ contractPackage: `@project/contract/${config.name}`,
506
+ extensionInfo: overlayResult?.extensionInfo,
507
+ appliedOverlays: overlayResult?.appliedOverlays,
508
+ });
509
+ const routesContent = generateWithTemplate(config.server.template, 'server', templateContext);
510
+ const routesPath = path.join(outputDir, routesFile);
511
+ writeAndLog(routesPath, routesContent, ' ');
512
+ }
513
+ /**
514
+ * Generate frontend client
515
+ */
516
+ async function generateFrontendClient(spec, config, overlayResult = null) {
517
+ if (!config.frontend)
518
+ return;
519
+ const outputDir = path.resolve(config.frontend.output);
520
+ const clientFile = config.frontend.client;
521
+ console.log(`\nGenerating frontend client...`);
522
+ fs.mkdirSync(outputDir, { recursive: true });
523
+ // Template is required for frontend client generation
524
+ if (!config.frontend.template) {
525
+ throw new Error('Frontend template is required. Please specify frontend.template in your config.');
526
+ }
527
+ const templateContext = buildTemplateContext(spec, config.name, {
528
+ contractPackage: `@project/contract/${config.name}`,
529
+ extensionInfo: overlayResult?.extensionInfo,
530
+ appliedOverlays: overlayResult?.appliedOverlays,
531
+ });
532
+ const clientContent = generateWithTemplate(config.frontend.template, 'frontend', templateContext);
533
+ const clientPath = path.join(outputDir, clientFile);
534
+ writeAndLog(clientPath, clientContent, ' ');
535
+ // Generate domain re-exports
536
+ const domainContent = generateDomainReExports(config.name);
537
+ const domainPath = path.join(outputDir, config.frontend.domain);
538
+ writeAndLog(domainPath, domainContent, ' ');
539
+ }
540
+ /**
541
+ * Generate domain re-exports file
542
+ */
543
+ function generateDomainReExports(moduleName) {
544
+ const lines = [];
545
+ lines.push('/**');
546
+ lines.push(' * Domain re-exports');
547
+ lines.push(' * Auto-generated - DO NOT EDIT');
548
+ lines.push(' */');
549
+ lines.push('');
550
+ // Re-export API clients from api.generated
551
+ lines.push('// API clients');
552
+ lines.push("export * from './api.generated';");
553
+ lines.push('');
554
+ // Re-export types from contract package
555
+ lines.push('// Contract types');
556
+ lines.push(`export * from '@project/contract/${moduleName}/schemas';`);
557
+ lines.push(`export * from '@project/contract/${moduleName}/domains';`);
558
+ lines.push(`export * from '@project/contract/${moduleName}/errors';`);
559
+ lines.push('');
560
+ return lines.join('\n');
561
+ }
562
+ /**
563
+ * Generate documentation (Redoc HTML)
564
+ */
565
+ async function generateDocumentation(openapiPath, outputDir) {
566
+ const { execSync } = await import('child_process');
567
+ const htmlPath = path.join(outputDir, 'api-reference.html');
568
+ try {
569
+ console.log(` Generating Redoc HTML...`);
570
+ execSync(`npx @redocly/cli build-docs "${openapiPath}" -o "${htmlPath}"`, {
571
+ stdio: 'pipe',
572
+ });
573
+ console.log(` Written: ${htmlPath}`);
574
+ }
575
+ catch (error) {
576
+ console.log(` Warning: Redoc HTML generation failed. Install @redocly/cli if needed.`);
577
+ console.log(` Run: npx @redocly/cli build-docs "${openapiPath}" -o "${htmlPath}"`);
578
+ }
579
+ }
580
+ /**
581
+ * Generate error types
582
+ */
583
+ function generateErrors() {
584
+ return `/**
585
+ * Error types
586
+ * Auto-generated - DO NOT EDIT
587
+ */
588
+
589
+ // Re-export ProblemDetails from schemas (RFC 9457)
590
+ export type { ProblemDetails, ValidationError } from '../schemas/types.js';
591
+ import type { ProblemDetails } from '../schemas/types.js';
592
+
593
+ /**
594
+ * API Error wrapper
595
+ */
596
+ export class ApiError extends Error {
597
+ constructor(
598
+ public readonly status: number,
599
+ public readonly problem: ProblemDetails,
600
+ public readonly requestId?: string,
601
+ ) {
602
+ super(problem.title);
603
+ this.name = 'ApiError';
604
+ }
605
+
606
+ get isValidationError(): boolean {
607
+ return this.status === 400;
608
+ }
609
+
610
+ get isNotFound(): boolean {
611
+ return this.status === 404;
612
+ }
613
+
614
+ get isServerError(): boolean {
615
+ return this.status >= 500;
616
+ }
617
+ }
618
+ `;
619
+ }
620
+ /**
621
+ * Generate empty error types (when no endpoints exist)
622
+ */
623
+ function generateEmptyErrors() {
624
+ return `/**
625
+ * Error types
626
+ * Auto-generated - DO NOT EDIT
627
+ *
628
+ * No endpoints defined - error types not needed.
629
+ */
630
+ `;
631
+ }
632
+ /**
633
+ * Filter OpenAPI spec for public endpoints only
634
+ */
635
+ function filterPublicSpec(spec) {
636
+ const filtered = {
637
+ ...spec,
638
+ paths: {},
639
+ tags: [],
640
+ components: {
641
+ schemas: {},
642
+ responses: spec.components?.responses,
643
+ parameters: spec.components?.parameters,
644
+ requestBodies: spec.components?.requestBodies,
645
+ },
646
+ };
647
+ // Collect all referenced schemas and tags from public endpoints
648
+ const usedSchemas = new Set();
649
+ const usedTags = new Set();
650
+ // Filter paths to only include x-micro-contracts-published: true
651
+ for (const [pathKey, pathItem] of Object.entries(spec.paths)) {
652
+ const filteredPathItem = {};
653
+ let hasPublicOperation = false;
654
+ for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
655
+ const operation = pathItem[method];
656
+ if (operation && operation['x-micro-contracts-published'] === true) {
657
+ filteredPathItem[method] = operation;
658
+ hasPublicOperation = true;
659
+ // Collect schema references from this operation
660
+ collectSchemaRefs(operation, usedSchemas);
661
+ // Collect tags from this operation
662
+ if (operation.tags) {
663
+ for (const tag of operation.tags) {
664
+ usedTags.add(tag);
665
+ }
666
+ }
667
+ }
668
+ }
669
+ if (hasPublicOperation) {
670
+ filtered.paths[pathKey] = filteredPathItem;
671
+ }
672
+ }
673
+ // Recursively resolve schema references
674
+ const allUsedSchemas = resolveSchemaRefsRecursively(spec, usedSchemas);
675
+ // Filter schemas to only include used ones
676
+ if (spec.components?.schemas) {
677
+ for (const [schemaName, schema] of Object.entries(spec.components.schemas)) {
678
+ if (allUsedSchemas.has(schemaName)) {
679
+ filtered.components.schemas[schemaName] = schema;
680
+ }
681
+ }
682
+ }
683
+ // Filter tags to only include used ones
684
+ if (spec.tags) {
685
+ filtered.tags = spec.tags.filter(tag => usedTags.has(tag.name));
686
+ }
687
+ // Clean up empty components
688
+ if (Object.keys(filtered.components?.schemas || {}).length === 0) {
689
+ delete filtered.components?.schemas;
690
+ }
691
+ // Clean up empty tags
692
+ if (filtered.tags?.length === 0) {
693
+ delete filtered.tags;
694
+ }
695
+ return filtered;
696
+ }
697
+ /**
698
+ * Collect $ref references from an operation
699
+ */
700
+ function collectSchemaRefs(obj, refs) {
701
+ if (!obj || typeof obj !== 'object')
702
+ return;
703
+ if (Array.isArray(obj)) {
704
+ for (const item of obj) {
705
+ collectSchemaRefs(item, refs);
706
+ }
707
+ return;
708
+ }
709
+ const record = obj;
710
+ // Check for $ref
711
+ if (typeof record['$ref'] === 'string') {
712
+ const ref = record['$ref'];
713
+ const match = ref.match(/#\/components\/schemas\/(.+)/);
714
+ if (match) {
715
+ refs.add(match[1]);
716
+ }
717
+ }
718
+ // Recurse into nested objects
719
+ for (const value of Object.values(record)) {
720
+ collectSchemaRefs(value, refs);
721
+ }
722
+ }
723
+ /**
724
+ * Recursively resolve schema references (schemas can reference other schemas)
725
+ */
726
+ function resolveSchemaRefsRecursively(spec, initialRefs) {
727
+ const allRefs = new Set(initialRefs);
728
+ const toProcess = [...initialRefs];
729
+ while (toProcess.length > 0) {
730
+ const schemaName = toProcess.pop();
731
+ const schema = spec.components?.schemas?.[schemaName];
732
+ if (!schema)
733
+ continue;
734
+ // Collect refs from this schema
735
+ const nestedRefs = new Set();
736
+ collectSchemaRefs(schema, nestedRefs);
737
+ // Add new refs to process
738
+ for (const ref of nestedRefs) {
739
+ if (!allRefs.has(ref)) {
740
+ allRefs.add(ref);
741
+ toProcess.push(ref);
742
+ }
743
+ }
744
+ }
745
+ return allRefs;
746
+ }
747
+ //# sourceMappingURL=index.js.map