@memberjunction/metadata-sync 2.89.0 → 2.91.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 +300 -1
- package/dist/lib/json-preprocessor.d.ts +67 -0
- package/dist/lib/json-preprocessor.js +241 -0
- package/dist/lib/json-preprocessor.js.map +1 -0
- package/dist/lib/record-dependency-analyzer.d.ts +6 -0
- package/dist/lib/record-dependency-analyzer.js +32 -1
- package/dist/lib/record-dependency-analyzer.js.map +1 -1
- package/dist/lib/sync-engine.d.ts +17 -0
- package/dist/lib/sync-engine.js +131 -6
- package/dist/lib/sync-engine.js.map +1 -1
- package/dist/services/PushService.d.ts +1 -0
- package/dist/services/PushService.js +136 -19
- package/dist/services/PushService.js.map +1 -1
- package/dist/services/ValidationService.d.ts +8 -0
- package/dist/services/ValidationService.js +86 -6
- package/dist/services/ValidationService.js.map +1 -1
- package/package.json +7 -7
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
|
|
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}"]}
|