@memberjunction/metadata-sync 2.88.0 → 2.90.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/README.md CHANGED
@@ -711,11 +711,13 @@ When a field value starts with `@file:`, the tool will:
711
711
  1. Read content from the specified file for push operations
712
712
  2. Write content to the specified file for pull operations
713
713
  3. Track both files for change detection
714
+ 4. **For JSON files**: Automatically process any `@include` directives within them
714
715
 
715
716
  Examples:
716
717
  - `@file:greeting.prompt.md` - File in same directory as JSON
717
718
  - `@file:./shared/common-prompt.md` - Relative path
718
719
  - `@file:../templates/standard-header.md` - Parent directory reference
720
+ - `@file:spec.json` - JSON file with `@include` directives (processed automatically)
719
721
 
720
722
  ### @url: References
721
723
  When a field value starts with `@url:`, the tool will:
@@ -798,7 +800,7 @@ Examples:
798
800
 
799
801
  The `Configuration`, `Tags`, and `Metadata` fields will automatically be converted to JSON strings when pushed to the database, while maintaining their structured format in your source files.
800
802
 
801
- ### {@include} References in Files (NEW)
803
+ ### {@include} References in Files
802
804
  Enable content composition within non-JSON files (like .md, .html, .txt) using JSDoc-style include syntax:
803
805
  - Pattern: `{@include path/to/file.ext}`
804
806
  - Supports relative paths from the containing file
@@ -876,6 +878,250 @@ Benefits:
876
878
  - **Flexibility**: Build complex documents from modular parts
877
879
  - **Validation**: Automatic checking of included file existence and circular references
878
880
 
