@featurevisor/core 2.14.0 → 2.16.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 (101) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/coverage/clover.xml +774 -652
  3. package/coverage/coverage-final.json +19 -18
  4. package/coverage/lcov-report/builder/allocator.ts.html +1 -1
  5. package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
  6. package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
  7. package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
  8. package/coverage/lcov-report/builder/index.html +1 -1
  9. package/coverage/lcov-report/builder/mutateVariables.ts.html +1 -1
  10. package/coverage/lcov-report/builder/mutator.ts.html +1 -1
  11. package/coverage/lcov-report/builder/revision.ts.html +1 -1
  12. package/coverage/lcov-report/builder/traffic.ts.html +1 -1
  13. package/coverage/lcov-report/config/index.html +30 -15
  14. package/coverage/lcov-report/config/index.ts.html +88 -0
  15. package/coverage/lcov-report/config/projectConfig.ts.html +79 -46
  16. package/coverage/lcov-report/datasource/adapter.ts.html +74 -11
  17. package/coverage/lcov-report/datasource/datasource.ts.html +76 -25
  18. package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +690 -66
  19. package/coverage/lcov-report/datasource/index.html +35 -35
  20. package/coverage/lcov-report/datasource/index.ts.html +1 -1
  21. package/coverage/lcov-report/index.html +43 -43
  22. package/coverage/lcov-report/linter/attributeSchema.ts.html +4 -4
  23. package/coverage/lcov-report/linter/checkCircularDependency.ts.html +1 -1
  24. package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +1 -1
  25. package/coverage/lcov-report/linter/conditionSchema.ts.html +7 -7
  26. package/coverage/lcov-report/linter/featureSchema.ts.html +152 -164
  27. package/coverage/lcov-report/linter/groupSchema.ts.html +3 -3
  28. package/coverage/lcov-report/linter/index.html +34 -34
  29. package/coverage/lcov-report/linter/lintProject.ts.html +428 -83
  30. package/coverage/lcov-report/linter/mutationNotation.ts.html +3 -3
  31. package/coverage/lcov-report/linter/printError.ts.html +9 -9
  32. package/coverage/lcov-report/linter/schema.ts.html +4 -4
  33. package/coverage/lcov-report/linter/segmentSchema.ts.html +3 -3
  34. package/coverage/lcov-report/linter/testSchema.ts.html +6 -6
  35. package/coverage/lcov-report/list/index.html +1 -1
  36. package/coverage/lcov-report/list/matrix.ts.html +1 -1
  37. package/coverage/lcov-report/parsers/index.html +1 -1
  38. package/coverage/lcov-report/parsers/index.ts.html +4 -4
  39. package/coverage/lcov-report/parsers/json.ts.html +2 -2
  40. package/coverage/lcov-report/parsers/yml.ts.html +6 -6
  41. package/coverage/lcov-report/tester/cliFormat.ts.html +1 -1
  42. package/coverage/lcov-report/tester/helpers.ts.html +1 -1
  43. package/coverage/lcov-report/tester/index.html +1 -1
  44. package/coverage/lcov-report/utils/git.ts.html +118 -16
  45. package/coverage/lcov-report/utils/index.html +14 -14
  46. package/coverage/lcov.info +1374 -1107
  47. package/lib/config/projectConfig.d.ts +3 -0
  48. package/lib/config/projectConfig.js +7 -1
  49. package/lib/config/projectConfig.js.map +1 -1
  50. package/lib/config/projectConfig.spec.d.ts +1 -0
  51. package/lib/config/projectConfig.spec.js +24 -0
  52. package/lib/config/projectConfig.spec.js.map +1 -0
  53. package/lib/datasource/adapter.d.ts +7 -0
  54. package/lib/datasource/adapter.js +7 -0
  55. package/lib/datasource/adapter.js.map +1 -1
  56. package/lib/datasource/datasource.d.ts +3 -1
  57. package/lib/datasource/datasource.js +6 -0
  58. package/lib/datasource/datasource.js.map +1 -1
  59. package/lib/datasource/filesystemAdapter.d.ts +8 -0
  60. package/lib/datasource/filesystemAdapter.js +134 -7
  61. package/lib/datasource/filesystemAdapter.js.map +1 -1
  62. package/lib/datasource/filesystemAdapter.spec.d.ts +1 -0
  63. package/lib/datasource/filesystemAdapter.spec.js +88 -0
  64. package/lib/datasource/filesystemAdapter.spec.js.map +1 -0
  65. package/lib/generate-code/index.d.ts +3 -0
  66. package/lib/generate-code/index.js +16 -1
  67. package/lib/generate-code/index.js.map +1 -1
  68. package/lib/generate-code/typescript.d.ts +6 -1
  69. package/lib/generate-code/typescript.js +193 -42
  70. package/lib/generate-code/typescript.js.map +1 -1
  71. package/lib/linter/conditionSchema.spec.js +2 -0
  72. package/lib/linter/conditionSchema.spec.js.map +1 -1
  73. package/lib/linter/featureSchema.js +46 -49
  74. package/lib/linter/featureSchema.js.map +1 -1
  75. package/lib/linter/featureSchema.spec.js +17 -0
  76. package/lib/linter/featureSchema.spec.js.map +1 -1
  77. package/lib/linter/lintProject.d.ts +2 -0
  78. package/lib/linter/lintProject.js +85 -8
  79. package/lib/linter/lintProject.js.map +1 -1
  80. package/lib/linter/lintProject.spec.js +37 -0
  81. package/lib/linter/lintProject.spec.js.map +1 -1
  82. package/lib/linter/segmentSchema.spec.js +2 -0
  83. package/lib/linter/segmentSchema.spec.js.map +1 -1
  84. package/lib/utils/git.js +32 -7
  85. package/lib/utils/git.js.map +1 -1
  86. package/package.json +2 -2
  87. package/src/config/projectConfig.spec.ts +31 -0
  88. package/src/config/projectConfig.ts +11 -0
  89. package/src/datasource/adapter.ts +21 -0
  90. package/src/datasource/datasource.ts +18 -1
  91. package/src/datasource/filesystemAdapter.spec.ts +153 -0
  92. package/src/datasource/filesystemAdapter.ts +216 -8
  93. package/src/generate-code/index.ts +20 -1
  94. package/src/generate-code/typescript.ts +266 -64
  95. package/src/linter/conditionSchema.spec.ts +2 -0
  96. package/src/linter/featureSchema.spec.ts +21 -0
  97. package/src/linter/featureSchema.ts +84 -88
  98. package/src/linter/lintProject.spec.ts +74 -0
  99. package/src/linter/lintProject.ts +123 -8
  100. package/src/linter/segmentSchema.spec.ts +2 -0
  101. package/src/utils/git.ts +41 -7
