@reactive-contracts/compiler 0.1.2-beta → 0.2.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.
- package/dist/index.cjs +489 -20
- package/dist/index.cli.js +112 -45
- package/dist/index.d.cts +42 -1
- package/dist/index.d.ts +42 -1
- package/dist/index.js +458 -20
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,10 +1,86 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createJiti } from 'jiti';
|
|
2
|
+
import * as path2 from 'path';
|
|
2
3
|
import { dirname } from 'path';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { mkdir, writeFile } from 'fs/promises';
|
|
6
|
+
import { glob } from 'glob';
|
|
3
7
|
|
|
4
8
|
// src/config/index.ts
|
|
9
|
+
var defaultConfig = {
|
|
10
|
+
contracts: "./contracts/**/*.contract.ts",
|
|
11
|
+
output: {
|
|
12
|
+
frontend: "./generated/frontend",
|
|
13
|
+
backend: "./generated/backend",
|
|
14
|
+
runtime: "./generated/runtime"
|
|
15
|
+
},
|
|
16
|
+
validation: {
|
|
17
|
+
strictLatency: false,
|
|
18
|
+
requireIntent: true,
|
|
19
|
+
maxComplexity: 10
|
|
20
|
+
},
|
|
21
|
+
optimization: {
|
|
22
|
+
bundleSplitting: false,
|
|
23
|
+
treeShaking: false,
|
|
24
|
+
precompute: []
|
|
25
|
+
}
|
|
26
|
+
};
|
|
5
27
|
function defineConfig(config) {
|
|
6
28
|
return config;
|
|
7
29
|
}
|
|
30
|
+
function resolveConfigPath(configPath, cwd = process.cwd()) {
|
|
31
|
+
if (configPath) {
|
|
32
|
+
const absolutePath = path2.isAbsolute(configPath) ? configPath : path2.join(cwd, configPath);
|
|
33
|
+
return existsSync(absolutePath) ? absolutePath : null;
|
|
34
|
+
}
|
|
35
|
+
const configNames = [
|
|
36
|
+
"rcontracts.config.ts",
|
|
37
|
+
"rcontracts.config.js",
|
|
38
|
+
"rcontracts.config.mjs",
|
|
39
|
+
"reactive-contracts.config.ts",
|
|
40
|
+
"reactive-contracts.config.js"
|
|
41
|
+
];
|
|
42
|
+
for (const name of configNames) {
|
|
43
|
+
const fullPath = path2.join(cwd, name);
|
|
44
|
+
if (existsSync(fullPath)) {
|
|
45
|
+
return fullPath;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
async function loadConfig(configPath, cwd = process.cwd()) {
|
|
51
|
+
const resolvedPath = resolveConfigPath(configPath, cwd);
|
|
52
|
+
if (!resolvedPath) {
|
|
53
|
+
return { ...defaultConfig };
|
|
54
|
+
}
|
|
55
|
+
const jiti = createJiti(import.meta.url, {
|
|
56
|
+
interopDefault: true,
|
|
57
|
+
moduleCache: false
|
|
58
|
+
// Disable cache to get fresh config on reload
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
const module = await jiti.import(resolvedPath);
|
|
62
|
+
const config = module.default || module;
|
|
63
|
+
return {
|
|
64
|
+
...defaultConfig,
|
|
65
|
+
...config,
|
|
66
|
+
output: {
|
|
67
|
+
...defaultConfig.output,
|
|
68
|
+
...config.output || {}
|
|
69
|
+
},
|
|
70
|
+
validation: {
|
|
71
|
+
...defaultConfig.validation,
|
|
72
|
+
...config.validation || {}
|
|
73
|
+
},
|
|
74
|
+
optimization: {
|
|
75
|
+
...defaultConfig.optimization,
|
|
76
|
+
...config.optimization || {}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
81
|
+
throw new Error(`Failed to load config from ${resolvedPath}: ${errorMessage}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
8
84
|
|
|
9
85
|
// src/validator/index.ts
|
|
10
86
|
function validateContract(contract) {
|
|
@@ -51,17 +127,17 @@ function validateContract(contract) {
|
|
|
51
127
|
warnings
|
|
52
128
|
};
|
|
53
129
|
}
|
|
54
|
-
function validateShape(shape,
|
|
130
|
+
function validateShape(shape, path3, errors, warnings) {
|
|
55
131
|
if (!shape || typeof shape !== "object") {
|
|
56
|
-
errors.push(`Invalid shape at ${
|
|
132
|
+
errors.push(`Invalid shape at ${path3 || "root"}: must be an object`);
|
|
57
133
|
return;
|
|
58
134
|
}
|
|
59
135
|
const keys = Object.keys(shape);
|
|
60
136
|
if (keys.length === 0) {
|
|
61
|
-
warnings.push(`Shape at ${
|
|
137
|
+
warnings.push(`Shape at ${path3 || "root"} is empty`);
|
|
62
138
|
}
|
|
63
139
|
for (const key of keys) {
|
|
64
|
-
const fieldPath =
|
|
140
|
+
const fieldPath = path3 ? `${path3}.${key}` : key;
|
|
65
141
|
const value = shape[key];
|
|
66
142
|
if (!value) {
|
|
67
143
|
errors.push(`Field "${fieldPath}" has undefined value`);
|
|
@@ -75,46 +151,46 @@ function validateShape(shape, path, errors, warnings) {
|
|
|
75
151
|
validateTypeDefinition(value, fieldPath, errors, warnings);
|
|
76
152
|
}
|
|
77
153
|
}
|
|
78
|
-
function validateTypeDefinition(type,
|
|
154
|
+
function validateTypeDefinition(type, path3, errors, warnings) {
|
|
79
155
|
if (typeof type === "string") {
|
|
80
156
|
const validPrimitives = ["string", "number", "boolean", "Date", "null", "undefined"];
|
|
81
157
|
const isURL = type === "URL" || type.startsWith("URL<");
|
|
82
158
|
if (!validPrimitives.includes(type) && !isURL) {
|
|
83
|
-
errors.push(`Invalid type at ${
|
|
159
|
+
errors.push(`Invalid type at ${path3}: "${type}" is not a valid primitive or URL type`);
|
|
84
160
|
}
|
|
85
161
|
if (isURL && type !== "URL") {
|
|
86
162
|
const urlMatch = type.match(/^URL<(.+)>$/);
|
|
87
163
|
if (!urlMatch) {
|
|
88
|
-
errors.push(`Invalid URL type at ${
|
|
164
|
+
errors.push(`Invalid URL type at ${path3}: must be "URL" or "URL<options>"`);
|
|
89
165
|
}
|
|
90
166
|
}
|
|
91
167
|
} else if (typeof type === "object" && type !== null) {
|
|
92
168
|
if ("_brand" in type && type._brand === "DerivedField") {
|
|
93
169
|
validateDerivedField(
|
|
94
170
|
type,
|
|
95
|
-
|
|
171
|
+
path3,
|
|
96
172
|
errors);
|
|
97
173
|
} else {
|
|
98
|
-
validateShape(type,
|
|
174
|
+
validateShape(type, path3, errors, warnings);
|
|
99
175
|
}
|
|
100
176
|
} else {
|
|
101
|
-
errors.push(`Invalid type at ${
|
|
177
|
+
errors.push(`Invalid type at ${path3}: must be a string, object, or DerivedField`);
|
|
102
178
|
}
|
|
103
179
|
}
|
|
104
|
-
function validateDerivedField(field,
|
|
180
|
+
function validateDerivedField(field, path3, errors, _warnings) {
|
|
105
181
|
if (typeof field.derive !== "function") {
|
|
106
|
-
errors.push(`Derived field at ${
|
|
182
|
+
errors.push(`Derived field at ${path3} must have a derive function`);
|
|
107
183
|
}
|
|
108
184
|
if (field.dependencies && !Array.isArray(field.dependencies)) {
|
|
109
|
-
errors.push(`Derived field dependencies at ${
|
|
185
|
+
errors.push(`Derived field dependencies at ${path3} must be an array`);
|
|
110
186
|
}
|
|
111
187
|
if (field.preferredLayer && !["client", "edge", "origin"].includes(field.preferredLayer)) {
|
|
112
|
-
errors.push(`Derived field preferredLayer at ${
|
|
188
|
+
errors.push(`Derived field preferredLayer at ${path3} must be 'client', 'edge', or 'origin'`);
|
|
113
189
|
}
|
|
114
190
|
if (field.dependencies && Array.isArray(field.dependencies)) {
|
|
115
191
|
for (const dep of field.dependencies) {
|
|
116
192
|
if (typeof dep !== "string") {
|
|
117
|
-
errors.push(`Derived field dependency at ${
|
|
193
|
+
errors.push(`Derived field dependency at ${path3} must be a string`);
|
|
118
194
|
}
|
|
119
195
|
}
|
|
120
196
|
}
|
|
@@ -243,10 +319,10 @@ function validateVersioning(versioning, errors, warnings) {
|
|
|
243
319
|
}
|
|
244
320
|
function collectFieldPaths(shape, prefix, result) {
|
|
245
321
|
for (const [key, value] of Object.entries(shape)) {
|
|
246
|
-
const
|
|
247
|
-
result.add(
|
|
322
|
+
const path3 = prefix ? `${prefix}.${key}` : key;
|
|
323
|
+
result.add(path3);
|
|
248
324
|
if (typeof value === "object" && value !== null && !("_brand" in value) && typeof value !== "string") {
|
|
249
|
-
collectFieldPaths(value,
|
|
325
|
+
collectFieldPaths(value, path3, result);
|
|
250
326
|
}
|
|
251
327
|
}
|
|
252
328
|
}
|
|
@@ -414,6 +490,216 @@ export const ${typeName}Intent = '${definition.intent}' as const;
|
|
|
414
490
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
415
491
|
await writeFile(outputPath, content, "utf-8");
|
|
416
492
|
}
|
|
493
|
+
async function generateBackendResolver(contract, outputPath) {
|
|
494
|
+
const { definition } = contract;
|
|
495
|
+
const typeName = definition.name;
|
|
496
|
+
const shapeType = generateTypeDefinitions(definition.shape, `${typeName}ResolverShape`);
|
|
497
|
+
const content = `/**
|
|
498
|
+
* Auto-generated resolver template for ${typeName} contract
|
|
499
|
+
* Generated by @reactive-contracts/compiler
|
|
500
|
+
*
|
|
501
|
+
* Intent: ${definition.intent}
|
|
502
|
+
*/
|
|
503
|
+
|
|
504
|
+
import { implementContract } from '@reactive-contracts/server';
|
|
505
|
+
import type { Contract } from '@reactive-contracts/core';
|
|
506
|
+
import type { ResolverContext } from '@reactive-contracts/server';
|
|
507
|
+
|
|
508
|
+
${shapeType}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Implement the ${typeName} contract resolver
|
|
512
|
+
*
|
|
513
|
+
* This function should return data matching the contract shape.
|
|
514
|
+
* Derived fields will be computed automatically - don't include them.
|
|
515
|
+
*/
|
|
516
|
+
export const ${typeName}Resolver = implementContract(
|
|
517
|
+
// Import your contract definition here
|
|
518
|
+
{} as Contract, // Replace with your contract
|
|
519
|
+
{
|
|
520
|
+
async resolve(params: Record<string, unknown>, context: ResolverContext): Promise<${typeName}ResolverShape> {
|
|
521
|
+
// TODO: Implement your data fetching logic here
|
|
522
|
+
|
|
523
|
+
// Example:
|
|
524
|
+
// const data = await db.query(...);
|
|
525
|
+
// return {
|
|
526
|
+
// // Map your data to match the contract shape
|
|
527
|
+
// };
|
|
528
|
+
|
|
529
|
+
throw new Error('${typeName}Resolver not implemented yet');
|
|
530
|
+
},
|
|
531
|
+
|
|
532
|
+
// Optional: Configure caching
|
|
533
|
+
cache: {
|
|
534
|
+
ttl: '5m',
|
|
535
|
+
staleWhileRevalidate: '1h',
|
|
536
|
+
tags: (params) => [\`${typeName.toLowerCase()}:\${params.id}\`],
|
|
537
|
+
},
|
|
538
|
+
}
|
|
539
|
+
);
|
|
540
|
+
`;
|
|
541
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
542
|
+
await writeFile(outputPath, content, "utf-8");
|
|
543
|
+
}
|
|
544
|
+
async function generateRuntimeNegotiator(contract, outputPath) {
|
|
545
|
+
const { definition } = contract;
|
|
546
|
+
const typeName = definition.name;
|
|
547
|
+
const content = `/**
|
|
548
|
+
* Auto-generated runtime negotiator for ${typeName} contract
|
|
549
|
+
* DO NOT EDIT - This file is generated by @reactive-contracts/compiler
|
|
550
|
+
*/
|
|
551
|
+
|
|
552
|
+
import type { Contract } from '@reactive-contracts/core';
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Runtime negotiator for ${typeName}
|
|
556
|
+
* Handles SLA monitoring, fallback logic, and performance tracking
|
|
557
|
+
*/
|
|
558
|
+
export class ${typeName}Negotiator {
|
|
559
|
+
private contract: Contract;
|
|
560
|
+
private metrics: {
|
|
561
|
+
executionTime: number[];
|
|
562
|
+
cacheHits: number;
|
|
563
|
+
cacheMisses: number;
|
|
564
|
+
} = {
|
|
565
|
+
executionTime: [],
|
|
566
|
+
cacheHits: 0,
|
|
567
|
+
cacheMisses: 0,
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
constructor(contract: Contract) {
|
|
571
|
+
this.contract = contract;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Execute contract with SLA monitoring
|
|
576
|
+
*/
|
|
577
|
+
async execute<TData>(
|
|
578
|
+
resolver: () => Promise<TData>,
|
|
579
|
+
options?: {
|
|
580
|
+
useCache?: boolean;
|
|
581
|
+
timeout?: number;
|
|
582
|
+
}
|
|
583
|
+
): Promise<{
|
|
584
|
+
data: TData;
|
|
585
|
+
status: {
|
|
586
|
+
latency: 'normal' | 'degraded' | 'violated';
|
|
587
|
+
freshness: 'fresh' | 'stale' | 'expired';
|
|
588
|
+
availability: 'available' | 'degraded' | 'unavailable';
|
|
589
|
+
};
|
|
590
|
+
metadata: {
|
|
591
|
+
executionTime: number;
|
|
592
|
+
cacheHit: boolean;
|
|
593
|
+
derivedAt: 'client' | 'edge' | 'origin';
|
|
594
|
+
};
|
|
595
|
+
}> {
|
|
596
|
+
const startTime = performance.now();
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
// Execute resolver
|
|
600
|
+
const data = await resolver();
|
|
601
|
+
const executionTime = performance.now() - startTime;
|
|
602
|
+
|
|
603
|
+
// Track metrics
|
|
604
|
+
this.metrics.executionTime.push(executionTime);
|
|
605
|
+
if (options?.useCache) {
|
|
606
|
+
this.metrics.cacheHits++;
|
|
607
|
+
} else {
|
|
608
|
+
this.metrics.cacheMisses++;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Determine latency status
|
|
612
|
+
const maxLatency = this.getMaxLatency();
|
|
613
|
+
const latencyStatus = this.evaluateLatency(executionTime, maxLatency);
|
|
614
|
+
|
|
615
|
+
return {
|
|
616
|
+
data,
|
|
617
|
+
status: {
|
|
618
|
+
latency: latencyStatus,
|
|
619
|
+
freshness: 'fresh',
|
|
620
|
+
availability: 'available',
|
|
621
|
+
},
|
|
622
|
+
metadata: {
|
|
623
|
+
executionTime,
|
|
624
|
+
cacheHit: options?.useCache ?? false,
|
|
625
|
+
derivedAt: 'origin',
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
} catch (error) {
|
|
629
|
+
// Handle fallback based on contract constraints
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Get metrics for monitoring
|
|
636
|
+
*/
|
|
637
|
+
getMetrics() {
|
|
638
|
+
const avgExecutionTime =
|
|
639
|
+
this.metrics.executionTime.length > 0
|
|
640
|
+
? this.metrics.executionTime.reduce((a, b) => a + b, 0) / this.metrics.executionTime.length
|
|
641
|
+
: 0;
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
averageExecutionTime: avgExecutionTime,
|
|
645
|
+
p95ExecutionTime: this.calculateP95(),
|
|
646
|
+
cacheHitRate:
|
|
647
|
+
this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses) || 0,
|
|
648
|
+
totalExecutions: this.metrics.executionTime.length,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private getMaxLatency(): number {
|
|
653
|
+
const latency = this.contract.definition.constraints?.latency?.max;
|
|
654
|
+
if (!latency) return Infinity;
|
|
655
|
+
|
|
656
|
+
// Simple parsing for MVP
|
|
657
|
+
const match = latency.match(/^(\\d+)(ms|s|m)$/);
|
|
658
|
+
if (!match) return Infinity;
|
|
659
|
+
|
|
660
|
+
const value = parseInt(match[1], 10);
|
|
661
|
+
const unit = match[2];
|
|
662
|
+
|
|
663
|
+
switch (unit) {
|
|
664
|
+
case 'ms': return value;
|
|
665
|
+
case 's': return value * 1000;
|
|
666
|
+
case 'm': return value * 60 * 1000;
|
|
667
|
+
default: return Infinity;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
private evaluateLatency(
|
|
672
|
+
executionTime: number,
|
|
673
|
+
maxLatency: number
|
|
674
|
+
): 'normal' | 'degraded' | 'violated' {
|
|
675
|
+
if (executionTime <= maxLatency) {
|
|
676
|
+
return 'normal';
|
|
677
|
+
} else if (executionTime <= maxLatency * 1.5) {
|
|
678
|
+
return 'degraded';
|
|
679
|
+
} else {
|
|
680
|
+
return 'violated';
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
private calculateP95(): number {
|
|
685
|
+
if (this.metrics.executionTime.length === 0) return 0;
|
|
686
|
+
|
|
687
|
+
const sorted = [...this.metrics.executionTime].sort((a, b) => a - b);
|
|
688
|
+
const index = Math.floor(sorted.length * 0.95);
|
|
689
|
+
return sorted[index];
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Create a new negotiator instance
|
|
695
|
+
*/
|
|
696
|
+
export function create${typeName}Negotiator(contract: Contract): ${typeName}Negotiator {
|
|
697
|
+
return new ${typeName}Negotiator(contract);
|
|
698
|
+
}
|
|
699
|
+
`;
|
|
700
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
701
|
+
await writeFile(outputPath, content, "utf-8");
|
|
702
|
+
}
|
|
417
703
|
function generateTypeDefinitions(shape, typeName) {
|
|
418
704
|
const fields = generateShapeFields(shape, 0);
|
|
419
705
|
return `export interface ${typeName} {
|
|
@@ -463,7 +749,159 @@ ${indentStr}}`;
|
|
|
463
749
|
}
|
|
464
750
|
return "any";
|
|
465
751
|
}
|
|
752
|
+
var silentLogger = {
|
|
753
|
+
info: () => {
|
|
754
|
+
},
|
|
755
|
+
success: () => {
|
|
756
|
+
},
|
|
757
|
+
warning: () => {
|
|
758
|
+
},
|
|
759
|
+
error: () => {
|
|
760
|
+
},
|
|
761
|
+
verbose: () => {
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
var consoleLogger = {
|
|
765
|
+
info: (msg) => console.log("[rcontracts]", msg),
|
|
766
|
+
success: (msg) => console.log("[rcontracts] \u2713", msg),
|
|
767
|
+
warning: (msg) => console.warn("[rcontracts] \u26A0", msg),
|
|
768
|
+
error: (msg) => console.error("[rcontracts] \u2717", msg),
|
|
769
|
+
verbose: (msg) => console.log("[rcontracts]", msg)
|
|
770
|
+
};
|
|
771
|
+
async function parseContractFile(filePath, logger = silentLogger) {
|
|
772
|
+
const contracts = [];
|
|
773
|
+
const errors = [];
|
|
774
|
+
const jiti = createJiti(import.meta.url, {
|
|
775
|
+
interopDefault: true,
|
|
776
|
+
moduleCache: false
|
|
777
|
+
// Disable cache to avoid stale imports
|
|
778
|
+
});
|
|
779
|
+
try {
|
|
780
|
+
const module = await jiti.import(filePath);
|
|
781
|
+
const contractExports = Object.entries(module).filter(
|
|
782
|
+
([key, value]) => key.endsWith("Contract") && typeof value === "object" && value !== null && "_brand" in value && value._brand === "Contract"
|
|
783
|
+
);
|
|
784
|
+
if (contractExports.length === 0) {
|
|
785
|
+
errors.push(`No contract found in ${path2.basename(filePath)}`);
|
|
786
|
+
return { contracts, errors };
|
|
787
|
+
}
|
|
788
|
+
for (const [exportName, contractObj] of contractExports) {
|
|
789
|
+
contracts.push({
|
|
790
|
+
name: exportName,
|
|
791
|
+
contract: contractObj
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
} catch (err) {
|
|
795
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
796
|
+
errors.push(`Failed to parse ${path2.basename(filePath)}: ${errorMessage}`);
|
|
797
|
+
logger.error?.(errorMessage);
|
|
798
|
+
}
|
|
799
|
+
return { contracts, errors };
|
|
800
|
+
}
|
|
801
|
+
async function compileContract(contract, config, cwd = process.cwd(), logger = silentLogger) {
|
|
802
|
+
const validation = validateContract(contract);
|
|
803
|
+
const latency = analyzeLatency(contract);
|
|
804
|
+
const generated = {};
|
|
805
|
+
for (const warn of validation.warnings) {
|
|
806
|
+
logger.warning?.(warn);
|
|
807
|
+
}
|
|
808
|
+
if (!validation.valid) {
|
|
809
|
+
for (const err of validation.errors) {
|
|
810
|
+
logger.error?.(err);
|
|
811
|
+
}
|
|
812
|
+
return { contract, validation, latency, generated };
|
|
813
|
+
}
|
|
814
|
+
if (latency.status === "error") {
|
|
815
|
+
logger.error?.(latency.message || "Latency analysis failed");
|
|
816
|
+
if (config.validation?.strictLatency) {
|
|
817
|
+
return { contract, validation, latency, generated };
|
|
818
|
+
}
|
|
819
|
+
} else if (latency.status === "warning") {
|
|
820
|
+
logger.warning?.(latency.message || "Latency constraint may not be met");
|
|
821
|
+
}
|
|
822
|
+
if (latency.suggestions?.length) {
|
|
823
|
+
for (const suggestion of latency.suggestions) {
|
|
824
|
+
logger.info?.(`\u{1F4A1} ${suggestion}`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const contractName = contract.definition.name;
|
|
828
|
+
try {
|
|
829
|
+
const frontendPath = path2.join(cwd, config.output.frontend, `${contractName}.ts`);
|
|
830
|
+
await generateFrontendTypes(contract, frontendPath);
|
|
831
|
+
generated.frontend = frontendPath;
|
|
832
|
+
const backendPath = path2.join(cwd, config.output.backend, `${contractName}.resolver.ts`);
|
|
833
|
+
await generateBackendResolver(contract, backendPath);
|
|
834
|
+
generated.backend = backendPath;
|
|
835
|
+
const runtimePath = path2.join(cwd, config.output.runtime, `${contractName}.negotiator.ts`);
|
|
836
|
+
await generateRuntimeNegotiator(contract, runtimePath);
|
|
837
|
+
generated.runtime = runtimePath;
|
|
838
|
+
logger.success?.(`Generated code for ${contractName}`);
|
|
839
|
+
} catch (err) {
|
|
840
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
841
|
+
logger.error?.(`Failed to generate code for ${contractName}: ${errorMessage}`);
|
|
842
|
+
validation.errors.push(errorMessage);
|
|
843
|
+
validation.valid = false;
|
|
844
|
+
}
|
|
845
|
+
return { contract, validation, latency, generated };
|
|
846
|
+
}
|
|
847
|
+
async function findContractFiles(config, cwd = process.cwd()) {
|
|
848
|
+
return glob(config.contracts, {
|
|
849
|
+
cwd,
|
|
850
|
+
absolute: true
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
async function compileAll(options) {
|
|
854
|
+
const { config, cwd = process.cwd(), logger = silentLogger, file } = options;
|
|
855
|
+
const results = [];
|
|
856
|
+
const errors = [];
|
|
857
|
+
const warnings = [];
|
|
858
|
+
let contractFiles;
|
|
859
|
+
if (file) {
|
|
860
|
+
contractFiles = [path2.isAbsolute(file) ? file : path2.join(cwd, file)];
|
|
861
|
+
} else {
|
|
862
|
+
contractFiles = await findContractFiles(config, cwd);
|
|
863
|
+
}
|
|
864
|
+
if (contractFiles.length === 0) {
|
|
865
|
+
errors.push(`No contract files found matching pattern: ${config.contracts}`);
|
|
866
|
+
return { success: false, results, errors, warnings };
|
|
867
|
+
}
|
|
868
|
+
logger.info?.(`Found ${contractFiles.length} contract file(s)`);
|
|
869
|
+
for (const filePath of contractFiles) {
|
|
870
|
+
const fileName = path2.basename(filePath, ".contract.ts");
|
|
871
|
+
logger.verbose?.(`Processing ${fileName}...`);
|
|
872
|
+
const { contracts, errors: parseErrors } = await parseContractFile(filePath, logger);
|
|
873
|
+
if (parseErrors.length > 0) {
|
|
874
|
+
errors.push(...parseErrors);
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
for (const { name, contract } of contracts) {
|
|
878
|
+
const result = await compileContract(contract, config, cwd, logger);
|
|
879
|
+
results.push(result);
|
|
880
|
+
if (!result.validation.valid) {
|
|
881
|
+
errors.push(`Validation failed for ${name}`);
|
|
882
|
+
}
|
|
883
|
+
warnings.push(...result.validation.warnings);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const success = errors.length === 0;
|
|
887
|
+
if (success) {
|
|
888
|
+
logger.success?.(`Successfully compiled ${results.length} contract(s)`);
|
|
889
|
+
} else {
|
|
890
|
+
logger.error?.(`Compilation failed with ${errors.length} error(s)`);
|
|
891
|
+
}
|
|
892
|
+
return { success, results, errors, warnings };
|
|
893
|
+
}
|
|
894
|
+
function isContractFile(filePath) {
|
|
895
|
+
return filePath.endsWith(".contract.ts");
|
|
896
|
+
}
|
|
897
|
+
function getGeneratedFilesForContract(contractName, config, cwd = process.cwd()) {
|
|
898
|
+
return {
|
|
899
|
+
frontend: path2.join(cwd, config.output.frontend, `${contractName}.ts`),
|
|
900
|
+
backend: path2.join(cwd, config.output.backend, `${contractName}.resolver.ts`),
|
|
901
|
+
runtime: path2.join(cwd, config.output.runtime, `${contractName}.negotiator.ts`)
|
|
902
|
+
};
|
|
903
|
+
}
|
|
466
904
|
|
|
467
|
-
export { analyzeLatency, defineConfig, generateFrontendTypes, validateContract };
|
|
905
|
+
export { analyzeLatency, compileAll, compileContract, consoleLogger, defineConfig, findContractFiles, generateBackendResolver, generateFrontendTypes, generateRuntimeNegotiator, getGeneratedFilesForContract, isContractFile, loadConfig, parseContractFile, silentLogger, validateContract };
|
|
468
906
|
//# sourceMappingURL=index.js.map
|
|
469
907
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reactive-contracts/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Build-time compiler and validator for Reactive Contracts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
"commander": "^14.0.2",
|
|
23
23
|
"fs-extra": "^11.3.3",
|
|
24
24
|
"glob": "^13.0.0",
|
|
25
|
+
"jiti": "^2.4.2",
|
|
25
26
|
"ora": "^9.0.0",
|
|
26
27
|
"picocolors": "^1.1.1",
|
|
27
|
-
"tsx": "^4.21.0",
|
|
28
28
|
"zod": "^4.3.5",
|
|
29
|
-
"@reactive-contracts/core": "0.1.
|
|
29
|
+
"@reactive-contracts/core": "0.1.3-beta"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/fs-extra": "^11.0.4",
|