881
+ ### @include References in JSON Files
882
+
883
+ Enable modular JSON composition by including external JSON files directly into your metadata files. This feature allows you to break large JSON configurations into smaller, reusable components.
884
+
885
+ #### Syntax Options
886
+
887
+ **Object Context - Property Spreading (Default)**
888
+ ```json
889
+ {
890
+ "name": "Parent Record",
891
+ "@include": "child.json",
892
+ "description": "Additional fields"
893
+ }
894
+ ```
895
+ The included file's properties are spread into the parent object.
896
+
897
+ **Multiple Includes with Dot Notation (Eliminates VS Code Warnings)**
898
+ ```json
899
+ {
900
+ "name": "Parent Record",
901
+ "@include.data": "shared/data-fields.json",
902
+ "description": "Middle field",
903
+ "@include.config": "shared/config-fields.json",
904
+ "status": "Active"
905
+ }
906
+ ```
907
+ Use dot notation (`@include.anything`) to include multiple files at different positions in your object. The part after the dot is ignored by the processor but makes each key unique, eliminating VS Code's duplicate key warnings. The includes are processed in the order they appear, allowing precise control over property ordering.
908
+
909
+ **Array Context - Element Insertion**
910
+ ```json
911
+ [
912
+ {"name": "First item"},
913
+ "@include:child.json",
914
+ {"name": "Last item"}
915
+ ]
916
+ ```
917
+ The included file's content is inserted as array element(s).
918
+
919
+ **Explicit Mode Control**
920
+ ```json
921
+ {
922
+ "@include": {
923
+ "file": "child.json",
924
+ "mode": "spread" // or "element"
925
+ }
926
+ }
927
+ ```
928
+
929
+ #### Modes Explained
930
+
931
+ **"spread" mode**:
932
+ - Merges all properties from the included file into the parent object
933
+ - Only works when including an object into an object
934
+ - Parent properties override child properties if there are conflicts
935
+ - Default mode for objects
936
+
937
+ **"element" mode**:
938
+ - Directly inserts the JSON content at that position
939
+ - Works with any JSON type (object, array, string, number, etc.)
940
+ - Replaces the @include directive with the actual content
941
+ - Default mode for arrays when using string syntax
942
+
943
+ #### Path Resolution
944
+ - All paths are relative to the file containing the @include
945
+ - Supports: `"child.json"`, `"./child.json"`, `"../shared/base.json"`, `"subfolder/config.json"`
946
+ - Circular references are detected and prevented
947
+
948
+ #### Complex Example
949
+
950
+ Directory structure:
951
+ ```
952
+ metadata/
953
+ ├── components/
954
+ │ ├── dashboard.json
955
+ │ ├── base-props.json
956
+ │ └── items/
957
+ │ └── dashboard-items.json
958
+ └── shared/
959
+ └── common-settings.json
960
+ ```
961
+
962
+ dashboard.json:
963
+ ```json
964
+ {
965
+ "fields": {
966
+ "Name": "Analytics Dashboard",
967
+ "@include.common": "../shared/common-settings.json",
968
+ "Type": "Dashboard",
969
+ "@include.defaults": "../shared/default-values.json",
970
+ "Configuration": {
971
+ "@include": {"file": "./base-props.json", "mode": "element"}
972
+ }
973
+ },
974
+ "relatedEntities": {
975
+ "Dashboard Items": [
976
+ "@include:./items/dashboard-items.json"
977
+ ]
978
+ }
979
+ }
980
+ ```
981
+
982
+ common-settings.json:
983
+ ```json
984
+ {
985
+ "CategoryID": "@lookup:Categories.Name=Analytics",
986
+ "Status": "Active",
987
+ "Priority": 1
988
+ }
989
+ ```
990
+
991
+ base-props.json:
992
+ ```json
993
+ {
994
+ "refreshInterval": 60,
995
+ "theme": "dark",
996
+ "layout": "grid"
997
+ }
998
+ ```
999
+
1000
+ dashboard-items.json:
1001
+ ```json
1002
+ [
1003
+ {"name": "Revenue Chart", "type": "chart"},
1004
+ {"name": "User Stats", "type": "stats"},
1005
+ {"name": "Activity Feed", "type": "feed"}
1006
+ ]
1007
+ ```
1008
+
1009
+ Result after processing:
1010
+ ```json
1011
+ {
1012
+ "fields": {
1013
+ "Name": "Analytics Dashboard",
1014
+ "CategoryID": "@lookup:Categories.Name=Analytics",
1015
+ "Status": "Active",
1016
+ "Priority": 1,
1017
+ "Type": "Dashboard",
1018
+ "Configuration": {
1019
+ "refreshInterval": 60,
1020
+ "theme": "dark",
1021
+ "layout": "grid"
1022
+ }
1023
+ },
1024
+ "relatedEntities": {
1025
+ "Dashboard Items": [
1026
+ {"name": "Revenue Chart", "type": "chart"},
1027
+ {"name": "User Stats", "type": "stats"},
1028
+ {"name": "Activity Feed", "type": "feed"}
1029
+ ]
1030
+ }
1031
+ }
1032
+ ```
1033
+
1034
+ #### Use Cases
1035
+ - **Shared Configurations**: Reuse common settings across multiple entities
1036
+ - **Modular Records**: Build complex records from smaller components
1037
+ - **Template Libraries**: Create libraries of reusable JSON fragments
1038
+ - **Environment Configs**: Include environment-specific settings
1039
+ - **Large Data Sets**: Break up large JSON files for better maintainability
1040
+ - **VS Code Compatibility**: Use dot notation to avoid duplicate key warnings when including multiple files
1041
+
1042
+ #### Practical Example: Component with Multiple Includes
1043
+ ```json
1044
+ {
1045
+ "name": "DashboardComponent",
1046
+ "type": "dashboard",
1047
+ "@include.dataRequirements": "../shared/data-requirements.json",
1048
+ "functionalRequirements": "Dashboard displays real-time metrics...",
1049
+ "@include.libraries": "../shared/chart-libraries.json",
1050
+ "technicalDesign": "Component uses React hooks for state...",
1051
+ "@include.eventHandlers": "../shared/event-handlers.json",
1052
+ "code": "const Dashboard = () => { ... }"
1053
+ }
1054
+ ```
1055
+ In this example, data requirements, libraries, and event handlers are spread into the component definition at their specific positions, maintaining a logical property order while avoiding VS Code warnings about duplicate `@include` keys.
1056
+
1057
+ #### @include in Referenced JSON Files
1058
+ When using `@file:` to reference a JSON file, any `@include` directives within that JSON file are automatically processed:
1059
+
1060
+ #### @file References in Included JSON Files
1061
+ The system now automatically resolves `@file` references found within JSON files that are pulled in via `@include`. This allows for complete nesting of references:
1062
+
1063
+ ```json
1064
+ // main-entity.json
1065
+ {
1066
+ "fields": {
1067
+ "Name": "MyComponent",
1068
+ "Specification": "@file:files/component-spec.json"
1069
+ }
1070
+ }
1071
+
1072
+ // files/component-spec.json
1073
+ {
1074
+ "name": "ComponentSpec",
1075
+ "@include.base": "../shared/base-spec.json",
1076
+ "customFields": {
1077
+ "feature": "advanced"
1078
+ },
1079
+ "@include.libs": "../shared/libraries.json"
1080
+ }
1081
+ ```
1082
+
1083
+ The `component-spec.json` file's `@include` directives are processed before the content is returned to the `Specification` field, ensuring all includes are resolved.
1084
+
1085
+ #### Nested @file References in JSON Files
1086
+ The system now recursively processes `@file` references within JSON files loaded via `@file`. This enables powerful composition patterns:
1087
+
1088
+ ```json
1089
+ // components.json
1090
+ {
1091
+ "fields": {
1092
+ "Name": "RecentDealsList",
1093
+ "Specification": "@file:spec/recent-deals-list.spec.json"
1094
+ }
1095
+ }
1096
+
1097
+ // spec/recent-deals-list.spec.json
1098
+ {
1099
+ "name": "RecentDealsList",
1100
+ "description": "List of recent deals",
1101
+ "code": "@file:../code/recent-deals-list.js", // This nested @file is now resolved!
1102
+ "style": "@file:../styles/deals.css",
1103
+ "config": {
1104
+ "template": "@file:../templates/deal-row.html" // Even deeply nested @file references work
1105
+ }
1106
+ }
1107
+ ```
1108
+
1109
+ All `@file` references are recursively resolved, regardless of nesting depth. The final result will have all file contents properly loaded and embedded.
1110
+
1111
+ #### Processing Order
1112
+ 1. @include directives are processed first (recursively)
1113
+ 2. @file references are recursively resolved (including nested ones in JSON)
1114
+ 3. Then @template references
1115
+ 4. Finally, other @ references (@lookup, etc.)
1116
+
1117
+ This ensures that included content can contain other special references that will be properly resolved.
1118
+
1119
+ **New Feature**: @file references within @included JSON files are now automatically resolved. This means you can have:
1120
+ - A main JSON file with `@include` directives
1121
+ - The included JSON files can have `@file` references to load code, templates, etc.
1122
+ - Those @file references are resolved to their actual content
1123
+ - If the @file points to a JSON file with @include directives, those are also processed
1124
+
879
1125
  ### @template: References (NEW)