@@ -0,0 +1,153 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+
5
+ import { getProjectConfig } from "../config";
6
+ import { Datasource } from "./datasource";
7
+
8
+ function writeFile(filePath: string, content: string) {
9
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
10
+ fs.writeFileSync(filePath, content, "utf8");
11
+ }
12
+
13
+ function createSplitProject(configBody: string) {
14
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "featurevisor-fs-adapter-"));
15
+ writeFile(path.join(root, "featurevisor.config.js"), configBody);
16
+
17
+ return root;
18
+ }
19
+
20
+ describe("core: filesystemAdapter (splitByEnvironment)", () => {
21
+ it("merges environment files into readFeature output", async () => {
22
+ const root = createSplitProject(
23
+ "module.exports = { environments: ['staging', 'production'], splitByEnvironment: true };",
24
+ );
25
+
26
+ writeFile(
27
+ path.join(root, "features", "checkout.yml"),
28
+ [
29
+ "key: checkout",
30
+ "description: Checkout feature",
31
+ "tags:",
32
+ " - all",
33
+ "bucketBy: userId",
34
+ ].join("\n"),
35
+ );
36
+
37
+ writeFile(
38
+ path.join(root, "environments", "staging", "checkout.yml"),
39
+ ["rules:", " - key: everyone", " segments: '*'", " percentage: 100"].join("\n"),
40
+ );
41
+
42
+ writeFile(
43
+ path.join(root, "environments", "production", "checkout.yml"),
44
+ [
45
+ "rules:",
46
+ " - key: everyone",
47
+ " segments: '*'",
48
+ " percentage: 0",
49
+ "expose: false",
50
+ ].join("\n"),
51
+ );
52
+
53
+ const config = getProjectConfig(root);
54
+ const datasource = new Datasource(config, root);
55
+ const feature = await datasource.readFeature("checkout");
56
+ const rulesByEnvironment = feature.rules as Record<string, any[]>;
57
+ const exposeByEnvironment = feature.expose as Record<string, unknown>;
58
+
59
+ expect(rulesByEnvironment?.staging?.[0]?.percentage).toBe(100);
60
+ expect(rulesByEnvironment?.production?.[0]?.percentage).toBe(0);
61
+ expect(exposeByEnvironment?.production).toBe(false);
62
+ });
63
+
64
+ it("throws when any environment feature file is missing", async () => {
65
+ const root = createSplitProject(
66
+ "module.exports = { environments: ['staging', 'production'], splitByEnvironment: true };",
67
+ );
68
+
69
+ writeFile(
70
+ path.join(root, "features", "myFeature.yml"),
71
+ ["key: myFeature", "description: My feature", "tags:", " - all", "bucketBy: userId"].join(
72
+ "\n",
73
+ ),
74
+ );
75
+
76
+ writeFile(
77
+ path.join(root, "environments", "staging", "myFeature.yml"),
78
+ ["rules:", " - key: everyone", " segments: '*'", " percentage: 100"].join("\n"),
79
+ );
80
+
81
+ const config = getProjectConfig(root);
82
+ const datasource = new Datasource(config, root);
83
+
84
+ await expect(datasource.readFeature("myFeature")).rejects.toMatchObject({
85
+ featurevisorFilePath: expect.stringContaining(
86
+ path.join("environments", "production", "myFeature.yml"),
87
+ ),
88
+ });
89
+ });
90
+
91
+ it("throws when base feature file still defines rules in split mode", async () => {
92
+ const root = createSplitProject(
93
+ "module.exports = { environments: ['staging'], splitByEnvironment: true };",
94
+ );
95
+
96
+ writeFile(
97
+ path.join(root, "features", "foo.yml"),
98
+ [
99
+ "key: foo",
100
+ "description: Foo",
101
+ "tags:",
102
+ " - all",
103
+ "bucketBy: userId",
104
+ "rules:",
105
+ " staging:",
106
+ " - key: everyone",
107
+ " segments: '*'",
108
+ " percentage: 100",
109
+ ].join("\n"),
110
+ );
111
+
112
+ writeFile(
113
+ path.join(root, "environments", "staging", "foo.yml"),
114
+ ["rules:", " - key: everyone", " segments: '*'", " percentage: 100"].join("\n"),
115
+ );
116
+
117
+ const config = getProjectConfig(root);
118
+ const datasource = new Datasource(config, root);
119
+
120
+ await expect(datasource.readFeature("foo")).rejects.toThrow(
121
+ "base file must not define rules, force, or expose when splitByEnvironment=true",
122
+ );
123
+ });
124
+
125
+ it("throws when environment file has unknown keys", async () => {
126
+ const root = createSplitProject(
127
+ "module.exports = { environments: ['staging'], splitByEnvironment: true };",
128
+ );
129
+
130
+ writeFile(
131
+ path.join(root, "features", "foo.yml"),
132
+ ["key: foo", "description: Foo", "tags:", " - all", "bucketBy: userId"].join("\n"),
133
+ );
134
+
135
+ writeFile(
136
+ path.join(root, "environments", "staging", "foo.yml"),
137
+ [
138
+ "rules:",
139
+ " - key: everyone",
140
+ " segments: '*'",
141
+ " percentage: 100",
142
+ "extraKey: not allowed",
143
+ ].join("\n"),
144
+ );
145
+
146
+ const config = getProjectConfig(root);
147
+ const datasource = new Datasource(config, root);
148
+
149
+ await expect(datasource.readFeature("foo")).rejects.toThrow(
150
+ "Unknown key(s) in environment feature file: extraKey",
151
+ );
152
+ });
153
+ });
@@ -11,6 +11,10 @@ import type {
11
11
  Commit,
12
12
  CommitHash,
13
13
  HistoryEntity,
14
+ ParsedFeature,
15
+ Expose,
16
+ Force,
17
+ Rule,
14
18
  } from "@featurevisor/types";
