@featurevisor/core 1.31.0 → 1.32.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 (76) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/coverage/clover.xml +11 -11
  3. package/coverage/coverage-final.json +2 -2
  4. package/coverage/lcov-report/index.html +7 -7
  5. package/coverage/lcov-report/lib/builder/allocator.js.html +1 -1
  6. package/coverage/lcov-report/lib/builder/index.html +1 -1
  7. package/coverage/lcov-report/lib/builder/revision.js.html +1 -1
  8. package/coverage/lcov-report/lib/builder/traffic.js.html +1 -1
  9. package/coverage/lcov-report/lib/tester/checkIfObjectsAreEqual.js.html +1 -1
  10. package/coverage/lcov-report/lib/tester/index.html +5 -5
  11. package/coverage/lcov-report/lib/tester/matrix.js.html +5 -5
  12. package/coverage/lcov-report/src/builder/allocator.ts.html +1 -1
  13. package/coverage/lcov-report/src/builder/index.html +1 -1
  14. package/coverage/lcov-report/src/builder/revision.ts.html +1 -1
  15. package/coverage/lcov-report/src/builder/traffic.ts.html +1 -1
  16. package/coverage/lcov-report/src/tester/checkIfObjectsAreEqual.ts.html +1 -1
  17. package/coverage/lcov-report/src/tester/index.html +5 -5
  18. package/coverage/lcov-report/src/tester/matrix.ts.html +5 -5
  19. package/coverage/lcov.info +30 -22
  20. package/lib/assess-distribution/index.d.ts +1 -1
  21. package/lib/assess-distribution/index.js +2 -2
  22. package/lib/assess-distribution/index.js.map +1 -1
  23. package/lib/benchmark/index.d.ts +1 -1
  24. package/lib/benchmark/index.js +2 -2
  25. package/lib/benchmark/index.js.map +1 -1
  26. package/lib/builder/buildDatafile.d.ts +1 -1
  27. package/lib/builder/buildDatafile.js +53 -40
  28. package/lib/builder/buildDatafile.js.map +1 -1
  29. package/lib/builder/buildProject.js +87 -50
  30. package/lib/builder/buildProject.js.map +1 -1
  31. package/lib/config/projectConfig.d.ts +1 -1
  32. package/lib/datasource/adapter.d.ts +3 -3
  33. package/lib/datasource/adapter.js.map +1 -1
  34. package/lib/datasource/datasource.d.ts +2 -2
  35. package/lib/datasource/datasource.js.map +1 -1
  36. package/lib/datasource/filesystemAdapter.d.ts +1 -1
  37. package/lib/datasource/filesystemAdapter.js +9 -3
  38. package/lib/datasource/filesystemAdapter.js.map +1 -1
  39. package/lib/evaluate/index.d.ts +1 -1
  40. package/lib/evaluate/index.js +2 -2
  41. package/lib/evaluate/index.js.map +1 -1
  42. package/lib/find-usage/index.js +60 -26
  43. package/lib/find-usage/index.js.map +1 -1
  44. package/lib/linter/featureSchema.d.ts +194 -5
  45. package/lib/linter/featureSchema.js +70 -60
  46. package/lib/linter/featureSchema.js.map +1 -1
  47. package/lib/linter/testSchema.d.ts +1 -1
  48. package/lib/linter/testSchema.js +16 -13
  49. package/lib/linter/testSchema.js.map +1 -1
  50. package/lib/site/generateSiteSearchIndex.js +65 -24
  51. package/lib/site/generateSiteSearchIndex.js.map +1 -1
  52. package/lib/tester/matrix.js +2 -2
  53. package/lib/tester/matrix.js.map +1 -1
  54. package/lib/tester/testFeature.d.ts +3 -4
  55. package/lib/tester/testFeature.js +1 -2
  56. package/lib/tester/testFeature.js.map +1 -1
  57. package/lib/tester/testProject.d.ts +1 -3
  58. package/lib/tester/testProject.js +28 -12
  59. package/lib/tester/testProject.js.map +1 -1
  60. package/package.json +5 -5
  61. package/src/assess-distribution/index.ts +3 -3
  62. package/src/benchmark/index.ts +3 -3
  63. package/src/builder/buildDatafile.ts +25 -8
  64. package/src/builder/buildProject.ts +72 -31
  65. package/src/config/projectConfig.ts +1 -1
  66. package/src/datasource/adapter.ts +6 -3
  67. package/src/datasource/datasource.ts +2 -2
  68. package/src/datasource/filesystemAdapter.ts +12 -4
  69. package/src/evaluate/index.ts +3 -3
  70. package/src/find-usage/index.ts +65 -29
  71. package/src/linter/featureSchema.ts +100 -82
  72. package/src/linter/testSchema.ts +21 -16
  73. package/src/site/generateSiteSearchIndex.ts +71 -24
  74. package/src/tester/matrix.ts +2 -2
  75. package/src/tester/testFeature.ts +3 -2
  76. package/src/tester/testProject.ts +27 -8