880
1126
  Enable JSON template composition for reusable configurations:
881
1127
 
@@ -982,6 +1228,9 @@ mj sync push --verbose
982
1228
  # Dry run to see what would change
983
1229
  mj sync push --dry-run
984
1230
 
1231
+ # Push with parallel processing (NEW)
1232
+ mj sync push --parallel-batch-size=20 # Process 20 records in parallel (default: 10, max: 50)
1233
+
985
1234
  # Show status of local vs database
986
1235
  mj sync status
987
1236
 
@@ -1008,6 +1257,56 @@ Configuration follows a hierarchical structure:
1008
1257
  - **Entity configs**: Each entity directory has its own config defining the entity type
1009
1258
  - **Inheritance**: All files within an entity directory are treated as records of that entity type
1010
1259
 
1260
+ ### Parallel Processing (NEW)
1261
+
1262
+ MetadataSync now supports parallel processing of records during push operations, significantly improving performance for large datasets.
1263
+
1264
+ #### How It Works
1265
+
1266
+ Records are automatically grouped into dependency levels:
1267
+ - **Level 0**: Records with no dependencies
1268
+ - **Level 1**: Records that depend only on Level 0 records
1269
+ - **Level 2**: Records that depend on Level 0 or Level 1 records
1270
+ - And so on...
1271
+
1272
+ Records within the same dependency level can be safely processed in parallel since they have no dependencies on each other.
1273
+
1274
+ #### Configuration
1275
+
1276
+ Use the `--parallel-batch-size` flag to control parallelism:
1277
+
1278
+ ```bash
1279
+ # Default: 10 records in parallel
1280
+ mj sync push
1281
+
1282
+ # Process 20 records in parallel
1283
+ mj sync push --parallel-batch-size=20
1284
+
1285
+ # Maximum parallelism (50 records)
1286
+ mj sync push --parallel-batch-size=50
1287
+
1288
+ # Conservative approach for debugging
1289
+ mj sync push --parallel-batch-size=1
1290
+ ```
1291
+
1292
+ #### Performance Benefits
1293
+
1294
+ - **2-3x faster** for typical metadata pushes
1295
+ - **5-10x faster** for records with many file references (@file) or lookups (@lookup)
1296
+ - Most beneficial when processing large numbers of independent records
1297
+
1298
+ #### When to Use
1299
+
1300
+ **Recommended for:**
1301
+ - Large initial data imports
1302
+ - Bulk metadata updates
1303
+ - CI/CD pipelines with time constraints
1304
+
1305
+ **Use conservative settings for:**
1306
+ - Debugging sync issues
1307
+ - Working with complex dependencies
1308
+ - Limited database connection pools
1309
+
1011
1310
  ### Directory Processing Order (NEW)
