@featurevisor/core 2.17.0 → 2.19.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.
Files changed (106) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/coverage/clover.xml +1692 -1196
  3. package/coverage/coverage-final.json +29 -22
  4. package/coverage/lcov-report/builder/allocator.ts.html +26 -26
  5. package/coverage/lcov-report/builder/buildDatafile.ts.html +2017 -0
  6. package/coverage/lcov-report/builder/buildScopedConditions.ts.html +35 -35
  7. package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +213 -45
  8. package/coverage/lcov-report/builder/buildScopedSegments.ts.html +28 -28
  9. package/coverage/lcov-report/builder/convertToV1.ts.html +583 -0
  10. package/coverage/lcov-report/builder/getFeatureRanges.ts.html +268 -0
  11. package/coverage/lcov-report/builder/hashes.ts.html +412 -0
  12. package/coverage/lcov-report/builder/index.html +90 -30
  13. package/coverage/lcov-report/builder/mutateVariables.ts.html +66 -45
  14. package/coverage/lcov-report/builder/mutator.ts.html +95 -95
  15. package/coverage/lcov-report/builder/revision.ts.html +1 -1
  16. package/coverage/lcov-report/builder/traffic.ts.html +60 -57
  17. package/coverage/lcov-report/config/index.html +1 -1
  18. package/coverage/lcov-report/config/index.ts.html +2 -2
  19. package/coverage/lcov-report/config/projectConfig.ts.html +28 -28
  20. package/coverage/lcov-report/datasource/adapter.ts.html +2 -2
  21. package/coverage/lcov-report/datasource/datasource.ts.html +4 -4
  22. package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +34 -34
  23. package/coverage/lcov-report/datasource/index.html +1 -1
  24. package/coverage/lcov-report/datasource/index.ts.html +1 -1
  25. package/coverage/lcov-report/index.html +39 -39
  26. package/coverage/lcov-report/linter/attributeSchema.ts.html +1 -1
  27. package/coverage/lcov-report/linter/checkCircularDependency.ts.html +1 -1
  28. package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +1 -1
  29. package/coverage/lcov-report/linter/conditionSchema.ts.html +17 -17
  30. package/coverage/lcov-report/linter/featureSchema.ts.html +879 -417
  31. package/coverage/lcov-report/linter/groupSchema.ts.html +1 -1
  32. package/coverage/lcov-report/linter/index.html +34 -34
  33. package/coverage/lcov-report/linter/lintProject.ts.html +25 -25
  34. package/coverage/lcov-report/linter/mutationNotation.ts.html +224 -122
  35. package/coverage/lcov-report/linter/printError.ts.html +1 -1
  36. package/coverage/lcov-report/linter/schema.ts.html +175 -85
  37. package/coverage/lcov-report/linter/segmentSchema.ts.html +1 -1
  38. package/coverage/lcov-report/linter/testSchema.ts.html +5 -5
  39. package/coverage/lcov-report/list/index.html +1 -1
  40. package/coverage/lcov-report/list/matrix.ts.html +1 -1
  41. package/coverage/lcov-report/parsers/index.html +1 -1
  42. package/coverage/lcov-report/parsers/index.ts.html +4 -4
  43. package/coverage/lcov-report/parsers/json.ts.html +2 -2
  44. package/coverage/lcov-report/parsers/yml.ts.html +6 -6
  45. package/coverage/lcov-report/tester/cliFormat.ts.html +1 -1
  46. package/coverage/lcov-report/tester/helpers.ts.html +1 -1
  47. package/coverage/lcov-report/tester/index.html +1 -1
  48. package/coverage/lcov-report/utils/extractKeys.ts.html +493 -0
  49. package/coverage/lcov-report/utils/git.ts.html +3 -3
  50. package/coverage/lcov-report/utils/index.html +54 -9
  51. package/coverage/lcov-report/utils/index.ts.html +94 -0
  52. package/coverage/lcov-report/utils/pretty.ts.html +121 -0
  53. package/coverage/lcov.info +3206 -2260
  54. package/json-schema/attribute.json +25 -9
  55. package/json-schema/feature.json +319 -238
  56. package/json-schema/segment.json +76 -45
  57. package/lib/builder/buildDatafile.js +50 -3
  58. package/lib/builder/buildDatafile.js.map +1 -1
  59. package/lib/builder/buildDatafile.spec.d.ts +1 -0
  60. package/lib/builder/buildDatafile.spec.js +233 -0
  61. package/lib/builder/buildDatafile.spec.js.map +1 -0
  62. package/lib/builder/buildScopedDatafile.js +37 -1
  63. package/lib/builder/buildScopedDatafile.js.map +1 -1
  64. package/lib/builder/buildScopedDatafile.spec.js +289 -0
  65. package/lib/builder/buildScopedDatafile.spec.js.map +1 -1
  66. package/lib/builder/mutateVariables.d.ts +1 -1
  67. package/lib/builder/mutateVariables.js +4 -1
  68. package/lib/builder/mutateVariables.js.map +1 -1
  69. package/lib/builder/mutateVariables.spec.js +29 -0
  70. package/lib/builder/mutateVariables.spec.js.map +1 -1
  71. package/lib/builder/traffic.js +1 -0
  72. package/lib/builder/traffic.js.map +1 -1
  73. package/lib/builder/traffic.spec.js +26 -0
  74. package/lib/builder/traffic.spec.js.map +1 -1
  75. package/lib/generate-code/typescript.js +63 -20
  76. package/lib/generate-code/typescript.js.map +1 -1
  77. package/lib/linter/featureSchema.d.ts +237 -22
  78. package/lib/linter/featureSchema.js +175 -84
  79. package/lib/linter/featureSchema.js.map +1 -1
  80. package/lib/linter/featureSchema.spec.js +268 -0
  81. package/lib/linter/featureSchema.spec.js.map +1 -1
  82. package/lib/linter/mutationNotation.js +54 -19
  83. package/lib/linter/mutationNotation.js.map +1 -1
  84. package/lib/linter/mutationNotation.spec.js +49 -0
  85. package/lib/linter/mutationNotation.spec.js.map +1 -1
  86. package/lib/linter/schema.d.ts +1 -0
  87. package/lib/linter/schema.js +13 -0
  88. package/lib/linter/schema.js.map +1 -1
  89. package/lib/linter/schema.spec.js +51 -0
  90. package/lib/linter/schema.spec.js.map +1 -1
  91. package/package.json +5 -5
  92. package/src/builder/buildDatafile.spec.ts +267 -0
  93. package/src/builder/buildDatafile.ts +85 -4
  94. package/src/builder/buildScopedDatafile.spec.ts +330 -0
  95. package/src/builder/buildScopedDatafile.ts +57 -1
  96. package/src/builder/mutateVariables.spec.ts +45 -0
  97. package/src/builder/mutateVariables.ts +7 -0
  98. package/src/builder/traffic.spec.ts +33 -0
  99. package/src/builder/traffic.ts +1 -0
  100. package/src/generate-code/typescript.ts +87 -20
  101. package/src/linter/featureSchema.spec.ts +315 -0
  102. package/src/linter/featureSchema.ts +269 -115
  103. package/src/linter/mutationNotation.spec.ts +62 -0
  104. package/src/linter/mutationNotation.ts +47 -13
  105. package/src/linter/schema.spec.ts +72 -0
  106. package/src/linter/schema.ts +30 -0
