@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 CHANGED
@@ -1,12 +1,108 @@
1
1
  'use strict';
2
2
 
3
+ var jiti = require('jiti');
4
+ var path2 = require('path');
5
+ var fs = require('fs');
3
6
  var promises = require('fs/promises');
4
- var path = require('path');
7
+ var glob = require('glob');
8
+
9
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
10
+ function _interopNamespace(e) {
11
+ if (e && e.__esModule) return e;
12
+ var n = Object.create(null);
13
+ if (e) {
14
+ Object.keys(e).forEach(function (k) {
15
+ if (k !== 'default') {
16
+ var d = Object.getOwnPropertyDescriptor(e, k);
17
+ Object.defineProperty(n, k, d.get ? d : {
18
+ enumerable: true,
19
+ get: function () { return e[k]; }
20
+ });
21
+ }
22
+ });
23
+ }
24
+ n.default = e;
25
+ return Object.freeze(n);
26
+ }
27
+
28
+ var path2__namespace = /*#__PURE__*/_interopNamespace(path2);
5
29
 
6
30
  // src/config/index.ts
31
+ var defaultConfig = {
32
+ contracts: "./contracts/**/*.contract.ts",
33
+ output: {
34
+ frontend: "./generated/frontend",
35
+ backend: "./generated/backend",
36
+ runtime: "./generated/runtime"
37
+ },
38
+ validation: {
39
+ strictLatency: false,
40
+ requireIntent: true,
41
+ maxComplexity: 10
42
+ },
43
+ optimization: {
44
+ bundleSplitting: false,
45
+ treeShaking: false,
46
+ precompute: []
47
+ }
48
+ };
7
49
  function defineConfig(config) {
8
50
  return config;
9
51
  }