@@ -1,4 +1,5 @@
1
- import { SCHEMA_VERSION } from "../config";
1
+ import { SCHEMA_VERSION, ProjectConfig } from "../config";
2
+ import { Datasource } from "../datasource";
2
3
 
3
4
  import { getNextRevision } from "./revision";
4
5
  import { buildDatafile, getCustomDatafile } from "./buildDatafile";
@@ -19,6 +20,58 @@ export interface BuildCLIOptions {
19
20
  datafilesDir?: string;
20
21
  }
21
22
 
23
+ async function buildForEnvironment({
24
+ projectConfig,
25
+ datasource,
26
+ nextRevision,
27
+ environment,
28
+ tags,
29
+ cliOptions,
30
+ }: {
31
+ projectConfig: ProjectConfig;
32
+ datasource: Datasource;
33
+ nextRevision: string;
34
+ environment: string | false;
35
+ tags: string[];
36
+ cliOptions: BuildCLIOptions;
37
+ }) {
38
+ console.log(`\nBuilding datafiles for environment: ${environment}`);
39
+
40
+ const existingState = await datasource.readState(environment);
41
+
42
+ for (const tag of tags) {
43
+ console.log(`\n => Tag: ${tag}`);
44
+
45
+ const datafileContent = await buildDatafile(
46
+ projectConfig,
47
+ datasource,
48
+ {
49
+ schemaVersion: cliOptions.schemaVersion || SCHEMA_VERSION,
50
+ revision: nextRevision,
51
+ environment: environment,
52
+ tag: tag,
53
+ inflate: cliOptions.inflate,
54
+ },
55
+ existingState,
56
+ );
57
+
58
+ // write datafile for environment/tag
59
+ await datasource.writeDatafile(datafileContent, {
60
+ environment,
61
+ tag,
62
+ datafilesDir: cliOptions.datafilesDir,
63
+ });
64
+ }
65
+
66
+ if (typeof cliOptions.stateFiles === "undefined" || cliOptions.stateFiles) {
67
+ // write state for environment
68
+ await datasource.writeState(environment, existingState);
69
+
70
+ // write revision
71
+ await datasource.writeRevision(nextRevision);
72
+ }
73
+ }
74
+
22
75
  export async function buildProject(deps: Dependencies, cliOptions: BuildCLIOptions = {}) {
23
76
  const { projectConfig, datasource } = deps;
24
77
 
@@ -63,42 +116,30 @@ export async function buildProject(deps: Dependencies, cliOptions: BuildCLIOptio
63
116
  const nextRevision =
64
117
  (cliOptions.revision && cliOptions.revision.toString()) || getNextRevision(currentRevision);
65
118
 
66
- for (const environment of environments) {
67
- console.log(`\nBuilding datafiles for environment: ${environment}`);
68
-
69
- const existingState = await datasource.readState(environment);
70
-
71
- for (const tag of tags) {
72
- console.log(`\n => Tag: ${tag}`);
73
-
74
- const datafileContent = await buildDatafile(
119
+ // with environments
120
+ if (Array.isArray(environments)) {
121
+ for (const environment of environments) {
122
+ await buildForEnvironment({
75
123
  projectConfig,
76
124
  datasource,
77
- {
78
- schemaVersion: cliOptions.schemaVersion || SCHEMA_VERSION,
79
- revision: nextRevision,
80
- environment: environment,
81
- tag: tag,
82
- inflate: cliOptions.inflate,
83
- },
84
- existingState,
85
- );
86
-
87
- // write datafile for environment/tag
88
- await datasource.writeDatafile(datafileContent, {
125
+ nextRevision,
89
126
  environment,
90
- tag,
91
- datafilesDir: cliOptions.datafilesDir,
127
+ tags,
128
+ cliOptions,
92
129
  });
93
130
  }
131
+ }
94
132
 
95
- if (typeof cliOptions.stateFiles === "undefined" || cliOptions.stateFiles) {
96
- // write state for environment
97
- await datasource.writeState(environment, existingState);
98
-
99
- // write revision
100
- await datasource.writeRevision(nextRevision);
101
- }
133
+ // no environment
134
+ if (environments === false) {
135
+ await buildForEnvironment({
136
+ projectConfig,
137
+ datasource,
138
+ nextRevision,
139
+ environment: false,
140
+ tags,
141
+ cliOptions,
142
+ });
102
143
  }
103
144
 
104
145
  console.log("\nLatest revision:", nextRevision);
@@ -39,7 +39,7 @@ export interface ProjectConfig {
39
39
  outputDirectoryPath: string;
40
40
  siteExportDirectoryPath: string;
41
41
 
42
- environments: string[];
42
+ environments: string[] | false;
43
43
  tags: string[];
44
44
 
45
45
  adapter: any; // @TODO: type this properly later
@@ -9,7 +9,7 @@ import {
9
9
  } from "@featurevisor/types";
10
10
 
11
11
  export interface DatafileOptions {
12
- environment: EnvironmentKey;
12
+ environment: EnvironmentKey | false;
13
13
  tag: string;
14
14
  datafilesDir?: string;
15
15
  }
@@ -23,8 +23,11 @@ export abstract class Adapter {
23
23
  abstract deleteEntity(entityType: EntityType, entityKey: string): Promise<void>;
24
24
 
25
25
  // state
26
- abstract readState(environment: EnvironmentKey): Promise<ExistingState>;
27
- abstract writeState(environment: EnvironmentKey, existingState: ExistingState): Promise<void>;
26
+ abstract readState(environment: EnvironmentKey | false): Promise<ExistingState>;
27
+ abstract writeState(
28
+ environment: EnvironmentKey | false,
29
+ existingState: ExistingState,
30
+ ): Promise<void>;
28
31
 
29
32
  // datafile
30
33
  abstract readDatafile(options: DatafileOptions): Promise<DatafileContent>;
@@ -35,11 +35,11 @@ export class Datasource {
35
35
  /**
36
36
  * State
37
37
  */
38
- readState(environment: EnvironmentKey): Promise<ExistingState> {
38
+ readState(environment: EnvironmentKey | false): Promise<ExistingState> {
39
39
  return this.adapter.readState(environment);
40
40
  }
41
41
 
42
- writeState(environment: EnvironmentKey, existingState: ExistingState) {
42
+ writeState(environment: EnvironmentKey | false, existingState: ExistingState) {
43
43
  return this.adapter.writeState(environment, existingState);
44
44
  }
45
45
 
@@ -23,9 +23,11 @@ const commitRegex = /^commit (\w+)\nAuthor: (.+) <(.+)>\nDate: (.+)\n\n(.+)/gm
23
23
 
24
24
  export function getExistingStateFilePath(
25
25
  projectConfig: ProjectConfig,
26
- environment: EnvironmentKey,
26
+ environment: EnvironmentKey | false,
27
27
  ): string {
28
- return path.join(projectConfig.stateDirectoryPath, `existing-state-${environment}.json`);
28
+ const fileName = environment ? `existing-state-${environment}.json` : `existing-state.json`;
29
+
30
+ return path.join(projectConfig.stateDirectoryPath, fileName);
29
31
  }
30
32
 
31
33
  export function getRevisionFilePath(projectConfig: ProjectConfig): string {
@@ -185,7 +187,11 @@ export class FilesystemAdapter extends Adapter {
185
187
  const fileName = `datafile-tag-${options.tag}.json`;
186
188
  const dir = options.datafilesDir || this.config.outputDirectoryPath;
187
189
 
188
- return path.join(dir, options.environment, fileName);
190
+ if (options.environment) {
191
+ return path.join(dir, options.environment, fileName);
192
+ }
193
+
194
+ return path.join(dir, fileName);
189
195
  }
190
196
 
191
197
  async readDatafile(options: DatafileOptions): Promise<DatafileContent> {
@@ -199,7 +205,9 @@ export class FilesystemAdapter extends Adapter {
199
205
  async writeDatafile(datafileContent: DatafileContent, options: DatafileOptions): Promise<void> {
200
206
  const dir = options.datafilesDir || this.config.outputDirectoryPath;
201
207
 
202
- const outputEnvironmentDirPath = path.join(dir, options.environment);
208
+ const outputEnvironmentDirPath = options.environment
209
+ ? path.join(dir, options.environment)
210
+ : dir;
203
211
  mkdirp.sync(outputEnvironmentDirPath);
204
212
 
205
213
  const outputFilePath = this.getDatafilePath(options);
@@ -50,7 +50,7 @@ function printHeader(message: string) {
50
50
  }
51
51
 
52
52
  export interface EvaluateOptions {
53
- environment: string;
53
+ environment?: string;
54
54
  feature: string;
55
55
  context: Record<string, unknown>;
56
56
  print?: boolean;
@@ -69,14 +69,14 @@ export interface Log {
69
69
  export async function evaluateFeature(deps: Dependencies, options: EvaluateOptions) {
70
70
  const { datasource, projectConfig } = deps;
71
71
 
72
- const existingState = await datasource.readState(options.environment);
72
+ const existingState = await datasource.readState(options.environment || false);
73
73
  const datafileContent = await buildDatafile(
74
74
  projectConfig,
75
75
  datasource,
76
76
  {
77
77
  schemaVersion: options.schemaVersion || SCHEMA_VERSION,
78
78
  revision: "include-all-features",
79
- environment: options.environment,
79
+ environment: options.environment || false,
80
80
  inflate: options.inflate,
81
81
  },
82
82
  existingState,
@@ -52,34 +52,70 @@ export async function findAllUsageInFeatures(deps: Dependencies): Promise<UsageI
52
52
  }
53
53
 
54
54
  // variable overrides inside variations
55
- projectConfig.environments.forEach((environment) => {
56
- if (feature.variations) {
57
- feature.variations.forEach((variation) => {
58
- if (variation.variables) {
59
- variation.variables.forEach((variable) => {
60
- if (variable.overrides) {
61
- variable.overrides.forEach((override) => {
62
- if (override.segments) {
63
- extractSegmentKeysFromGroupSegments(override.segments).forEach((segmentKey) =>
64
- usageInFeatures[featureKey].segments.add(segmentKey),
65
- );
66
- }
67
-
68
- if (override.conditions) {
69
- extractAttributeKeysFromConditions(override.conditions).forEach(
70
- (attributeKey) => usageInFeatures[featureKey].attributes.add(attributeKey),
71
- );
72
- }
73
- });
74
- }
75
- });
76
- }
77
- });
78
- }
55
+ if (feature.variations) {
56
+ feature.variations.forEach((variation) => {
57
+ if (variation.variables) {
58
+ variation.variables.forEach((variable) => {
59
+ if (variable.overrides) {
60
+ variable.overrides.forEach((override) => {
61
+ if (override.segments) {
62
+ extractSegmentKeysFromGroupSegments(override.segments).forEach((segmentKey) =>
63
+ usageInFeatures[featureKey].segments.add(segmentKey),
64
+ );
65
+ }
66
+
67
+ if (override.conditions) {
68
+ extractAttributeKeysFromConditions(override.conditions).forEach((attributeKey) =>
69
+ usageInFeatures[featureKey].attributes.add(attributeKey),
70
+ );
71
+ }
72
+ });
73
+ }
74
+ });
75
+ }
76
+ });
77
+ }
78
+
79
+ // with environments
80
+ if (Array.isArray(projectConfig.environments)) {
81
+ projectConfig.environments.forEach((environment) => {
82
+ if (!feature.environments) {
83
+ return;
84
+ }
85
+
86
+ // force
87
+ if (feature.environments[environment].force) {
88
+ feature.environments[environment].force?.forEach((force) => {
89
+ if (force.segments) {
90
+ extractSegmentKeysFromGroupSegments(force.segments).forEach((segmentKey) =>
91
+ usageInFeatures[featureKey].segments.add(segmentKey),
92
+ );
93
+ }
94
+
95
+ if (force.conditions) {
96
+ extractAttributeKeysFromConditions(force.conditions).forEach((attributeKey) =>
97
+ usageInFeatures[featureKey].attributes.add(attributeKey),
98
+ );
99
+ }
100
+ });
101
+ }
79
102
 
103
+ // rules
104
+ if (feature.environments[environment].rules) {
105
+ feature.environments[environment].rules?.forEach((rule) => {
106
+ extractSegmentKeysFromGroupSegments(rule.segments).forEach((segmentKey) =>
107
+ usageInFeatures[featureKey].segments.add(segmentKey),
108
+ );
109
+ });
110
+ }
111
+ });
112
+ }
113
+
114
+ // no environments
115
+ if (projectConfig.environments === false) {
80
116
  // force
81
- if (feature.environments[environment].force) {
82
- feature.environments[environment].force?.forEach((force) => {
117
+ if (feature.force) {
118
+ feature.force.forEach((force) => {
83
119
  if (force.segments) {
84
120
  extractSegmentKeysFromGroupSegments(force.segments).forEach((segmentKey) =>
85
121
  usageInFeatures[featureKey].segments.add(segmentKey),
@@ -95,14 +131,14 @@ export async function findAllUsageInFeatures(deps: Dependencies): Promise<UsageI
95
131
  }
96
132
 
97
133
  // rules
98
- if (feature.environments[environment].rules) {
99
- feature.environments[environment].rules?.forEach((rule) => {
134
+ if (feature.rules) {
135
+ feature.rules.forEach((rule) => {
100
136
  extractSegmentKeysFromGroupSegments(rule.segments).forEach((segmentKey) =>
101
137
  usageInFeatures[featureKey].segments.add(segmentKey),
102
138
  );
103
139
  });
104
140
  }
105
- });
141
+ }
106
142
  }
107
143
 
108
144
  return usageInFeatures;
@@ -227,93 +227,102 @@ export function getFeatureZodSchema(
227
227
 
228
228
  const groupSegmentsZodSchema = z.union([z.array(groupSegmentZodSchema), groupSegmentZodSchema]);
229
229
 
230
- const environmentZodSchema = z
231
- .object({
232
- expose: z
233
- .union([
234
- z.boolean(),
235
- z.array(z.string().refine((value) => projectConfig.tags.includes(value))),
236
- ])
237
- .optional(),
238
- rules: z
239
- .array(
240
- z
241
- .object({
242
- key: z.string(),
243
- description: z.string().optional(),
244
- segments: groupSegmentsZodSchema,
245
- percentage: z.number().min(0).max(100),
246
-
247
- enabled: z.boolean().optional(),
248
- variation: variationValueZodSchema.optional(),
249
- variables: z.record(variableValueZodSchema).optional(),
250
- })
251
- .strict(),
252
- )
253
-
254
- // must have at least one rule
255
- .refine(
256
- (value) => value.length > 0,
257
- () => ({
258
- message: "Must have at least one rule",
259
- }),
260
- )
230
+ const exposeSchema = z
231
+ .union([z.boolean(), z.array(z.string().refine((value) => projectConfig.tags.includes(value)))])
232
+ .optional();
233
+
234
+ const rulesSchema = z
235
+ .array(
236
+ z
237
+ .object({
238
+ key: z.string(),
239
+ description: z.string().optional(),
240
+ segments: groupSegmentsZodSchema,
241
+ percentage: z.number().min(0).max(100),
242
+
243
+ enabled: z.boolean().optional(),
244
+ variation: variationValueZodSchema.optional(),
245
+ variables: z.record(variableValueZodSchema).optional(),
246
+ })
247
+ .strict(),
248
+ )
249
+
250
+ // must have at least one rule
251
+ .refine(
252
+ (value) => value.length > 0,
253
+ () => ({
254
+ message: "Must have at least one rule",
255
+ }),
256
+ )
257
+
258
+ // duplicate rules
259
+ .refine(
260
+ (value) => {
261
+ const keys = value.map((v) => v.key);
262
+ return keys.length === new Set(keys).size;
263
+ },
264
+ (value) => ({
265
+ message: "Duplicate rule keys found: " + value.map((v) => v.key).join(", "),
266
+ }),
267
+ )
261
268
 
262
- // duplicate rules
263
- .refine(
264
- (value) => {
265
- const keys = value.map((v) => v.key);
266
- return keys.length === new Set(keys).size;
267
- },
268
- (value) => ({
269
- message: "Duplicate rule keys found: " + value.map((v) => v.key).join(", "),
270
- }),
271
- )
269
+ // enforce catch-all rule
270
+ .refine(
271
+ (value) => {
272
+ if (!projectConfig.enforceCatchAllRule) {
273
+ return true;
274
+ }
272
275
 
273
- // enforce catch-all rule
274
- .refine(
275
- (value) => {
276
- if (!projectConfig.enforceCatchAllRule) {
277
- return true;
278
- }
276
+ const hasCatchAllAsLastRule = value[value.length - 1].segments === "*";
277
+ return hasCatchAllAsLastRule;
278
+ },
279
+ () => ({
280
+ message: `Missing catch-all rule with \`segments: "*"\` at the end`,
281
+ }),
282
+ );
283
+
284
+ const forceSchema = z
285
+ .array(
286
+ z.union([
287
+ z
288
+ .object({
289
+ segments: groupSegmentsZodSchema,
290
+ enabled: z.boolean().optional(),
291
+ variation: variationValueZodSchema.optional(),
292
+ variables: z.record(variableValueZodSchema).optional(),
293
+ })
294
+ .strict(),
295
+ z
296
+ .object({
297
+ conditions: conditionsZodSchema,
298
+ enabled: z.boolean().optional(),
299
+ variation: variationValueZodSchema.optional(),
300
+ variables: z.record(variableValueZodSchema).optional(),
301
+ })
302
+ .strict(),
303
+ ]),
304
+ )
305
+ .optional();
279
306
 
280
- const hasCatchAllAsLastRule = value[value.length - 1].segments === "*";
281
- return hasCatchAllAsLastRule;
282
- },
283
- () => ({
284
- message: `Missing catch-all rule with \`segments: "*"\` at the end`,
285
- }),
286
- ),
287
- force: z
288
- .array(
289
- z.union([
290
- z
291
- .object({
292
- segments: groupSegmentsZodSchema,
293
- enabled: z.boolean().optional(),
294
- variation: variationValueZodSchema.optional(),
295
- variables: z.record(variableValueZodSchema).optional(),
296
- })
297
- .strict(),
298
- z
299
- .object({
300
- conditions: conditionsZodSchema,
301
- enabled: z.boolean().optional(),
302
- variation: variationValueZodSchema.optional(),
303
- variables: z.record(variableValueZodSchema).optional(),
304
- })
305
- .strict(),
306
- ]),
307
- )
308
- .optional(),
307
+ const environmentZodSchema = z
308
+ .object({
309
+ expose: exposeSchema,
310
+ rules: rulesSchema,
311
+ force: forceSchema,
309
312
  })
310
313
  .strict();
311
314
 
312
- const allEnvironmentsSchema = {};
313
- projectConfig.environments.forEach((environmentKey) => {
314
- allEnvironmentsSchema[environmentKey] = environmentZodSchema;
315
- });
316
- const allEnvironmentsZodSchema = z.object(allEnvironmentsSchema).strict();
315
+ let allEnvironmentsZodSchema: z.ZodTypeAny = z.never();
316
+
317
+ if (Array.isArray(projectConfig.environments)) {
318
+ const allEnvironmentsSchema = {};
319
+
320
+ projectConfig.environments.forEach((environmentKey) => {
321
+ allEnvironmentsSchema[environmentKey] = environmentZodSchema;
322
+ });
323
+
324
+ allEnvironmentsZodSchema = z.object(allEnvironmentsSchema).strict();
325
+ }
317
326
 
318
327
  const attributeKeyZodSchema = z.string().refine(
319
328
  (value) => value === "*" || availableAttributeKeys.includes(value),
@@ -447,7 +456,16 @@ export function getFeatureZodSchema(
447
456
  }),
448
457
  )
449
458
  .optional(),
450
- environments: allEnvironmentsZodSchema,
459
+
460
+ // with environments
461
+ environments: Array.isArray(projectConfig.environments)
462
+ ? allEnvironmentsZodSchema
463
+ : z.never().optional(),
464
+
465
+ // no environments
466
+ expose: projectConfig.environments === false ? exposeSchema : z.never().optional(),
467
+ rules: projectConfig.environments === false ? rulesSchema : z.never().optional(),
468
+ force: projectConfig.environments === false ? forceSchema : z.never().optional(),
451
469
  })
452
470
  .strict()
453
471
  .superRefine((value, ctx) => {
@@ -59,24 +59,29 @@ export function getTestsZodSchema(
59
59
  // because of supporting matrix
60
60
  z.string(),
61
61
  ]),
62
- environment: z.string().refine(
63
- (value) => {
64
- if (value.indexOf("${{") === 0) {
65
- // allow unknown strings for matrix
66
- return true;
67
- }
62
+ environment: Array.isArray(projectConfig.environments)
63
+ ? z.string().refine(
64
+ (value) => {
65
+ if (value.indexOf("${{") === 0) {
66
+ // allow unknown strings for matrix
67
+ return true;
68
+ }
68
69
 
69
- // otherwise only known environments should be passed
70
- if (projectConfig.environments.includes(value)) {
71
- return true;
72
- }
70
+ // otherwise only known environments should be passed
71
+ if (
72
+ Array.isArray(projectConfig.environments) &&
73
+ projectConfig.environments.includes(value)
74
+ ) {
75
+ return true;
76
+ }
73
77
 
74
- return false;
75
- },
76
- (value) => ({
77
- message: `Unknown environment "${value}"`,
78
- }),
79
- ),
78
+ return false;
79
+ },
80
+ (value) => ({
81
+ message: `Unknown environment "${value}"`,
82
+ }),
83
+ )
84
+ : z.never().optional(),
80
85
  context: z.record(z.unknown()),
81
86
  expectedToBeEnabled: z.boolean(),
82
87
  expectedVariation: z.string().optional(),