@@ -956,6 +956,336 @@ describe("core: buildScopedDatafile", function () {
956
956
  });
957
957
  });
958
958
 
959
+ describe("features with traffic and variableOverrides", function () {
960
+ test("traffic variableOverride with matching segments becomes *", function () {
961
+ const datafile: DatafileContent = {
962
+ schemaVersion: "2",
963
+ revision: "unknown",
964
+ segments: {
965
+ mobile: {
966
+ conditions: [{ attribute: "device", operator: "equals", value: "mobile" }],
967
+ },
968
+ },
969
+ features: {
970
+ feature1: {
971
+ bucketBy: ["userId"],
972
+ traffic: [
973
+ {
974
+ key: "rule1",
975
+ segments: "*",
976
+ percentage: 100000,
977
+ variableOverrides: {
978
+ config: [{ segments: "mobile", value: { source: "mobile" } }],
979
+ },
980
+ },
981
+ ],
982
+ },
983
+ },
984
+ };
985
+
986
+ const result = buildScopedDatafile(datafile, { device: "mobile" });
987
+
988
+ expect(result.features.feature1.traffic[0].variableOverrides?.config[0].segments).toEqual(
989
+ "*",
990
+ );
991
+ });
992
+
993
+ test("traffic variableOverride with non-matching segments remains", function () {
994
+ const datafile: DatafileContent = {
995
+ schemaVersion: "2",
996
+ revision: "unknown",
997
+ segments: {
998
+ mobile: {
999
+ conditions: [{ attribute: "device", operator: "equals", value: "mobile" }],
1000
+ },
1001
+ },
1002
+ features: {
1003
+ feature1: {
1004
+ bucketBy: ["userId"],
1005
+ traffic: [
1006
+ {
1007
+ key: "rule1",
1008
+ segments: "*",
1009
+ percentage: 100000,
1010
+ variableOverrides: {
1011
+ config: [{ segments: "mobile", value: { source: "mobile" } }],
1012
+ },
1013
+ },
1014
+ ],
1015
+ },
1016
+ },
1017
+ };
1018
+
1019
+ const result = buildScopedDatafile(datafile, { device: "desktop" });
1020
+
1021
+ expect(result.features.feature1.traffic[0].variableOverrides?.config[0].segments).toEqual(
1022
+ "mobile",
1023
+ );
1024
+ });
1025
+
1026
+ test("traffic variableOverride with matching conditions becomes *", function () {
1027
+ const datafile: DatafileContent = {
1028
+ schemaVersion: "2",
1029
+ revision: "unknown",
1030
+ segments: {},
1031
+ features: {
1032
+ feature1: {
1033
+ bucketBy: ["userId"],
1034
+ traffic: [
1035
+ {
1036
+ key: "rule1",
1037
+ segments: "*",
1038
+ percentage: 100000,
1039
+ variableOverrides: {
1040
+ config: [
1041
+ {
1042
+ conditions: [{ attribute: "country", operator: "equals", value: "nl" }],
1043
+ value: { source: "nl" },
1044
+ },
1045
+ ],
1046
+ },
1047
+ },
1048
+ ],
1049
+ },
1050
+ },
1051
+ };
1052
+
1053
+ const result = buildScopedDatafile(datafile, { country: "nl" });
1054
+
1055
+ expect(result.features.feature1.traffic[0].variableOverrides?.config[0].conditions).toEqual(
1056
+ "*",
1057
+ );
1058
+ });
1059
+
1060
+ test("traffic variableOverride with non-matching conditions remains", function () {
1061
+ const datafile: DatafileContent = {
1062
+ schemaVersion: "2",
1063
+ revision: "unknown",
1064
+ segments: {},
1065
+ features: {
1066
+ feature1: {
1067
+ bucketBy: ["userId"],
1068
+ traffic: [
1069
+ {
1070
+ key: "rule1",
1071
+ segments: "*",
1072
+ percentage: 100000,
1073
+ variableOverrides: {
1074
+ config: [
1075
+ {
1076
+ conditions: [{ attribute: "country", operator: "equals", value: "nl" }],
1077
+ value: { source: "nl" },
1078
+ },
1079
+ ],
1080
+ },
1081
+ },
1082
+ ],
1083
+ },
1084
+ },
1085
+ };
1086
+
1087
+ const result = buildScopedDatafile(datafile, { country: "de" });
1088
+
1089
+ expect(result.features.feature1.traffic[0].variableOverrides?.config[0].conditions).toEqual([
1090
+ { attribute: "country", operator: "equals", value: "nl" },
1091
+ ]);
1092
+ });
1093
+
1094
+ test("traffic variableOverride supports array/group segments and conditions", function () {
1095
+ const datafile: DatafileContent = {
1096
+ schemaVersion: "2",
1097
+ revision: "unknown",
1098
+ segments: {
1099
+ mobile: {
1100
+ conditions: [{ attribute: "device", operator: "equals", value: "mobile" }],
1101
+ },
1102
+ touch: {
1103
+ conditions: [{ attribute: "touch", operator: "equals", value: true }],
1104
+ },
1105
+ },
1106
+ features: {
1107
+ feature1: {
1108
+ bucketBy: ["userId"],
1109
+ traffic: [
1110
+ {
1111
+ key: "rule1",
1112
+ segments: "*",
1113
+ percentage: 100000,
1114
+ variableOverrides: {
1115
+ config: [
1116
+ {
1117
+ segments: { and: ["mobile", "touch"] },
1118
+ value: { source: "group-segments" },
1119
+ },
1120
+ {
1121
+ conditions: {
1122
+ and: [
1123
+ { attribute: "country", operator: "equals", value: "nl" },
1124
+ { attribute: "tier", operator: "equals", value: "premium" },
1125
+ ],
1126
+ },
1127
+ value: { source: "group-conditions" },
1128
+ },
1129
+ ],
1130
+ },
1131
+ },
1132
+ ],
1133
+ },
1134
+ },
1135
+ };
1136
+
1137
+ const result = buildScopedDatafile(datafile, {
1138
+ device: "mobile",
1139
+ touch: true,
1140
+ country: "nl",
1141
+ tier: "premium",
1142
+ });
1143
+
1144
+ expect(result.features.feature1.traffic[0].variableOverrides?.config[0].segments).toEqual(
1145
+ "*",
1146
+ );
1147
+ expect(result.features.feature1.traffic[0].variableOverrides?.config[1].conditions).toEqual(
1148
+ "*",
1149
+ );
1150
+ });
1151
+
1152
+ test("traffic variableOverride supports stringified segments and conditions", function () {
1153
+ const datafile: DatafileContent = {
1154
+ schemaVersion: "2",
1155
+ revision: "unknown",
1156
+ segments: {
1157
+ mobile: {
1158
+ conditions: [{ attribute: "device", operator: "equals", value: "mobile" }],
1159
+ },
1160
+ touch: {
1161
+ conditions: [{ attribute: "touch", operator: "equals", value: true }],
1162
+ },
1163
+ },
1164
+ features: {
1165
+ feature1: {
1166
+ bucketBy: ["userId"],
1167
+ traffic: [
1168
+ {
1169
+ key: "rule1",
1170
+ segments: "*",
1171
+ percentage: 100000,
1172
+ variableOverrides: {
1173
+ config: [
1174
+ {
1175
+ segments: JSON.stringify({ and: ["mobile", "touch"] }),
1176
+ value: { source: "stringified-segments" },
1177
+ },
1178
+ {
1179
+ conditions: JSON.stringify([
1180
+ { attribute: "country", operator: "equals", value: "nl" },
1181
+ ]),
1182
+ value: { source: "stringified-conditions" },
1183
+ },
1184
+ ],
1185
+ },
1186
+ },
1187
+ ],
1188
+ },
1189
+ },
1190
+ };
1191
+
1192
+ const result = buildScopedDatafile(datafile, {
1193
+ device: "mobile",
1194
+ touch: true,
1195
+ country: "nl",
1196
+ });
1197
+
1198
+ expect(result.features.feature1.traffic[0].variableOverrides?.config[0].segments).toEqual(
1199
+ "*",
1200
+ );
1201
+ expect(result.features.feature1.traffic[0].variableOverrides?.config[1].conditions).toEqual(
1202
+ "*",
1203
+ );
1204
+ });
1205
+
1206
+ test("multiple traffic entries still apply consecutive * dedupe", function () {
1207
+ const datafile: DatafileContent = {
1208
+ schemaVersion: "2",
1209
+ revision: "unknown",
1210
+ segments: {
1211
+ web: {
1212
+ conditions: [{ attribute: "platform", operator: "equals", value: "web" }],
1213
+ },
1214
+ mobile: {
1215
+ conditions: [{ attribute: "device", operator: "equals", value: "mobile" }],
1216
+ },
1217
+ },
1218
+ features: {
1219
+ feature1: {
1220
+ bucketBy: ["userId"],
1221
+ traffic: [
1222
+ {
1223
+ key: "rule1",
1224
+ segments: "web",
1225
+ percentage: 50000,
1226
+ variableOverrides: {
1227
+ config: [{ segments: "mobile", value: { source: "rule1" } }],
1228
+ },
1229
+ },
1230
+ {
1231
+ key: "rule2",
1232
+ segments: "web",
1233
+ percentage: 50000,
1234
+ variableOverrides: {
1235
+ config: [{ segments: "mobile", value: { source: "rule2" } }],
1236
+ },
1237
+ },
1238
+ ],
1239
+ },
1240
+ },
1241
+ };
1242
+
1243
+ const result = buildScopedDatafile(datafile, {
1244
+ platform: "web",
1245
+ device: "mobile",
1246
+ });
1247
+
1248
+ expect(result.features.feature1.traffic).toHaveLength(1);
1249
+ expect(result.features.feature1.traffic[0].key).toEqual("rule1");
1250
+ expect(result.features.feature1.traffic[0].variableOverrides?.config[0].segments).toEqual(
1251
+ "*",
1252
+ );
1253
+ });
1254
+
1255
+ test("does not mutate original datafile traffic variableOverrides", function () {
1256
+ const datafile: DatafileContent = {
1257
+ schemaVersion: "2",
1258
+ revision: "unknown",
1259
+ segments: {
1260
+ mobile: {
1261
+ conditions: [{ attribute: "device", operator: "equals", value: "mobile" }],
1262
+ },
1263
+ },
1264
+ features: {
1265
+ feature1: {
1266
+ bucketBy: ["userId"],
1267
+ traffic: [
1268
+ {
1269
+ key: "rule1",
1270
+ segments: "*",
1271
+ percentage: 100000,
1272
+ variableOverrides: {
1273
+ config: [{ segments: "mobile", value: { source: "mobile" } }],
1274
+ },
1275
+ },
1276
+ ],
1277
+ },
1278
+ },
1279
+ };
1280
+
1281
+ const original = JSON.stringify(datafile);
1282
+
1283
+ buildScopedDatafile(datafile, { device: "mobile" });
1284
+
1285
+ expect(JSON.stringify(datafile)).toEqual(original);
1286
+ });
1287
+ });
1288
+
959
1289
  describe("features with variations and variableOverrides", function () {
960
1290
  test("variableOverride with matching segments becomes *", function () {
961
1291
  const datafile: DatafileContent = {
@@ -4,6 +4,23 @@ import { DatafileReader, createLogger } from "@featurevisor/sdk";
4
4
  import { buildScopedConditions } from "./buildScopedConditions";
5
5
  import { buildScopedSegments } from "./buildScopedSegments";
6
6
 
7
+ function parseIfStringified<T>(value: T): T {
8
+ if (typeof value !== "string" || value === "*") {
9
+ return value;
10
+ }
11
+
12
+ const firstChar = value[0];
13
+ if (firstChar !== "{" && firstChar !== "[") {
14
+ return value;
15
+ }
16
+
17
+ try {
18
+ return JSON.parse(value) as T;
19
+ } catch {
20
+ return value;
21
+ }
22
+ }
23
+
7
24
  export function buildScopedDatafile(
8
25
  originalDatafileContent: DatafileContent,
9
26
  context: Context,
@@ -76,13 +93,52 @@ export function buildScopedDatafile(
76
93
 
77
94
  // segments
78
95
  if (traffic.segments) {
96
+ const segments = parseIfStringified(traffic.segments);
79
97
  feature.traffic[trafficI].segments = buildScopedSegments(
80
98
  originalDatafileReader,
81
- traffic.segments,
99
+ segments,
82
100
  context,
83
101
  removeSegments,
84
102
  );
85
103
  }
104
+
105
+ // variable overrides
106
+ if (traffic.variableOverrides) {
107
+ for (const variableKey in traffic.variableOverrides) {
108
+ const variableOverrides = traffic.variableOverrides[variableKey];
109
+
110
+ for (
111
+ let variableOverrideI = 0;
112
+ variableOverrideI < variableOverrides.length;
113
+ variableOverrideI++
114
+ ) {
115
+ const variableOverride = variableOverrides[variableOverrideI];
116
+
117
+ // segments
118
+ if (variableOverride.segments) {
119
+ const segments = parseIfStringified(variableOverride.segments);
120
+ variableOverride.segments = buildScopedSegments(
121
+ originalDatafileReader,
122
+ segments,
123
+ context,
124
+ removeSegments,
125
+ );
126
+ }
127
+
128
+ // conditions
129
+ if (variableOverride.conditions) {
130
+ const conditions = parseIfStringified(variableOverride.conditions);
131
+ variableOverride.conditions = buildScopedConditions(
132
+ originalDatafileReader,
133
+ conditions,
134
+ context,
135
+ );
136
+ }
137
+
138
+ variableOverrides[variableOverrideI] = variableOverride;
139
+ }
140
+ }
141
+ }
86
142
  }
87
143
  }
88
144
 
@@ -987,6 +987,51 @@ describe("mutateVariables", function () {
987
987
  const result = resolveMutationsForSingleVariable(schema, "foo", {});
988
988
  expect(result).toEqual({});
989
989
  });
990
+
991
+ test("uses provided baseValue instead of defaultValue for path mutations", function () {
992
+ const schema: Record<string, VariableSchema> = {
993
+ cfg: {
994
+ type: "object",
995
+ defaultValue: { source: "default", nested: { value: 1 } },
996
+ },
997
+ };
998
+
999
+ const result = resolveMutationsForSingleVariable(
1000
+ schema,
1001
+ "cfg",
1002
+ {
1003
+ "nested.value": 2,
1004
+ },
1005
+ { source: "rule", nested: { value: 10 }, extra: true },
1006
+ );
1007
+
1008
+ expect(result).toEqual({
1009
+ source: "rule",
1010
+ nested: { value: 2 },
1011
+ extra: true,
1012
+ });
1013
+ });
1014
+
1015
+ test("baseValue with full replacement key still applies replacement first", function () {
1016
+ const schema: Record<string, VariableSchema> = {
1017
+ cfg: {
1018
+ type: "object",
1019
+ defaultValue: { a: 1, b: 2 },
1020
+ },
1021
+ };
1022
+
1023
+ const result = resolveMutationsForSingleVariable(
1024
+ schema,
1025
+ "cfg",
1026
+ {
1027
+ cfg: { a: 100, c: 300 },
1028
+ c: 999,
1029
+ },
1030
+ { a: 10, b: 20 },
1031
+ );
1032
+
1033
+ expect(result).toEqual({ a: 100, c: 999 });
1034
+ });
990
1035
  });
991
1036
 
992
1037
  describe("does not mutate input", function () {
@@ -89,14 +89,21 @@ export function resolveMutationsForSingleVariable(
89
89
  variablesSchema: Record<string, VariableSchema> | undefined,
90
90
  variableKey: string,
91
91
  overrideValue: VariableValue,
92
+ baseValue?: VariableValue,
92
93
  ): VariableValue {
93
94
  if (!variablesSchema || !variablesSchema[variableKey]) return overrideValue;
94
95
  if (overrideValue === null || overrideValue === undefined) return overrideValue;
95
96
  if (typeof overrideValue !== "object" || Array.isArray(overrideValue)) {
96
97
  return overrideValue;
97
98
  }
99
+
98
100
  const pathMap = overrideValue as Record<string, VariableValue>;
99
101
  const flat: Record<string, VariableValue> = {};
102
+
103
+ if (typeof baseValue !== "undefined") {
104
+ flat[variableKey] = JSON.parse(JSON.stringify(baseValue)) as VariableValue;
105
+ }
106
+
100
107
  for (const [k, v] of Object.entries(pathMap)) {
101
108
  flat[k === variableKey ? variableKey : variableKey + "." + k] = v;
102
109
  }
@@ -910,4 +910,37 @@ describe("core: Traffic", function () {
910
910
  },
911
911
  ]);
912
912
  });
913
+
914
+ it("should include variableOverrides from parsed rule in traffic", function () {
915
+ const variableOverrides = {
916
+ config: [
917
+ {
918
+ segments: "mobile",
919
+ value: {
920
+ "layout.width": 900,
921
+ },
922
+ },
923
+ ],
924
+ };
925
+
926
+ const result = getTraffic(
927
+ [
928
+ {
929
+ value: "control",
930
+ weight: 100,
931
+ },
932
+ ],
933
+ [
934
+ {
935
+ key: "1",
936
+ segments: "*",
937
+ percentage: 100,
938
+ variableOverrides,
939
+ },
940
+ ],
941
+ undefined,
942
+ );
943
+
944
+ expect(result[0].variableOverrides).toEqual(variableOverrides);
945
+ });
913
946
  });
@@ -91,6 +91,7 @@ export function getTraffic(
91
91
  percentage: rulePercentage * (MAX_BUCKETED_NUMBER / 100),
92
92
  allocation: [],
93
93
  variationWeights: parsedRule.variationWeights,
94
+ variableOverrides: parsedRule.variableOverrides,
94
95
  };
95
96
 
96
97
  // overrides
@@ -48,6 +48,63 @@ function formatObjectKey(key: string): string {
48
48
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
49
49
  }
50
50
 
51
+ function buildObjectTypeFromSchema(
52
+ schema: Schema,
53
+ schemasByKey: Record<string, Schema>,
54
+ schemaTypeNames?: Record<string, string>,
55
+ ): string {
56
+ const props = schema.properties;
57
+ const additional = schema.additionalProperties;
58
+ const hasProps = props && typeof props === "object" && Object.keys(props).length > 0;
59
+ const hasAdditional = additional && typeof additional === "object";
60
+
61
+ if (hasProps && hasAdditional) {
62
+ const requiredSet = new Set(schema.required || []);
63
+ const propEntries = Object.entries(props as Record<string, Schema>).map(([k, v]) => {
64
+ const propType = schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames);
65
+ const optional = !requiredSet.has(k);
66
+ return optional
67
+ ? `${formatObjectKey(k)}?: ${propType}`
68
+ : `${formatObjectKey(k)}: ${propType}`;
69
+ });
70
+ const propUnion = Object.entries(props as Record<string, Schema>)
71
+ .map(([, v]) => schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames))
72
+ .join(" | ");
73
+ const additionalType = schemaToTypeScriptType(
74
+ additional as Schema,
75
+ schemasByKey,
76
+ schemaTypeNames,
77
+ );
78
+ const indexType = [additionalType, propUnion].filter(Boolean).join(" | ");
79
+ return `{ ${propEntries.join("; ")}; [key: string]: ${indexType} }`;
80
+ }
81
+
82
+ if (hasProps) {
83
+ const requiredSet = new Set(schema.required || []);
84
+ const entries = Object.entries(props as Record<string, Schema>)
85
+ .map(([k, v]) => {
86
+ const propType = schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames);
87
+ const optional = !requiredSet.has(k);
88
+ return optional
89
+ ? `${formatObjectKey(k)}?: ${propType}`
90
+ : `${formatObjectKey(k)}: ${propType}`;
91
+ })
92
+ .join("; ");
93
+ return `{ ${entries} }`;
94
+ }
95
+
96
+ if (hasAdditional) {
97
+ const additionalType = schemaToTypeScriptType(
98
+ additional as Schema,
99
+ schemasByKey,
100
+ schemaTypeNames,
101
+ );
102
+ return `Record<string, ${additionalType}>`;
103
+ }
104
+
105
+ return "Record<string, unknown>";
106
+ }
107
+
51
108
  function formatTypeImport(typeNames: string[], fromPath: string): string {
52
109
  if (typeNames.length === 0) {
53
110
  return "";
@@ -132,19 +189,7 @@ function schemaToTypeScriptType(
132
189
  }
133
190
  return "string[]";
134
191
  case "object": {
135
- const props = resolved.properties;
136
- if (props && typeof props === "object" && Object.keys(props).length > 0) {
137
- const requiredSet = new Set(resolved.required || []);
138
- const entries = Object.entries(props)
139
- .map(([k, v]) => {
140
- const propType = schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames);
141
- const optional = !requiredSet.has(k);
142
- return optional ? `${k}?: ${propType}` : `${k}: ${propType}`;
143
- })
144
- .join("; ");
145
- return `{ ${entries} }`;
146
- }
147
- return "Record<string, unknown>";
192
+ return buildObjectTypeFromSchema(resolved, schemasByKey, schemaTypeNames);
148
193
  }
149
194
  default:
150
195
  return "unknown";
@@ -265,13 +310,17 @@ function generateVariableTypeDeclarations(
265
310
 
266
311
  if (type === "object") {
267
312
  const resolvedEffective =
268
- effective && "properties" in effective
313
+ effective && ("properties" in effective || "additionalProperties" in effective)
269
314
  ? (resolveSchema(effective as Schema, schemasByKey) as Schema)
270
315
  : undefined;
271
316
  const props = resolvedEffective?.properties;
272
- if (props && typeof props === "object" && Object.keys(props).length > 0) {
317
+ const additional = resolvedEffective?.additionalProperties;
318
+ if (
319
+ (props && typeof props === "object" && Object.keys(props).length > 0) ||
320
+ (additional && typeof additional === "object")
321
+ ) {
273
322
  const requiredSet = new Set(resolvedEffective?.required || []);
274
- const entries = Object.entries(props)
323
+ const entries = Object.entries(props ?? {})
275
324
  .map(([k, v]) => {
276
325
  const propType = schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames);
277
326
  if (schemaTypeNames)
@@ -280,11 +329,29 @@ function generateVariableTypeDeclarations(
280
329
  });
281
330
  const optional = !requiredSet.has(k);
282
331
  return optional
283
- ? `${INDENT_NS_BODY}${k}?: ${propType};`
284
- : `${INDENT_NS_BODY}${k}: ${propType};`;
332
+ ? `${INDENT_NS_BODY}${formatObjectKey(k)}?: ${propType};`
333
+ : `${INDENT_NS_BODY}${formatObjectKey(k)}: ${propType};`;
285
334
  })
286
- .join("\n");
287
- declarations.push(`${INDENT_NS}export interface ${typeName} {\n${entries}\n${INDENT_NS}}`);
335
+ .filter(Boolean);
336
+ if (additional && typeof additional === "object") {
337
+ const additionalType = schemaToTypeScriptType(
338
+ additional as Schema,
339
+ schemasByKey,
340
+ schemaTypeNames,
341
+ );
342
+ if (schemaTypeNames)
343
+ Object.values(schemaTypeNames).forEach((n) => {
344
+ if (additionalType.includes(n)) addSchemaUsed(n);
345
+ });
346
+ const propUnion = Object.entries(props ?? {})
347
+ .map(([, v]) => schemaToTypeScriptType(v as Schema, schemasByKey, schemaTypeNames))
348
+ .join(" | ");
349
+ const indexType = [additionalType, propUnion].filter(Boolean).join(" | ");
350
+ entries.push(`${INDENT_NS_BODY}[key: string]: ${indexType};`);
351
+ }
352
+ declarations.push(
353
+ `${INDENT_NS}export interface ${typeName} {\n${entries.join("\n")}\n${INDENT_NS}}`,
354
+ );
288
355
  return { declarations, returnTypeName: typeName, genericArg: typeName, schemaTypesUsed };
289
356
  }
290
357
  declarations.push(`${INDENT_NS}export type ${typeName} = Record<string, unknown>;`);