52
+ function resolveConfigPath(configPath, cwd = process.cwd()) {
53
+ if (configPath) {
54
+ const absolutePath = path2__namespace.isAbsolute(configPath) ? configPath : path2__namespace.join(cwd, configPath);
55
+ return fs.existsSync(absolutePath) ? absolutePath : null;
56
+ }
57
+ const configNames = [
58
+ "rcontracts.config.ts",
59
+ "rcontracts.config.js",
60
+ "rcontracts.config.mjs",
61
+ "reactive-contracts.config.ts",
62
+ "reactive-contracts.config.js"
63
+ ];
64
+ for (const name of configNames) {
65
+ const fullPath = path2__namespace.join(cwd, name);
66
+ if (fs.existsSync(fullPath)) {
67
+ return fullPath;
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ async function loadConfig(configPath, cwd = process.cwd()) {
73
+ const resolvedPath = resolveConfigPath(configPath, cwd);
74
+ if (!resolvedPath) {
75
+ return { ...defaultConfig };
76
+ }
77
+ const jiti$1 = jiti.createJiti((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)), {
78
+ interopDefault: true,
79
+ moduleCache: false
80
+ // Disable cache to get fresh config on reload
81
+ });
82
+ try {
83
+ const module = await jiti$1.import(resolvedPath);
84
+ const config = module.default || module;
85
+ return {
86
+ ...defaultConfig,
87
+ ...config,
88
+ output: {
89
+ ...defaultConfig.output,
90
+ ...config.output || {}
91
+ },
92
+ validation: {
93
+ ...defaultConfig.validation,
94
+ ...config.validation || {}
95
+ },
96
+ optimization: {
97
+ ...defaultConfig.optimization,
98
+ ...config.optimization || {}
99
+ }
100
+ };
101
+ } catch (err) {
102
+ const errorMessage = err instanceof Error ? err.message : String(err);
103
+ throw new Error(`Failed to load config from ${resolvedPath}: ${errorMessage}`);
104
+ }
105
+ }
10
106
 
11
107
  // src/validator/index.ts
12
108
  function validateContract(contract) {
@@ -53,17 +149,17 @@ function validateContract(contract) {
53
149
  warnings
54
150
  };
55
151
  }
56
- function validateShape(shape, path, errors, warnings) {
152
+ function validateShape(shape, path3, errors, warnings) {
57
153
  if (!shape || typeof shape !== "object") {
58
- errors.push(`Invalid shape at ${path || "root"}: must be an object`);
154
+ errors.push(`Invalid shape at ${path3 || "root"}: must be an object`);
59
155
  return;
60
156
  }
61
157
  const keys = Object.keys(shape);
62
158
  if (keys.length === 0) {
63
- warnings.push(`Shape at ${path || "root"} is empty`);
159
+ warnings.push(`Shape at ${path3 || "root"} is empty`);
64
160
  }
65
161
  for (const key of keys) {
66
- const fieldPath = path ? `${path}.${key}` : key;
162
+ const fieldPath = path3 ? `${path3}.${key}` : key;
67
163
  const value = shape[key];
68
164
  if (!value) {
69
165
  errors.push(`Field "${fieldPath}" has undefined value`);
@@ -77,46 +173,46 @@ function validateShape(shape, path, errors, warnings) {
77
173
  validateTypeDefinition(value, fieldPath, errors, warnings);
78
174
  }
79
175
  }
80
- function validateTypeDefinition(type, path, errors, warnings) {
176
+ function validateTypeDefinition(type, path3, errors, warnings) {
81
177
  if (typeof type === "string") {
82
178
  const validPrimitives = ["string", "number", "boolean", "Date", "null", "undefined"];
83
179
  const isURL = type === "URL" || type.startsWith("URL<");
84
180
  if (!validPrimitives.includes(type) && !isURL) {
85
- errors.push(`Invalid type at ${path}: "${type}" is not a valid primitive or URL type`);
181
+ errors.push(`Invalid type at ${path3}: "${type}" is not a valid primitive or URL type`);
86
182
  }
87
183
  if (isURL && type !== "URL") {
88
184
  const urlMatch = type.match(/^URL<(.+)>$/);
89
185
  if (!urlMatch) {
90
- errors.push(`Invalid URL type at ${path}: must be "URL" or "URL<options>"`);
186
+ errors.push(`Invalid URL type at ${path3}: must be "URL" or "URL<options>"`);
91
187
  }
92
188
  }
93
189
  } else if (typeof type === "object" && type !== null) {
94
190
  if ("_brand" in type && type._brand === "DerivedField") {
95
191
  validateDerivedField(
96
192
  type,
97
- path,
193
+ path3,
98
194
  errors);
99
195
  } else {
100
- validateShape(type, path, errors, warnings);
196
+ validateShape(type, path3, errors, warnings);
101
197
  }
102
198
  } else {
103
- errors.push(`Invalid type at ${path}: must be a string, object, or DerivedField`);
199
+ errors.push(`Invalid type at ${path3}: must be a string, object, or DerivedField`);
104
200
  }
105
201
  }
106
- function validateDerivedField(field, path, errors, _warnings) {
202
+ function validateDerivedField(field, path3, errors, _warnings) {
107
203
  if (typeof field.derive !== "function") {
108
- errors.push(`Derived field at ${path} must have a derive function`);
204
+ errors.push(`Derived field at ${path3} must have a derive function`);
109
205
  }
110
206
  if (field.dependencies && !Array.isArray(field.dependencies)) {
111
- errors.push(`Derived field dependencies at ${path} must be an array`);
207
+ errors.push(`Derived field dependencies at ${path3} must be an array`);
112
208
  }
113
209
  if (field.preferredLayer && !["client", "edge", "origin"].includes(field.preferredLayer)) {
114
- errors.push(`Derived field preferredLayer at ${path} must be 'client', 'edge', or 'origin'`);
210
+ errors.push(`Derived field preferredLayer at ${path3} must be 'client', 'edge', or 'origin'`);
115
211
  }
116
212
  if (field.dependencies && Array.isArray(field.dependencies)) {
117
213
  for (const dep of field.dependencies) {
118
214
  if (typeof dep !== "string") {
119
- errors.push(`Derived field dependency at ${path} must be a string`);
215
+ errors.push(`Derived field dependency at ${path3} must be a string`);
120
216
  }
121
217
  }
122
218
  }
@@ -245,10 +341,10 @@ function validateVersioning(versioning, errors, warnings) {
245
341
  }
246
342
  function collectFieldPaths(shape, prefix, result) {
247
343
  for (const [key, value] of Object.entries(shape)) {
248
- const path = prefix ? `${prefix}.${key}` : key;
249
- result.add(path);
344
+ const path3 = prefix ? `${prefix}.${key}` : key;
345
+ result.add(path3);
250
346
  if (typeof value === "object" && value !== null && !("_brand" in value) && typeof value !== "string") {
251
- collectFieldPaths(value, path, result);
347
+ collectFieldPaths(value, path3, result);
252
348
  }
253
349
  }
254
350
  }
@@ -413,7 +509,217 @@ export interface ${typeName}Result {
413
509
  */
414
510
  export const ${typeName}Intent = '${definition.intent}' as const;
415
511
  `;
416
- await promises.mkdir(path.dirname(outputPath), { recursive: true });
512
+ await promises.mkdir(path2.dirname(outputPath), { recursive: true });
513
+ await promises.writeFile(outputPath, content, "utf-8");
514
+ }
515
+ async function generateBackendResolver(contract, outputPath) {
516
+ const { definition } = contract;
517
+ const typeName = definition.name;
518
+ const shapeType = generateTypeDefinitions(definition.shape, `${typeName}ResolverShape`);
519
+ const content = `/**
520
+ * Auto-generated resolver template for ${typeName} contract
521
+ * Generated by @reactive-contracts/compiler
522
+ *
523
+ * Intent: ${definition.intent}
524
+ */
525
+
526
+ import { implementContract } from '@reactive-contracts/server';
527
+ import type { Contract } from '@reactive-contracts/core';
528
+ import type { ResolverContext } from '@reactive-contracts/server';
529
+
530
+ ${shapeType}
531
+
532
+ /**
533
+ * Implement the ${typeName} contract resolver
534
+ *
535
+ * This function should return data matching the contract shape.
536
+ * Derived fields will be computed automatically - don't include them.
537
+ */
538
+ export const ${typeName}Resolver = implementContract(
539
+ // Import your contract definition here
540
+ {} as Contract, // Replace with your contract
541
+ {
542
+ async resolve(params: Record<string, unknown>, context: ResolverContext): Promise<${typeName}ResolverShape> {
543
+ // TODO: Implement your data fetching logic here
544
+
545
+ // Example:
546
+ // const data = await db.query(...);
547
+ // return {
548
+ // // Map your data to match the contract shape
549
+ // };
550
+
551
+ throw new Error('${typeName}Resolver not implemented yet');
552
+ },
553
+
554
+ // Optional: Configure caching
555
+ cache: {
556
+ ttl: '5m',
557
+ staleWhileRevalidate: '1h',
558
+ tags: (params) => [\`${typeName.toLowerCase()}:\${params.id}\`],
559
+ },
560
+ }
561
+ );
562
+ `;
563
+ await promises.mkdir(path2.dirname(outputPath), { recursive: true });
564
+ await promises.writeFile(outputPath, content, "utf-8");
565
+ }
566
+ async function generateRuntimeNegotiator(contract, outputPath) {
567
+ const { definition } = contract;
568
+ const typeName = definition.name;
569
+ const content = `/**
570
+ * Auto-generated runtime negotiator for ${typeName} contract
571
+ * DO NOT EDIT - This file is generated by @reactive-contracts/compiler
572
+ */
573
+
574
+ import type { Contract } from '@reactive-contracts/core';
575
+
576
+ /**
577
+ * Runtime negotiator for ${typeName}
578
+ * Handles SLA monitoring, fallback logic, and performance tracking
579
+ */
580
+ export class ${typeName}Negotiator {
581
+ private contract: Contract;
582
+ private metrics: {
583
+ executionTime: number[];
584
+ cacheHits: number;
585
+ cacheMisses: number;
586
+ } = {
587
+ executionTime: [],
588
+ cacheHits: 0,
589
+ cacheMisses: 0,
590
+ };
591
+
592
+ constructor(contract: Contract) {
593
+ this.contract = contract;
594
+ }
595
+
596
+ /**
597
+ * Execute contract with SLA monitoring
598
+ */
599
+ async execute<TData>(
600
+ resolver: () => Promise<TData>,
601
+ options?: {
602
+ useCache?: boolean;
603
+ timeout?: number;
604
+ }
605
+ ): Promise<{
606
+ data: TData;
607
+ status: {
608
+ latency: 'normal' | 'degraded' | 'violated';
609
+ freshness: 'fresh' | 'stale' | 'expired';
610
+ availability: 'available' | 'degraded' | 'unavailable';
611
+ };
612
+ metadata: {
613
+ executionTime: number;
614
+ cacheHit: boolean;
615
+ derivedAt: 'client' | 'edge' | 'origin';
616
+ };
617
+ }> {
618
+ const startTime = performance.now();
619
+
620
+ try {
621
+ // Execute resolver
622
+ const data = await resolver();
623
+ const executionTime = performance.now() - startTime;
624
+
625
+ // Track metrics
626
+ this.metrics.executionTime.push(executionTime);
627
+ if (options?.useCache) {
628
+ this.metrics.cacheHits++;
629
+ } else {
630
+ this.metrics.cacheMisses++;
631
+ }
632
+
633
+ // Determine latency status
634
+ const maxLatency = this.getMaxLatency();
635
+ const latencyStatus = this.evaluateLatency(executionTime, maxLatency);
636
+
637
+ return {
638
+ data,
639
+ status: {
640
+ latency: latencyStatus,
641
+ freshness: 'fresh',
642
+ availability: 'available',
643
+ },
644
+ metadata: {
645
+ executionTime,
646
+ cacheHit: options?.useCache ?? false,
647
+ derivedAt: 'origin',
648
+ },
649
+ };
650
+ } catch (error) {
651
+ // Handle fallback based on contract constraints
652
+ throw error;
653
+ }
654
+ }
655
+
656
+ /**
657
+ * Get metrics for monitoring
658
+ */
659
+ getMetrics() {
660
+ const avgExecutionTime =
661
+ this.metrics.executionTime.length > 0
662
+ ? this.metrics.executionTime.reduce((a, b) => a + b, 0) / this.metrics.executionTime.length
663
+ : 0;
664
+
665
+ return {
666
+ averageExecutionTime: avgExecutionTime,
667
+ p95ExecutionTime: this.calculateP95(),
668
+ cacheHitRate:
669
+ this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses) || 0,
670
+ totalExecutions: this.metrics.executionTime.length,
671
+ };
672
+ }
673
+
674
+ private getMaxLatency(): number {
675
+ const latency = this.contract.definition.constraints?.latency?.max;
676
+ if (!latency) return Infinity;
677
+
678
+ // Simple parsing for MVP
679
+ const match = latency.match(/^(\\d+)(ms|s|m)$/);
680
+ if (!match) return Infinity;
681
+
682
+ const value = parseInt(match[1], 10);
683
+ const unit = match[2];
684
+
685
+ switch (unit) {
686
+ case 'ms': return value;
687
+ case 's': return value * 1000;
688
+ case 'm': return value * 60 * 1000;
689
+ default: return Infinity;
690
+ }
691
+ }
692
+
693
+ private evaluateLatency(
694
+ executionTime: number,
695
+ maxLatency: number
696
+ ): 'normal' | 'degraded' | 'violated' {
697
+ if (executionTime <= maxLatency) {
698
+ return 'normal';
699
+ } else if (executionTime <= maxLatency * 1.5) {
700
+ return 'degraded';
701
+ } else {
702
+ return 'violated';
703
+ }
704
+ }
705
+
706
+ private calculateP95(): number {
707
+ if (this.metrics.executionTime.length === 0) return 0;
708
+
709
+ const sorted = [...this.metrics.executionTime].sort((a, b) => a - b);
710
+ const index = Math.floor(sorted.length * 0.95);
711
+ return sorted[index];
712
+ }
713
+ }
714
+
715
+ /**
716
+ * Create a new negotiator instance
717
+ */
718
+ export function create${typeName}Negotiator(contract: Contract): ${typeName}Negotiator {
719
+ return new ${typeName}Negotiator(contract);
720
+ }
721
+ `;
722
+ await promises.mkdir(path2.dirname(outputPath), { recursive: true });
417
723
  await promises.writeFile(outputPath, content, "utf-8");
418
724
  }
419
725
  function generateTypeDefinitions(shape, typeName) {
@@ -465,10 +771,173 @@ ${indentStr}}`;
465
771
  }
466
772
  return "any";
467
773
  }
774
+ var silentLogger = {
775
+ info: () => {
776
+ },
777
+ success: () => {
778
+ },
779
+ warning: () => {
780
+ },
781
+ error: () => {
782
+ },
783
+ verbose: () => {
784
+ }
785
+ };
786
+ var consoleLogger = {
787
+ info: (msg) => console.log("[rcontracts]", msg),
788
+ success: (msg) => console.log("[rcontracts] \u2713", msg),
789
+ warning: (msg) => console.warn("[rcontracts] \u26A0", msg),
790
+ error: (msg) => console.error("[rcontracts] \u2717", msg),
791
+ verbose: (msg) => console.log("[rcontracts]", msg)
792
+ };
793
+ async function parseContractFile(filePath, logger = silentLogger) {
794
+ const contracts = [];
795
+ const errors = [];
796
+ const jiti$1 = jiti.createJiti((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)), {
797
+ interopDefault: true,
798
+ moduleCache: false
799
+ // Disable cache to avoid stale imports
800
+ });
801
+ try {
802
+ const module = await jiti$1.import(filePath);
803
+ const contractExports = Object.entries(module).filter(
804
+ ([key, value]) => key.endsWith("Contract") && typeof value === "object" && value !== null && "_brand" in value && value._brand === "Contract"
805
+ );
806
+ if (contractExports.length === 0) {
807
+ errors.push(`No contract found in ${path2__namespace.basename(filePath)}`);
808
+ return { contracts, errors };
809
+ }
810
+ for (const [exportName, contractObj] of contractExports) {
811
+ contracts.push({
812
+ name: exportName,
813
+ contract: contractObj
814
+ });
815
+ }
816
+ } catch (err) {
817
+ const errorMessage = err instanceof Error ? err.message : String(err);
818
+ errors.push(`Failed to parse ${path2__namespace.basename(filePath)}: ${errorMessage}`);
819
+ logger.error?.(errorMessage);
820
+ }
821
+ return { contracts, errors };
822
+ }
823
+ async function compileContract(contract, config, cwd = process.cwd(), logger = silentLogger) {
824
+ const validation = validateContract(contract);
825
+ const latency = analyzeLatency(contract);
826
+ const generated = {};
827
+ for (const warn of validation.warnings) {
828
+ logger.warning?.(warn);
829
+ }
830
+ if (!validation.valid) {
831
+ for (const err of validation.errors) {
832
+ logger.error?.(err);
833
+ }
834
+ return { contract, validation, latency, generated };
835
+ }
836
+ if (latency.status === "error") {
837
+ logger.error?.(latency.message || "Latency analysis failed");
838
+ if (config.validation?.strictLatency) {
839
+ return { contract, validation, latency, generated };
840
+ }
841
+ } else if (latency.status === "warning") {
842
+ logger.warning?.(latency.message || "Latency constraint may not be met");
843
+ }
844
+ if (latency.suggestions?.length) {
845
+ for (const suggestion of latency.suggestions) {
846
+ logger.info?.(`\u{1F4A1} ${suggestion}`);
847
+ }
848
+ }
849
+ const contractName = contract.definition.name;
850
+ try {
851
+ const frontendPath = path2__namespace.join(cwd, config.output.frontend, `${contractName}.ts`);
852
+ await generateFrontendTypes(contract, frontendPath);
853
+ generated.frontend = frontendPath;
854
+ const backendPath = path2__namespace.join(cwd, config.output.backend, `${contractName}.resolver.ts`);
855
+ await generateBackendResolver(contract, backendPath);
856
+ generated.backend = backendPath;
857
+ const runtimePath = path2__namespace.join(cwd, config.output.runtime, `${contractName}.negotiator.ts`);
858
+ await generateRuntimeNegotiator(contract, runtimePath);
859
+ generated.runtime = runtimePath;
860
+ logger.success?.(`Generated code for ${contractName}`);
861
+ } catch (err) {
862
+ const errorMessage = err instanceof Error ? err.message : String(err);
863
+ logger.error?.(`Failed to generate code for ${contractName}: ${errorMessage}`);
864
+ validation.errors.push(errorMessage);
865
+ validation.valid = false;
866
+ }
867
+ return { contract, validation, latency, generated };
868
+ }
869
+ async function findContractFiles(config, cwd = process.cwd()) {
870
+ return glob.glob(config.contracts, {
871
+ cwd,
872
+ absolute: true
873
+ });
874
+ }
875
+ async function compileAll(options) {
876
+ const { config, cwd = process.cwd(), logger = silentLogger, file } = options;
877
+ const results = [];
878
+ const errors = [];
879
+ const warnings = [];
880
+ let contractFiles;
881
+ if (file) {
882
+ contractFiles = [path2__namespace.isAbsolute(file) ? file : path2__namespace.join(cwd, file)];
883
+ } else {
884
+ contractFiles = await findContractFiles(config, cwd);
885
+ }
886
+ if (contractFiles.length === 0) {
887
+ errors.push(`No contract files found matching pattern: ${config.contracts}`);
888
+ return { success: false, results, errors, warnings };
889
+ }
890
+ logger.info?.(`Found ${contractFiles.length} contract file(s)`);
891
+ for (const filePath of contractFiles) {
892
+ const fileName = path2__namespace.basename(filePath, ".contract.ts");
893
+ logger.verbose?.(`Processing ${fileName}...`);
894
+ const { contracts, errors: parseErrors } = await parseContractFile(filePath, logger);
895
+ if (parseErrors.length > 0) {
896
+ errors.push(...parseErrors);
897
+ continue;
898
+ }
899
+ for (const { name, contract } of contracts) {
900
+ const result = await compileContract(contract, config, cwd, logger);
901
+ results.push(result);
902
+ if (!result.validation.valid) {
903
+ errors.push(`Validation failed for ${name}`);
904
+ }
905
+ warnings.push(...result.validation.warnings);
906
+ }
907
+ }
908
+ const success = errors.length === 0;
909
+ if (success) {
910
+ logger.success?.(`Successfully compiled ${results.length} contract(s)`);
911
+ } else {
912
+ logger.error?.(`Compilation failed with ${errors.length} error(s)`);
913
+ }
914
+ return { success, results, errors, warnings };
915
+ }
916
+ function isContractFile(filePath) {
917
+ return filePath.endsWith(".contract.ts");
918
+ }
919
+ function getGeneratedFilesForContract(contractName, config, cwd = process.cwd()) {
920
+ return {
921
+ frontend: path2__namespace.join(cwd, config.output.frontend, `${contractName}.ts`),
922
+ backend: path2__namespace.join(cwd, config.output.backend, `${contractName}.resolver.ts`),
923
+ runtime: path2__namespace.join(cwd, config.output.runtime, `${contractName}.negotiator.ts`)
924
+ };
925
+ }
468
926
 
469
927
  exports.analyzeLatency = analyzeLatency;
928
+ exports.compileAll = compileAll;
929
+ exports.compileContract = compileContract;
930
+ exports.consoleLogger = consoleLogger;
470
931
  exports.defineConfig = defineConfig;
932
+ exports.findContractFiles = findContractFiles;
933
+ exports.generateBackendResolver = generateBackendResolver;
471
934
  exports.generateFrontendTypes = generateFrontendTypes;
935
+ exports.generateRuntimeNegotiator = generateRuntimeNegotiator;
936
+ exports.getGeneratedFilesForContract = getGeneratedFilesForContract;
937
+ exports.isContractFile = isContractFile;
938
+ exports.loadConfig = loadConfig;
939
+ exports.parseContractFile = parseContractFile;
940
+ exports.silentLogger = silentLogger;
472
941
  exports.validateContract = validateContract;
473
942
  //# sourceMappingURL=index.cjs.map
474
943
  //# sourceMappingURL=index.cjs.map