15
19
 
16
20
  import { Adapter, DatafileOptions } from "./adapter";
@@ -18,6 +22,18 @@ import { ProjectConfig } from "../config";
18
22
  import { CustomParser } from "../parsers";
19
23
  import { getCommit } from "../utils/git";
20
24
 
25
+ const FEATURE_ENVIRONMENT_ALLOWED_KEYS = ["rules", "force", "expose"];
26
+
27
+ class FeatureSplitConfigError extends Error {
28
+ featurevisorFilePath: string;
29
+
30
+ constructor(filePath: string, message: string) {
31
+ super(message);
32
+ this.featurevisorFilePath = filePath;
33
+ this.name = "FeatureSplitConfigError";
34
+ }
35
+ }
36
+
21
37
  export function getExistingStateFilePath(
22
38
  projectConfig: ProjectConfig,
23
39
  environment: EnvironmentKey | false,
@@ -54,6 +70,12 @@ export function getAllEntityFilePathsRecursively(directoryPath, extension) {
54
70
  return entities;
55
71
  }
56
72
 
73
+ function isWithinDirectory(directoryPath: string, fileDirectoryPath: string): boolean {
74
+ return (
75
+ fileDirectoryPath === directoryPath || fileDirectoryPath.startsWith(directoryPath + path.sep)
76
+ );
77
+ }
78
+
57
79
  export class FilesystemAdapter extends Adapter {
58
80
  private parser: CustomParser;
59
81
 
@@ -91,6 +113,57 @@ export class FilesystemAdapter extends Adapter {
91
113
  return path.join(basePath, `${relativeEntityPath}.${this.parser.extension}`);
92
114
  }
93
115
 
116
+ getFeatureEnvironmentPath(featureKey: string, environment: string): string {
117
+ const relativeEntityPath = featureKey.replace(/\//g, path.sep);
118
+
119
+ return path.join(
120
+ this.config.environmentsDirectoryPath,
121
+ environment,
122
+ `${relativeEntityPath}.${this.parser.extension}`,
123
+ );
124
+ }
125
+
126
+ private assertSplitByEnvironmentIsValidFeatureBase(
127
+ baseFeature: ParsedFeature,
128
+ featurePath: string,
129
+ ) {
130
+ if (
131
+ typeof baseFeature.rules !== "undefined" ||
132
+ typeof baseFeature.force !== "undefined" ||
133
+ typeof baseFeature.expose !== "undefined"
134
+ ) {
135
+ throw new FeatureSplitConfigError(
136
+ featurePath,
137
+ `Feature "${baseFeature.key}" base file must not define rules, force, or expose when splitByEnvironment=true`,
138
+ );
139
+ }
140
+ }
141
+
142
+ private readFeatureEnvironmentEntity(featureKey: string, environment: string) {
143
+ const featureEnvironmentPath = this.getFeatureEnvironmentPath(featureKey, environment);
144
+
145
+ if (!fs.existsSync(featureEnvironmentPath)) {
146
+ throw new FeatureSplitConfigError(
147
+ featureEnvironmentPath,
148
+ `Missing environment feature file: environments/${environment}/${featureKey}.${this.parser.extension}`,
149
+ );
150
+ }
151
+
152
+ const content = fs.readFileSync(featureEnvironmentPath, "utf8");
153
+ const parsed = this.parser.parse<Record<string, unknown>>(content, featureEnvironmentPath);
154
+ const keys = Object.keys(parsed);
155
+ const unknownKeys = keys.filter((key) => FEATURE_ENVIRONMENT_ALLOWED_KEYS.indexOf(key) === -1);
156
+
157
+ if (unknownKeys.length > 0) {
158
+ throw new FeatureSplitConfigError(
159
+ featureEnvironmentPath,
160
+ `Unknown key(s) in environment feature file: ${unknownKeys.join(", ")}`,
161
+ );
162
+ }
163
+
164
+ return parsed;
165
+ }
166
+
94
167
  async listEntities(entityType: EntityType): Promise<string[]> {
95
168
  const directoryPath = this.getEntityDirectoryPath(entityType);
96
169
  const filePaths = getAllEntityFilePathsRecursively(directoryPath, this.parser.extension);
@@ -118,6 +191,50 @@ export class FilesystemAdapter extends Adapter {
118
191
  }
119
192
 
120
193
  async readEntity<T>(entityType: EntityType, entityKey: string): Promise<T> {
194
+ if (entityType === "feature" && this.config.splitByEnvironment) {
195
+ const featurePath = this.getEntityPath(entityType, entityKey);
196
+ const featureContent = fs.readFileSync(featurePath, "utf8");
197
+ const baseFeature = this.parser.parse<ParsedFeature>(featureContent, featurePath);
198
+
199
+ this.assertSplitByEnvironmentIsValidFeatureBase(baseFeature, featurePath);
200
+
201
+ if (!Array.isArray(this.config.environments)) {
202
+ throw new FeatureSplitConfigError(
203
+ featurePath,
204
+ "splitByEnvironment=true requires environments to be configured as an array",
205
+ );
206
+ }
207
+
208
+ const rulesByEnvironment: Record<string, Rule[]> = {};
209
+ const forceByEnvironment: Record<string, Force[]> = {};
210
+ const exposeByEnvironment: Record<string, Expose> = {};
211
+
212
+ for (const environment of this.config.environments) {
213
+ const envFeature = this.readFeatureEnvironmentEntity(entityKey, environment);
214
+
215
+ if (typeof envFeature.rules !== "undefined") {
216
+ rulesByEnvironment[environment] = envFeature.rules as Rule[];
217
+ }
218
+
219
+ if (typeof envFeature.force !== "undefined") {
220
+ forceByEnvironment[environment] = envFeature.force as Force[];
221
+ }
222
+
223
+ if (typeof envFeature.expose !== "undefined") {
224
+ exposeByEnvironment[environment] = envFeature.expose as Expose;
225
+ }
226
+ }
227
+
228
+ const mergedFeature: ParsedFeature = {
229
+ ...baseFeature,
230
+ rules: Object.keys(rulesByEnvironment).length > 0 ? rulesByEnvironment : undefined,
231
+ force: Object.keys(forceByEnvironment).length > 0 ? forceByEnvironment : undefined,
232
+ expose: Object.keys(exposeByEnvironment).length > 0 ? exposeByEnvironment : undefined,
233
+ };
234
+
235
+ return mergedFeature as T;
236
+ }
237
+
121
238
  const filePath = this.getEntityPath(entityType, entityKey);
122
239
  const entityContent = fs.readFileSync(filePath, "utf8");
123
240
 
@@ -306,6 +423,16 @@ export class FilesystemAdapter extends Adapter {
306
423
 
307
424
  if (entityType && entityKey) {
308
425
  pathPatterns = [this.getEntityPath(entityType, entityKey)];
426
+
427
+ if (
428
+ entityType === "feature" &&
429
+ this.config.splitByEnvironment &&
430
+ Array.isArray(this.config.environments)
431
+ ) {
432
+ for (const environment of this.config.environments) {
433
+ pathPatterns.push(this.getFeatureEnvironmentPath(entityKey, environment));
434
+ }
435
+ }
309
436
  } else if (entityType) {
310
437
  if (entityType === "attribute") {
311
438
  pathPatterns = [this.config.attributesDirectoryPath];
@@ -313,6 +440,9 @@ export class FilesystemAdapter extends Adapter {
313
440
  pathPatterns = [this.config.segmentsDirectoryPath];
314
441
  } else if (entityType === "feature") {
315
442
  pathPatterns = [this.config.featuresDirectoryPath];
443
+ if (this.config.splitByEnvironment) {
444
+ pathPatterns.push(this.config.environmentsDirectoryPath);
445
+ }
316
446
  } else if (entityType === "group") {
317
447
  pathPatterns = [this.config.groupsDirectoryPath];
318
448
  } else if (entityType === "schema") {
@@ -323,6 +453,7 @@ export class FilesystemAdapter extends Adapter {
323
453
  } else {
324
454
  pathPatterns = [
325
455
  this.config.featuresDirectoryPath,
456
+ ...(this.config.splitByEnvironment ? [this.config.environmentsDirectoryPath] : []),
326
457
  this.config.attributesDirectoryPath,
327
458
  this.config.segmentsDirectoryPath,
328
459
  this.config.groupsDirectoryPath,
@@ -334,6 +465,34 @@ export class FilesystemAdapter extends Adapter {
334
465
  return pathPatterns.map((p) => p.replace((this.rootDirectoryPath as string) + path.sep, ""));
335
466
  }
336
467
 
468
+ async getFeatureSourcePaths(featureKey: string) {
469
+ const baseFilePath = this.getEntityPath("feature", featureKey);
470
+ const environmentFilePaths: Record<string, string> = {};
471
+
472
+ if (this.config.splitByEnvironment && Array.isArray(this.config.environments)) {
473
+ for (const environment of this.config.environments) {
474
+ environmentFilePaths[environment] = this.getFeatureEnvironmentPath(featureKey, environment);
475
+ }
476
+ }
477
+
478
+ return {
479
+ baseFilePath,
480
+ environmentFilePaths,
481
+ };
482
+ }
483
+
484
+ async getFeaturePropertySourcePath(
485
+ featureKey: string,
486
+ _property: "rules" | "force" | "expose",
487
+ environment?: string,
488
+ ) {
489
+ if (this.config.splitByEnvironment && environment) {
490
+ return this.getFeatureEnvironmentPath(featureKey, environment);
491
+ }
492
+
493
+ return this.getEntityPath("feature", featureKey);
494
+ }
495
+
337
496
  async listHistoryEntries(entityType?: EntityType, entityKey?: string): Promise<HistoryEntry[]> {
338
497
  const pathPatterns = this.getPathPatterns(entityType, entityKey);
339
498
  const rawHistory = await this.getRawHistory(pathPatterns);
@@ -361,26 +520,75 @@ export class FilesystemAdapter extends Adapter {
361
520
  const absolutePath = path.join(this.rootDirectoryPath as string, relativePath);
362
521
  const fileName = absolutePath.split(path.sep).pop() as string;
363
522
  const relativeDir = path.dirname(absolutePath);
364
-
365
- const key = fileName.replace("." + this.parser.extension, "");
523
+ const extensionWithDot = "." + this.parser.extension;
524
+ const key = fileName.replace(extensionWithDot, "");
366
525
 
367
526
  let type: EntityType = "attribute";
368
- if (relativeDir === this.config.attributesDirectoryPath) {
527
+ if (isWithinDirectory(this.config.attributesDirectoryPath, relativeDir)) {
369
528
  type = "attribute";
370
- } else if (relativeDir === this.config.segmentsDirectoryPath) {
529
+ } else if (isWithinDirectory(this.config.segmentsDirectoryPath, relativeDir)) {
371
530
  type = "segment";
372
- } else if (relativeDir === this.config.featuresDirectoryPath) {
531
+ } else if (isWithinDirectory(this.config.featuresDirectoryPath, relativeDir)) {
373
532
  type = "feature";
374
- } else if (relativeDir === this.config.groupsDirectoryPath) {
533
+ } else if (
534
+ this.config.splitByEnvironment &&
535
+ isWithinDirectory(this.config.environmentsDirectoryPath, relativeDir)
536
+ ) {
537
+ type = "feature";
538
+ } else if (isWithinDirectory(this.config.groupsDirectoryPath, relativeDir)) {
375
539
  type = "group";
376
- } else if (relativeDir === this.config.schemasDirectoryPath) {
540
+ } else if (isWithinDirectory(this.config.schemasDirectoryPath, relativeDir)) {
377
541
  type = "schema";
378
- } else if (relativeDir === this.config.testsDirectoryPath) {
542
+ } else if (isWithinDirectory(this.config.testsDirectoryPath, relativeDir)) {
379
543
  type = "test";
380
544
  } else {
381
545
  continue;
382
546
  }
383
547
 
548
+ if (type === "feature") {
549
+ if (
550
+ this.config.splitByEnvironment &&
551
+ relativePath.startsWith(
552
+ this.config.environmentsDirectoryPath.replace(
553
+ (this.rootDirectoryPath as string) + path.sep,
554
+ "",
555
+ ) + path.sep,
556
+ )
557
+ ) {
558
+ const featureRelativePath = relativePath
559
+ .replace(
560
+ this.config.environmentsDirectoryPath.replace(
561
+ (this.rootDirectoryPath as string) + path.sep,
562
+ "",
563
+ ) + path.sep,
564
+ "",
565
+ )
566
+ .split(path.sep)
567
+ .slice(1)
568
+ .join(path.sep)
569
+ .replace(extensionWithDot, "");
570
+
571
+ entities.push({
572
+ type,
573
+ key: featureRelativePath.replace(/\\/g, "/"),
574
+ });
575
+
576
+ continue;
577
+ }
578
+
579
+ const baseRelativePath = absolutePath
580
+ .replace(this.config.featuresDirectoryPath + path.sep, "")
581
+ .replace(extensionWithDot, "")
582
+ .replace(/\\/g, "/");
583
+
584
+ entities.push({
585
+ type,
586
+ key: baseRelativePath,
587
+ });
588
+
589
+ continue;
590
+ }
591
+
384
592
  entities.push({
385
593
  type,
386
594
  key,
@@ -10,6 +10,9 @@ export const ALLOWED_LANGUAGES_FOR_CODE_GENERATION = ["typescript"];
10
10
  export interface GenerateCodeCLIOptions {
11
11
  language: string;
12
12
  outDir: string;
13
+ tag?: string | string[];
14
+ react?: boolean;
15
+ individualFeatures?: boolean;
13
16
  }
14
17
 
15
18
  export async function generateCodeForProject(
@@ -44,7 +47,11 @@ export async function generateCodeForProject(
44
47
  }
45
48
 
46
49
  if (cliOptions.language === "typescript") {
47
- return await generateTypeScriptCodeForProject(deps, absolutePath);
50
+ return await generateTypeScriptCodeForProject(deps, absolutePath, {
51
+ tag: cliOptions.tag,
52
+ react: cliOptions.react,
53
+ individualFeatures: cliOptions.individualFeatures,
54
+ });
48
55
  }
49
56
 
50
57
  throw new Error(`Language ${cliOptions.language} is not supported`);
@@ -63,6 +70,14 @@ export const generateCodePlugin: Plugin = {
63
70
  {
64
71
  language: parsed.language,
65
72
  outDir: parsed.outDir,
73
+ tag: parsed.tag,
74
+ react: parsed.react,
75
+ individualFeatures:
76
+ parsed.individualFeatures !== undefined
77
+ ? Boolean(parsed.individualFeatures)
78
+ : parsed["individual-features"] !== undefined
79
+ ? Boolean(parsed["individual-features"])
80
+ : true,
66
81
  },
67
82
  );
68
83
  },
@@ -71,5 +86,9 @@ export const generateCodePlugin: Plugin = {
71
86
  command: "generate-code --language typescript --out-dir src/generated",
72
87
  description: "Generate TypeScript code for the project",
73
88
  },
89
+ {
90
+ command: "generate-code --language typescript --out-dir src/generated --tag web --react",
91
+ description: "Generate TypeScript and React helper code for tagged features",
92
+ },
74
93
  ],
75
94
  };