1012
1311
 
1013
1312
  The MetadataSync tool now supports custom directory processing order to handle dependencies between entity types. This feature ensures that dependent entities are processed in the correct order.
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Include directive configuration
3
+ */
4
+ export interface IncludeDirective {
5
+ file: string;
6
+ mode?: 'spread' | 'element';
7
+ }
8
+ /**
9
+ * Preprocesses JSON files to handle @include directives
10
+ * Supports including external JSON files with spreading or element insertion
11
+ */
12
+ export declare class JsonPreprocessor {
13
+ private visitedPaths;
14
+ /**
15
+ * Process a JSON file and resolve all @include directives
16
+ * @param filePath - Path to the JSON file to process
17
+ * @returns The processed JSON data with all includes resolved
18
+ */
19
+ processFile(filePath: string): Promise<any>;
20
+ /**
21
+ * Recursively process @include directives in JSON data
22
+ * @param data - The JSON data to process
23
+ * @param currentFilePath - Path of the current file for resolving relative paths
24
+ * @returns The processed data with includes resolved
25
+ */
26
+ private processIncludesInternal;
27
+ /**
28
+ * Process an array, handling @include directives
29
+ * @param arr - The array to process
30
+ * @param currentFilePath - Current file path for resolving relative paths
31
+ * @returns Processed array with includes resolved
32
+ */
33
+ private processArray;
34
+ /**
35
+ * Process an object, handling @include directives
36
+ * @param obj - The object to process
37
+ * @param currentFilePath - Current file path for resolving relative paths
38
+ * @returns Processed object with includes resolved
39
+ */
40
+ private processObject;
41
+ /**
42
+ * Load and process an included file
43
+ * @param filePath - Path to the file to include
44
+ * @returns The processed content of the included file
45
+ */
46
+ private loadAndProcessInclude;
47
+ /**
48
+ * Load file content and process it if it's JSON with @include directives
49
+ * @param filePath - Path to the file to load
50
+ * @returns The file content (processed if JSON with @includes)
51
+ */
52
+ private loadFileContent;
53
+ /**
54
+ * Resolve a potentially relative path to an absolute path
55
+ * @param includePath - The path specified in the @include
56
+ * @param currentFilePath - The current file's path
57
+ * @returns Absolute path to the included file
58
+ */
59
+ private resolvePath;
60
+ /**
61
+ * Process JSON data that's already loaded (for integration with existing code)
62
+ * @param data - The JSON data to process
63
+ * @param filePath - The file path (for resolving relative includes)
64
+ * @returns Processed data with includes resolved
65
+ */
66
+ processJsonData(data: any, filePath: string): Promise<any>;
67
+ }
@@ -0,0 +1,241 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.JsonPreprocessor = void 0;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ /**
10
+ * Preprocesses JSON files to handle @include directives
11
+ * Supports including external JSON files with spreading or element insertion
12
+ */
13
+ class JsonPreprocessor {
14
+ visitedPaths = new Set();
15
+ /**
16
+ * Process a JSON file and resolve all @include directives
17
+ * @param filePath - Path to the JSON file to process
18
+ * @returns The processed JSON data with all includes resolved
19
+ */
20
+ async processFile(filePath) {
21
+ this.visitedPaths.clear();
22
+ const fileContent = await fs_extra_1.default.readJson(filePath);
23
+ return this.processIncludesInternal(fileContent, filePath);
24
+ }
25
+ /**
26
+ * Recursively process @include directives in JSON data
27
+ * @param data - The JSON data to process
28
+ * @param currentFilePath - Path of the current file for resolving relative paths
29
+ * @returns The processed data with includes resolved
30
+ */
31
+ async processIncludesInternal(data, currentFilePath) {
32
+ // Process based on data type
33
+ if (Array.isArray(data)) {
34
+ return this.processArray(data, currentFilePath);
35
+ }
36
+ else if (data && typeof data === 'object') {
37
+ return this.processObject(data, currentFilePath);
38
+ }
39
+ else {
40
+ return data;
41
+ }
42
+ }
43
+ /**
44
+ * Process an array, handling @include directives
45
+ * @param arr - The array to process
46
+ * @param currentFilePath - Current file path for resolving relative paths
47
+ * @returns Processed array with includes resolved
48
+ */
49
+ async processArray(arr, currentFilePath) {
50
+ const result = [];
51
+ for (const item of arr) {
52
+ // Check for string-based include in array (default element mode)
53
+ if (typeof item === 'string' && item.startsWith('@include:')) {
54
+ const includePath = item.substring(9).trim();
55
+ const resolvedPath = this.resolvePath(includePath, currentFilePath);
56
+ const includedContent = await this.loadAndProcessInclude(resolvedPath);
57
+ // If included content is an array, spread its elements
58
+ if (Array.isArray(includedContent)) {
59
+ result.push(...includedContent);
60
+ }
61
+ else {
62
+ // Otherwise add as single element
63
+ result.push(includedContent);
64
+ }
65
+ }
66
+ else if (item && typeof item === 'object') {
67
+ // Process nested objects/arrays
68
+ result.push(await this.processIncludesInternal(item, currentFilePath));
69
+ }
70
+ else {
71
+ result.push(item);
72
+ }
73
+ }
74
+ return result;
75
+ }
76
+ /**
77
+ * Process an object, handling @include directives
78
+ * @param obj - The object to process
79
+ * @param currentFilePath - Current file path for resolving relative paths
80
+ * @returns Processed object with includes resolved
81
+ */
82
+ async processObject(obj, currentFilePath) {
83
+ const result = {};
84
+ const includeKeys = [];
85
+ const includeDirectives = new Map();
86
+ // First pass: identify all @include keys (both @include and @include.*)
87
+ for (const key of Object.keys(obj)) {
88
+ if (key === '@include' || key.startsWith('@include.')) {
89
+ includeKeys.push(key);
90
+ const includeValue = obj[key];
91
+ if (typeof includeValue === 'string') {
92
+ // Simple string include - default to spread mode in objects
93
+ includeDirectives.set(key, { file: includeValue, mode: 'spread' });
94
+ }
95
+ else if (includeValue && typeof includeValue === 'object') {
96
+ // Explicit include configuration
97
+ const directive = includeValue;
98
+ // Default to spread mode if not specified
99
+ if (!directive.mode) {
100
+ directive.mode = 'spread';
101
+ }
102
+ includeDirectives.set(key, directive);
103
+ }
104
+ }
105
+ }
106
+ // Second pass: process all properties in order, spreading includes when encountered
107
+ for (const [key, value] of Object.entries(obj)) {
108
+ if (key === '@include' || key.startsWith('@include.')) {
109
+ // Process this include directive
110
+ const includeDirective = includeDirectives.get(key);
111
+ if (includeDirective) {
112
+ const resolvedPath = this.resolvePath(includeDirective.file, currentFilePath);
113
+ const includedContent = await this.loadAndProcessInclude(resolvedPath);
114
+ if (includeDirective.mode === 'spread') {
115
+ // Spread mode: merge included object properties at this position
116
+ if (includedContent && typeof includedContent === 'object' && !Array.isArray(includedContent)) {
117
+ Object.assign(result, includedContent);
118
+ }
119
+ else {
120
+ throw new Error(`Cannot spread non-object content from ${includeDirective.file}. Use mode: "element" for non-object includes.`);
121
+ }
122
+ }
123
+ else if (includeDirective.mode === 'element') {
124
+ // Element mode: directly insert the content
125
+ // For dot notation includes, we can't replace the whole object,
126
+ // so we'll add it as a property instead (though this is unusual)
127
+ if (key.includes('.')) {
128
+ // Extract the part after the dot to use as property name
129
+ const propName = key.split('.').slice(1).join('.');
130
+ result[propName] = includedContent;
131
+ }
132
+ else {
133
+ // For plain @include with element mode, replace the entire object
134
+ return includedContent;
135
+ }
136
+ }
137
+ }
138
+ }
139
+ else {
140
+ // Regular property - process recursively and handle @file references
141
+ if (typeof value === 'string' && value.startsWith('@file:')) {
142
+ // Process @file reference
143
+ const filePath = value.substring(6); // Remove '@file:' prefix
144
+ const resolvedPath = this.resolvePath(filePath, currentFilePath);
145
+ result[key] = await this.loadFileContent(resolvedPath);
146
+ }
147
+ else if (value && typeof value === 'object') {
148
+ result[key] = await this.processIncludesInternal(value, currentFilePath);
149
+ }
150
+ else {
151
+ result[key] = value;
152
+ }
153
+ }
154
+ }
155
+ return result;
156
+ }
157
+ /**
158
+ * Load and process an included file
159
+ * @param filePath - Path to the file to include
160
+ * @returns The processed content of the included file
161
+ */
162
+ async loadAndProcessInclude(filePath) {
163
+ const absolutePath = path_1.default.resolve(filePath);
164
+ // Check if this file is already being processed (circular reference)
165
+ if (this.visitedPaths.has(absolutePath)) {
166
+ throw new Error(`Circular reference detected: ${absolutePath} is already being processed`);
167
+ }
168
+ if (!await fs_extra_1.default.pathExists(filePath)) {
169
+ throw new Error(`Include file not found: ${filePath}`);
170
+ }
171
+ // Add to visited paths before processing
172
+ this.visitedPaths.add(absolutePath);
173
+ try {
174
+ const content = await fs_extra_1.default.readJson(filePath);
175
+ // Process the content (visited tracking is handled in this method)
176
+ return this.processIncludesInternal(content, filePath);
177
+ }
178
+ finally {
179
+ // Remove from visited paths after processing
180
+ this.visitedPaths.delete(absolutePath);
181
+ }
182
+ }
183
+ /**
184
+ * Load file content and process it if it's JSON with @include directives
185
+ * @param filePath - Path to the file to load
186
+ * @returns The file content (processed if JSON with @includes)
187
+ */
188
+ async loadFileContent(filePath) {
189
+ if (!await fs_extra_1.default.pathExists(filePath)) {
190
+ // Return the original @file reference if file doesn't exist
191
+ // This matches the behavior of SyncEngine.processFieldValue
192
+ return `@file:${path_1.default.relative(path_1.default.dirname(filePath), filePath)}`;
193
+ }
194
+ try {
195
+ if (filePath.endsWith('.json')) {
196
+ // For JSON files, load and check for @include directives
197
+ const jsonContent = await fs_extra_1.default.readJson(filePath);
198
+ const jsonString = JSON.stringify(jsonContent);
199
+ const hasIncludes = jsonString.includes('"@include"') || jsonString.includes('"@include.');
200
+ if (hasIncludes) {
201
+ // Process @include directives in the JSON file
202
+ return await this.processIncludesInternal(jsonContent, filePath);
203
+ }
204
+ else {
205
+ // Return the JSON content as-is
206
+ return jsonContent;
207
+ }
208
+ }
209
+ else {
210
+ // For non-JSON files, return the text content
211
+ return await fs_extra_1.default.readFile(filePath, 'utf-8');
212
+ }
213
+ }
214
+ catch (error) {
215
+ // On error, return the original @file reference
216
+ return `@file:${path_1.default.relative(path_1.default.dirname(filePath), filePath)}`;
217
+ }
218
+ }
219
+ /**
220
+ * Resolve a potentially relative path to an absolute path
221
+ * @param includePath - The path specified in the @include
222
+ * @param currentFilePath - The current file's path
223
+ * @returns Absolute path to the included file
224
+ */
225
+ resolvePath(includePath, currentFilePath) {
226
+ const currentDir = path_1.default.dirname(currentFilePath);
227
+ return path_1.default.resolve(currentDir, includePath);
228
+ }
229
+ /**
230
+ * Process JSON data that's already loaded (for integration with existing code)
231
+ * @param data - The JSON data to process
232
+ * @param filePath - The file path (for resolving relative includes)
233
+ * @returns Processed data with includes resolved
234
+ */
235
+ async processJsonData(data, filePath) {
236
+ this.visitedPaths.clear();
237
+ return this.processIncludesInternal(data, filePath);
238
+ }
239
+ }
240
+ exports.JsonPreprocessor = JsonPreprocessor;
241
+ //# sourceMappingURL=json-preprocessor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-preprocessor.js","sourceRoot":"","sources":["../../src/lib/json-preprocessor.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'fs-extra';\nimport path from 'path';\n\n/**\n * Include directive configuration\n */\nexport interface IncludeDirective {\n file: string;\n mode?: 'spread' | 'element';\n}\n\n/**\n * Preprocesses JSON files to handle @include directives\n * Supports including external JSON files with spreading or element insertion\n */\nexport class JsonPreprocessor {\n private visitedPaths: Set<string> = new Set();\n\n /**\n * Process a JSON file and resolve all @include directives\n * @param filePath - Path to the JSON file to process\n * @returns The processed JSON data with all includes resolved\n */\n async processFile(filePath: string): Promise<any> {\n this.visitedPaths.clear();\n const fileContent = await fs.readJson(filePath);\n return this.processIncludesInternal(fileContent, filePath);\n }\n\n /**\n * Recursively process @include directives in JSON data\n * @param data - The JSON data to process\n * @param currentFilePath - Path of the current file for resolving relative paths\n * @returns The processed data with includes resolved\n */\n private async processIncludesInternal(data: any, currentFilePath: string): Promise<any> {\n // Process based on data type\n if (Array.isArray(data)) {\n return this.processArray(data, currentFilePath);\n } else if (data && typeof data === 'object') {\n return this.processObject(data, currentFilePath);\n } else {\n return data;\n }\n }\n\n /**\n * Process an array, handling @include directives\n * @param arr - The array to process\n * @param currentFilePath - Current file path for resolving relative paths\n * @returns Processed array with includes resolved\n */\n private async processArray(arr: any[], currentFilePath: string): Promise<any[]> {\n const result: any[] = [];\n \n for (const item of arr) {\n // Check for string-based include in array (default element mode)\n if (typeof item === 'string' && item.startsWith('@include:')) {\n const includePath = item.substring(9).trim();\n const resolvedPath = this.resolvePath(includePath, currentFilePath);\n const includedContent = await this.loadAndProcessInclude(resolvedPath);\n \n // If included content is an array, spread its elements\n if (Array.isArray(includedContent)) {\n result.push(...includedContent);\n } else {\n // Otherwise add as single element\n result.push(includedContent);\n }\n } else if (item && typeof item === 'object') {\n // Process nested objects/arrays\n result.push(await this.processIncludesInternal(item, currentFilePath));\n } else {\n result.push(item);\n }\n }\n \n return result;\n }\n\n /**\n * Process an object, handling @include directives\n * @param obj - The object to process\n * @param currentFilePath - Current file path for resolving relative paths\n * @returns Processed object with includes resolved\n */\n private async processObject(obj: any, currentFilePath: string): Promise<any> {\n const result: any = {};\n const includeKeys: string[] = [];\n const includeDirectives: Map<string, IncludeDirective> = new Map();\n \n // First pass: identify all @include keys (both @include and @include.*)\n for (const key of Object.keys(obj)) {\n if (key === '@include' || key.startsWith('@include.')) {\n includeKeys.push(key);\n const includeValue = obj[key];\n \n if (typeof includeValue === 'string') {\n // Simple string include - default to spread mode in objects\n includeDirectives.set(key, { file: includeValue, mode: 'spread' });\n } else if (includeValue && typeof includeValue === 'object') {\n // Explicit include configuration\n const directive = includeValue as IncludeDirective;\n // Default to spread mode if not specified\n if (!directive.mode) {\n directive.mode = 'spread';\n }\n includeDirectives.set(key, directive);\n }\n }\n }\n \n // Second pass: process all properties in order, spreading includes when encountered\n for (const [key, value] of Object.entries(obj)) {\n if (key === '@include' || key.startsWith('@include.')) {\n // Process this include directive\n const includeDirective = includeDirectives.get(key);\n if (includeDirective) {\n const resolvedPath = this.resolvePath(includeDirective.file, currentFilePath);\n const includedContent = await this.loadAndProcessInclude(resolvedPath);\n \n if (includeDirective.mode === 'spread') {\n // Spread mode: merge included object properties at this position\n if (includedContent && typeof includedContent === 'object' && !Array.isArray(includedContent)) {\n Object.assign(result, includedContent);\n } else {\n throw new Error(`Cannot spread non-object content from ${includeDirective.file}. Use mode: \"element\" for non-object includes.`);\n }\n } else if (includeDirective.mode === 'element') {\n // Element mode: directly insert the content\n // For dot notation includes, we can't replace the whole object,\n // so we'll add it as a property instead (though this is unusual)\n if (key.includes('.')) {\n // Extract the part after the dot to use as property name\n const propName = key.split('.').slice(1).join('.');\n result[propName] = includedContent;\n } else {\n // For plain @include with element mode, replace the entire object\n return includedContent;\n }\n }\n }\n } else {\n // Regular property - process recursively and handle @file references\n if (typeof value === 'string' && value.startsWith('@file:')) {\n // Process @file reference\n const filePath = value.substring(6); // Remove '@file:' prefix\n const resolvedPath = this.resolvePath(filePath, currentFilePath);\n result[key] = await this.loadFileContent(resolvedPath);\n } else if (value && typeof value === 'object') {\n result[key] = await this.processIncludesInternal(value, currentFilePath);\n } else {\n result[key] = value;\n }\n }\n }\n \n return result;\n }\n\n /**\n * Load and process an included file\n * @param filePath - Path to the file to include\n * @returns The processed content of the included file\n */\n private async loadAndProcessInclude(filePath: string): Promise<any> {\n const absolutePath = path.resolve(filePath);\n \n // Check if this file is already being processed (circular reference)\n if (this.visitedPaths.has(absolutePath)) {\n throw new Error(`Circular reference detected: ${absolutePath} is already being processed`);\n }\n \n if (!await fs.pathExists(filePath)) {\n throw new Error(`Include file not found: ${filePath}`);\n }\n \n // Add to visited paths before processing\n this.visitedPaths.add(absolutePath);\n \n try {\n const content = await fs.readJson(filePath);\n // Process the content (visited tracking is handled in this method)\n return this.processIncludesInternal(content, filePath);\n } finally {\n // Remove from visited paths after processing\n this.visitedPaths.delete(absolutePath);\n }\n }\n\n /**\n * Load file content and process it if it's JSON with @include directives\n * @param filePath - Path to the file to load\n * @returns The file content (processed if JSON with @includes)\n */\n private async loadFileContent(filePath: string): Promise<any> {\n if (!await fs.pathExists(filePath)) {\n // Return the original @file reference if file doesn't exist\n // This matches the behavior of SyncEngine.processFieldValue\n return `@file:${path.relative(path.dirname(filePath), filePath)}`;\n }\n\n try {\n if (filePath.endsWith('.json')) {\n // For JSON files, load and check for @include directives\n const jsonContent = await fs.readJson(filePath);\n const jsonString = JSON.stringify(jsonContent);\n const hasIncludes = jsonString.includes('\"@include\"') || jsonString.includes('\"@include.');\n \n if (hasIncludes) {\n // Process @include directives in the JSON file\n return await this.processIncludesInternal(jsonContent, filePath);\n } else {\n // Return the JSON content as-is\n return jsonContent;\n }\n } else {\n // For non-JSON files, return the text content\n return await fs.readFile(filePath, 'utf-8');\n }\n } catch (error) {\n // On error, return the original @file reference\n return `@file:${path.relative(path.dirname(filePath), filePath)}`;\n }\n }\n\n /**\n * Resolve a potentially relative path to an absolute path\n * @param includePath - The path specified in the @include\n * @param currentFilePath - The current file's path\n * @returns Absolute path to the included file\n */\n private resolvePath(includePath: string, currentFilePath: string): string {\n const currentDir = path.dirname(currentFilePath);\n return path.resolve(currentDir, includePath);\n }\n\n /**\n * Process JSON data that's already loaded (for integration with existing code)\n * @param data - The JSON data to process\n * @param filePath - The file path (for resolving relative includes)\n * @returns Processed data with includes resolved\n */\n async processJsonData(data: any, filePath: string): Promise<any> {\n this.visitedPaths.clear();\n return this.processIncludesInternal(data, filePath);\n }\n}"]}