@aspruyt/xfg 5.3.0 → 5.4.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.
|
@@ -132,6 +132,23 @@ export function resolveFileReferencesInConfig(raw, options) {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
|
+
// Resolve conditional group file content
|
|
136
|
+
if (result.conditionalGroups) {
|
|
137
|
+
for (const cg of result.conditionalGroups) {
|
|
138
|
+
if (cg.files) {
|
|
139
|
+
for (const [fileName, fileConfig] of Object.entries(cg.files)) {
|
|
140
|
+
if (fileConfig &&
|
|
141
|
+
typeof fileConfig === "object" &&
|
|
142
|
+
"content" in fileConfig) {
|
|
143
|
+
const resolved = resolveContentValue(fileConfig.content, configDir);
|
|
144
|
+
if (resolved !== undefined) {
|
|
145
|
+
cg.files[fileName] = { ...fileConfig, content: resolved };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
135
152
|
// Resolve per-repo file content
|
|
136
153
|
if (result.repos) {
|
|
137
154
|
for (const repo of result.repos) {
|
package/dist/config/merge.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export declare function deepMerge(base: Record<string, unknown>, overlay: Record
|
|
|
18
18
|
* Strip xfg merge directive keys ($arrayMerge, $values) from an object.
|
|
19
19
|
* Works recursively on nested objects and arrays.
|
|
20
20
|
* Standard $-prefixed keys ($schema, $id, $ref, etc.) are preserved.
|
|
21
|
+
*
|
|
22
|
+
* When an unresolved directive object is found (only contains $arrayMerge + $values),
|
|
23
|
+
* it is replaced with the $values array. This handles the case where a directive
|
|
24
|
+
* had no base array to merge with.
|
|
21
25
|
*/
|
|
22
26
|
export declare function stripMergeDirectives(obj: Record<string, unknown>): Record<string, unknown>;
|
|
23
27
|
export declare function createMergeContext(defaultStrategy?: ArrayMergeStrategy): MergeContext;
|
package/dist/config/merge.js
CHANGED
|
@@ -14,6 +14,20 @@ const arrayMergeStrategies = new Map([
|
|
|
14
14
|
["append", (base, overlay) => [...base, ...overlay]],
|
|
15
15
|
["prepend", (base, overlay) => [...overlay, ...base]],
|
|
16
16
|
]);
|
|
17
|
+
/**
|
|
18
|
+
* Checks if a value is an unresolved $arrayMerge directive object
|
|
19
|
+
* (only contains $arrayMerge + $values keys, with a valid strategy and array values).
|
|
20
|
+
*/
|
|
21
|
+
function isUnresolvedDirective(value) {
|
|
22
|
+
if (!isPlainObject(value))
|
|
23
|
+
return false;
|
|
24
|
+
const keys = Object.keys(value);
|
|
25
|
+
return (keys.length === 2 &&
|
|
26
|
+
keys.every((k) => XFG_DIRECTIVES.has(k)) &&
|
|
27
|
+
typeof value.$arrayMerge === "string" &&
|
|
28
|
+
arrayMergeStrategies.has(value.$arrayMerge) &&
|
|
29
|
+
Array.isArray(value.$values));
|
|
30
|
+
}
|
|
17
31
|
function mergeArrays(base, overlay, strategy) {
|
|
18
32
|
const handler = arrayMergeStrategies.get(strategy);
|
|
19
33
|
if (handler) {
|
|
@@ -36,6 +50,11 @@ export function deepMerge(base, overlay, ctx) {
|
|
|
36
50
|
if (XFG_DIRECTIVES.has(key))
|
|
37
51
|
continue;
|
|
38
52
|
const baseValue = base[key];
|
|
53
|
+
// If base is an unresolved directive (from a previous layer with no base array),
|
|
54
|
+
// resolve it to its $values array before proceeding with merge logic.
|
|
55
|
+
const resolvedBase = isUnresolvedDirective(baseValue)
|
|
56
|
+
? baseValue.$values
|
|
57
|
+
: baseValue;
|
|
39
58
|
// Per-field $arrayMerge + $values directive
|
|
40
59
|
if (isPlainObject(overlayValue) && "$arrayMerge" in overlayValue) {
|
|
41
60
|
const strategy = overlayValue.$arrayMerge;
|
|
@@ -44,19 +63,19 @@ export function deepMerge(base, overlay, ctx) {
|
|
|
44
63
|
strategy === "append" ||
|
|
45
64
|
strategy === "prepend") &&
|
|
46
65
|
Array.isArray(values) &&
|
|
47
|
-
Array.isArray(
|
|
48
|
-
result[key] = mergeArrays(
|
|
66
|
+
Array.isArray(resolvedBase)) {
|
|
67
|
+
result[key] = mergeArrays(resolvedBase, values, strategy);
|
|
49
68
|
continue;
|
|
50
69
|
}
|
|
51
70
|
}
|
|
52
71
|
// Both are arrays — use default strategy
|
|
53
|
-
if (Array.isArray(
|
|
54
|
-
result[key] = mergeArrays(
|
|
72
|
+
if (Array.isArray(resolvedBase) && Array.isArray(overlayValue)) {
|
|
73
|
+
result[key] = mergeArrays(resolvedBase, overlayValue, ctx.defaultArrayStrategy);
|
|
55
74
|
continue;
|
|
56
75
|
}
|
|
57
76
|
// Both are plain objects — recurse
|
|
58
|
-
if (isPlainObject(
|
|
59
|
-
result[key] = deepMerge(
|
|
77
|
+
if (isPlainObject(resolvedBase) && isPlainObject(overlayValue)) {
|
|
78
|
+
result[key] = deepMerge(resolvedBase, overlayValue, ctx);
|
|
60
79
|
continue;
|
|
61
80
|
}
|
|
62
81
|
// Otherwise, overlay wins (including null values)
|
|
@@ -68,6 +87,10 @@ export function deepMerge(base, overlay, ctx) {
|
|
|
68
87
|
* Strip xfg merge directive keys ($arrayMerge, $values) from an object.
|
|
69
88
|
* Works recursively on nested objects and arrays.
|
|
70
89
|
* Standard $-prefixed keys ($schema, $id, $ref, etc.) are preserved.
|
|
90
|
+
*
|
|
91
|
+
* When an unresolved directive object is found (only contains $arrayMerge + $values),
|
|
92
|
+
* it is replaced with the $values array. This handles the case where a directive
|
|
93
|
+
* had no base array to merge with.
|
|
71
94
|
*/
|
|
72
95
|
export function stripMergeDirectives(obj) {
|
|
73
96
|
const result = {};
|
|
@@ -76,7 +99,13 @@ export function stripMergeDirectives(obj) {
|
|
|
76
99
|
if (XFG_DIRECTIVES.has(key))
|
|
77
100
|
continue;
|
|
78
101
|
if (isPlainObject(value)) {
|
|
79
|
-
|
|
102
|
+
if (isUnresolvedDirective(value)) {
|
|
103
|
+
// Resolve to the $values array, stripping directives from items
|
|
104
|
+
result[key] = value.$values.map((item) => isPlainObject(item) ? stripMergeDirectives(item) : item);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
result[key] = stripMergeDirectives(value);
|
|
108
|
+
}
|
|
80
109
|
}
|
|
81
110
|
else if (Array.isArray(value)) {
|
|
82
111
|
result[key] = value.map((item) => isPlainObject(item) ? stripMergeDirectives(item) : item);
|
|
@@ -170,7 +170,8 @@ export function mergeSettings(root, perRepo) {
|
|
|
170
170
|
if (!inheritRulesets && !repoRuleset && rootRuleset) {
|
|
171
171
|
continue;
|
|
172
172
|
}
|
|
173
|
-
|
|
173
|
+
const merged = mergeRuleset(rootRuleset, repoRuleset);
|
|
174
|
+
result.rulesets[name] = stripMergeDirectives(merged);
|
|
174
175
|
}
|
|
175
176
|
// Clean up empty rulesets object
|
|
176
177
|
if (Object.keys(result.rulesets).length === 0) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ValidationError } from "../../shared/errors.js";
|
|
2
|
+
import { isPlainObject } from "../../shared/type-guards.js";
|
|
2
3
|
/** Compile-time validates an array matches a type union, while keeping string[] runtime type for .includes() */
|
|
3
4
|
function validValues(values) {
|
|
4
5
|
return values;
|
|
@@ -62,6 +63,31 @@ const VALID_RULE_TYPES = validValues([
|
|
|
62
63
|
"max_file_path_length",
|
|
63
64
|
"max_file_size",
|
|
64
65
|
]);
|
|
66
|
+
// Intentionally duplicated from merge.ts — validator should not depend on merge internals
|
|
67
|
+
const VALID_MERGE_STRATEGIES = ["replace", "append", "prepend"];
|
|
68
|
+
/**
|
|
69
|
+
* Checks if a value is an $arrayMerge directive: { $arrayMerge: strategy, $values: [...] }
|
|
70
|
+
*/
|
|
71
|
+
function isArrayMergeDirective(value) {
|
|
72
|
+
if (!isPlainObject(value))
|
|
73
|
+
return false;
|
|
74
|
+
const keys = Object.keys(value);
|
|
75
|
+
return (keys.length === 2 &&
|
|
76
|
+
keys.every((k) => k === "$arrayMerge" || k === "$values") &&
|
|
77
|
+
VALID_MERGE_STRATEGIES.includes(value.$arrayMerge) &&
|
|
78
|
+
Array.isArray(value.$values));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Extracts the $values array from a directive, or returns the value as-is if it's already an array.
|
|
82
|
+
* Returns null if value is neither an array nor a valid directive.
|
|
83
|
+
*/
|
|
84
|
+
function extractArrayOrDirectiveValues(value) {
|
|
85
|
+
if (Array.isArray(value))
|
|
86
|
+
return value;
|
|
87
|
+
if (isArrayMergeDirective(value))
|
|
88
|
+
return value.$values;
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
65
91
|
/**
|
|
66
92
|
* Validates a single ruleset rule.
|
|
67
93
|
*/
|
|
@@ -158,11 +184,12 @@ export function validateRuleset(ruleset, name, context) {
|
|
|
158
184
|
}
|
|
159
185
|
// Validate bypassActors
|
|
160
186
|
if (rs.bypassActors !== undefined) {
|
|
161
|
-
|
|
162
|
-
|
|
187
|
+
const actors = extractArrayOrDirectiveValues(rs.bypassActors);
|
|
188
|
+
if (actors === null) {
|
|
189
|
+
throw new ValidationError(`${context}: ruleset '${name}' bypassActors must be an array or $arrayMerge directive`);
|
|
163
190
|
}
|
|
164
|
-
for (let i = 0; i <
|
|
165
|
-
const actor =
|
|
191
|
+
for (let i = 0; i < actors.length; i++) {
|
|
192
|
+
const actor = actors[i];
|
|
166
193
|
if (typeof actor !== "object" || actor === null) {
|
|
167
194
|
throw new ValidationError(`${context}: ruleset '${name}' bypassActors[${i}] must be an object`);
|
|
168
195
|
}
|
|
@@ -193,25 +220,28 @@ export function validateRuleset(ruleset, name, context) {
|
|
|
193
220
|
Array.isArray(refName)) {
|
|
194
221
|
throw new ValidationError(`${context}: ruleset '${name}' conditions.refName must be an object`);
|
|
195
222
|
}
|
|
196
|
-
if (refName.include !== undefined
|
|
197
|
-
(
|
|
198
|
-
|
|
199
|
-
|
|
223
|
+
if (refName.include !== undefined) {
|
|
224
|
+
const include = extractArrayOrDirectiveValues(refName.include);
|
|
225
|
+
if (include === null || !include.every((s) => typeof s === "string")) {
|
|
226
|
+
throw new ValidationError(`${context}: ruleset '${name}' conditions.refName.include must be an array of strings or $arrayMerge directive with string $values`);
|
|
227
|
+
}
|
|
200
228
|
}
|
|
201
|
-
if (refName.exclude !== undefined
|
|
202
|
-
(
|
|
203
|
-
|
|
204
|
-
|
|
229
|
+
if (refName.exclude !== undefined) {
|
|
230
|
+
const exclude = extractArrayOrDirectiveValues(refName.exclude);
|
|
231
|
+
if (exclude === null || !exclude.every((s) => typeof s === "string")) {
|
|
232
|
+
throw new ValidationError(`${context}: ruleset '${name}' conditions.refName.exclude must be an array of strings or $arrayMerge directive with string $values`);
|
|
233
|
+
}
|
|
205
234
|
}
|
|
206
235
|
}
|
|
207
236
|
}
|
|
208
237
|
// Validate rules array
|
|
209
238
|
if (rs.rules !== undefined) {
|
|
210
|
-
|
|
211
|
-
|
|
239
|
+
const rules = extractArrayOrDirectiveValues(rs.rules);
|
|
240
|
+
if (rules === null) {
|
|
241
|
+
throw new ValidationError(`${context}: ruleset '${name}' rules must be an array or $arrayMerge directive`);
|
|
212
242
|
}
|
|
213
|
-
for (let i = 0; i <
|
|
214
|
-
validateRule(
|
|
243
|
+
for (let i = 0; i < rules.length; i++) {
|
|
244
|
+
validateRule(rules[i], `${context}: ruleset '${name}' rules[${i}]`);
|
|
215
245
|
}
|
|
216
246
|
}
|
|
217
247
|
}
|
package/package.json
CHANGED