@reactive-contracts/compiler 0.1.3-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.js CHANGED
@@ -1,10 +1,86 @@
1
- import { mkdir, writeFile } from 'fs/promises';
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, path, errors, warnings) {
130
+ function validateShape(shape, path3, errors, warnings) {
55
131
  if (!shape || typeof shape !== "object") {
56
- errors.push(`Invalid shape at ${path || "root"}: must be an object`);
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 ${path || "root"} is empty`);
137
+ warnings.push(`Shape at ${path3 || "root"} is empty`);
62
138
  }
63
139
  for (const key of keys) {
64
- const fieldPath = path ? `${path}.${key}` : key;
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, path, errors, warnings) {
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 ${path}: "${type}" is not a valid primitive or URL type`);
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 ${path}: must be "URL" or "URL<options>"`);
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
- path,
171
+ path3,
96
172
  errors);
97
173
  } else {
98
- validateShape(type, path, errors, warnings);
174
+ validateShape(type, path3, errors, warnings);
99
175
  }
100
176
  } else {
101
- errors.push(`Invalid type at ${path}: must be a string, object, or DerivedField`);
177
+ errors.push(`Invalid type at ${path3}: must be a string, object, or DerivedField`);
102
178
  }
103
179
  }
104
- function validateDerivedField(field, path, errors, _warnings) {
180
+ function validateDerivedField(field, path3, errors, _warnings) {
105
181
  if (typeof field.derive !== "function") {
106
- errors.push(`Derived field at ${path} must have a derive function`);
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 ${path} must be an array`);
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 ${path} must be 'client', 'edge', or 'origin'`);
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 ${path} must be a string`);
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 path = prefix ? `${prefix}.${key}` : key;
247
- result.add(path);
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, path, result);
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.1.3-beta",
3
+ "version": "0.2.0",
4
4
  "description": "Build-time compiler and validator for Reactive Contracts",
5
5
  "type": "module",
6
6
  "bin": {