@featurevisor/core 2.14.0 → 2.15.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/CHANGELOG.md +11 -0
- package/coverage/clover.xml +774 -652
- package/coverage/coverage-final.json +19 -18
- package/coverage/lcov-report/builder/allocator.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
- package/coverage/lcov-report/builder/index.html +1 -1
- package/coverage/lcov-report/builder/mutateVariables.ts.html +1 -1
- package/coverage/lcov-report/builder/mutator.ts.html +1 -1
- package/coverage/lcov-report/builder/revision.ts.html +1 -1
- package/coverage/lcov-report/builder/traffic.ts.html +1 -1
- package/coverage/lcov-report/config/index.html +30 -15
- package/coverage/lcov-report/config/index.ts.html +88 -0
- package/coverage/lcov-report/config/projectConfig.ts.html +79 -46
- package/coverage/lcov-report/datasource/adapter.ts.html +74 -11
- package/coverage/lcov-report/datasource/datasource.ts.html +76 -25
- package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +690 -66
- package/coverage/lcov-report/datasource/index.html +35 -35
- package/coverage/lcov-report/datasource/index.ts.html +1 -1
- package/coverage/lcov-report/index.html +43 -43
- package/coverage/lcov-report/linter/attributeSchema.ts.html +4 -4
- package/coverage/lcov-report/linter/checkCircularDependency.ts.html +1 -1
- package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +1 -1
- package/coverage/lcov-report/linter/conditionSchema.ts.html +7 -7
- package/coverage/lcov-report/linter/featureSchema.ts.html +152 -164
- package/coverage/lcov-report/linter/groupSchema.ts.html +3 -3
- package/coverage/lcov-report/linter/index.html +34 -34
- package/coverage/lcov-report/linter/lintProject.ts.html +428 -83
- package/coverage/lcov-report/linter/mutationNotation.ts.html +3 -3
- package/coverage/lcov-report/linter/printError.ts.html +9 -9
- package/coverage/lcov-report/linter/schema.ts.html +4 -4
- package/coverage/lcov-report/linter/segmentSchema.ts.html +3 -3
- package/coverage/lcov-report/linter/testSchema.ts.html +6 -6
- package/coverage/lcov-report/list/index.html +1 -1
- package/coverage/lcov-report/list/matrix.ts.html +1 -1
- package/coverage/lcov-report/parsers/index.html +1 -1
- package/coverage/lcov-report/parsers/index.ts.html +4 -4
- package/coverage/lcov-report/parsers/json.ts.html +2 -2
- package/coverage/lcov-report/parsers/yml.ts.html +6 -6
- package/coverage/lcov-report/tester/cliFormat.ts.html +1 -1
- package/coverage/lcov-report/tester/helpers.ts.html +1 -1
- package/coverage/lcov-report/tester/index.html +1 -1
- package/coverage/lcov-report/utils/git.ts.html +118 -16
- package/coverage/lcov-report/utils/index.html +14 -14
- package/coverage/lcov.info +1374 -1107
- package/lib/config/projectConfig.d.ts +3 -0
- package/lib/config/projectConfig.js +7 -1
- package/lib/config/projectConfig.js.map +1 -1
- package/lib/config/projectConfig.spec.d.ts +1 -0
- package/lib/config/projectConfig.spec.js +24 -0
- package/lib/config/projectConfig.spec.js.map +1 -0
- package/lib/datasource/adapter.d.ts +7 -0
- package/lib/datasource/adapter.js +7 -0
- package/lib/datasource/adapter.js.map +1 -1
- package/lib/datasource/datasource.d.ts +3 -1
- package/lib/datasource/datasource.js +6 -0
- package/lib/datasource/datasource.js.map +1 -1
- package/lib/datasource/filesystemAdapter.d.ts +8 -0
- package/lib/datasource/filesystemAdapter.js +134 -7
- package/lib/datasource/filesystemAdapter.js.map +1 -1
- package/lib/datasource/filesystemAdapter.spec.d.ts +1 -0
- package/lib/datasource/filesystemAdapter.spec.js +88 -0
- package/lib/datasource/filesystemAdapter.spec.js.map +1 -0
- package/lib/linter/conditionSchema.spec.js +2 -0
- package/lib/linter/conditionSchema.spec.js.map +1 -1
- package/lib/linter/featureSchema.js +46 -49
- package/lib/linter/featureSchema.js.map +1 -1
- package/lib/linter/featureSchema.spec.js +17 -0
- package/lib/linter/featureSchema.spec.js.map +1 -1
- package/lib/linter/lintProject.d.ts +2 -0
- package/lib/linter/lintProject.js +85 -8
- package/lib/linter/lintProject.js.map +1 -1
- package/lib/linter/lintProject.spec.js +37 -0
- package/lib/linter/lintProject.spec.js.map +1 -1
- package/lib/linter/segmentSchema.spec.js +2 -0
- package/lib/linter/segmentSchema.spec.js.map +1 -1
- package/lib/utils/git.js +32 -7
- package/lib/utils/git.js.map +1 -1
- package/package.json +2 -2
- package/src/config/projectConfig.spec.ts +31 -0
- package/src/config/projectConfig.ts +11 -0
- package/src/datasource/adapter.ts +21 -0
- package/src/datasource/datasource.ts +18 -1
- package/src/datasource/filesystemAdapter.spec.ts +153 -0
- package/src/datasource/filesystemAdapter.ts +216 -8
- package/src/linter/conditionSchema.spec.ts +2 -0
- package/src/linter/featureSchema.spec.ts +21 -0
- package/src/linter/featureSchema.ts +84 -88
- package/src/linter/lintProject.spec.ts +74 -0
- package/src/linter/lintProject.ts +123 -8
- package/src/linter/segmentSchema.spec.ts +2 -0
- 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(
|
|
523
|
+
const extensionWithDot = "." + this.parser.extension;
|
|
524
|
+
const key = fileName.replace(extensionWithDot, "");
|
|
366
525
|
|
|
367
526
|
let type: EntityType = "attribute";
|
|
368
|
-
if (
|
|
527
|
+
if (isWithinDirectory(this.config.attributesDirectoryPath, relativeDir)) {
|
|
369
528
|
type = "attribute";
|
|
370
|
-
} else if (
|
|
529
|
+
} else if (isWithinDirectory(this.config.segmentsDirectoryPath, relativeDir)) {
|
|
371
530
|
type = "segment";
|
|
372
|
-
} else if (
|
|
531
|
+
} else if (isWithinDirectory(this.config.featuresDirectoryPath, relativeDir)) {
|
|
373
532
|
type = "feature";
|
|
374
|
-
} else if (
|
|
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 (
|
|
540
|
+
} else if (isWithinDirectory(this.config.schemasDirectoryPath, relativeDir)) {
|
|
377
541
|
type = "schema";
|
|
378
|
-
} else if (
|
|
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,
|
|
@@ -21,7 +21,9 @@ function minimalProjectConfig(): ProjectConfig {
|
|
|
21
21
|
datafileNamePattern: "",
|
|
22
22
|
revisionFileName: "",
|
|
23
23
|
siteExportDirectoryPath: "",
|
|
24
|
+
environmentsDirectoryPath: "",
|
|
24
25
|
environments: ["staging", "production"],
|
|
26
|
+
splitByEnvironment: false,
|
|
25
27
|
tags: ["all"],
|
|
26
28
|
adapter: {},
|
|
27
29
|
plugins: [],
|
|
@@ -25,7 +25,9 @@ function minimalProjectConfig(overrides: Partial<ProjectConfig> = {}): ProjectCo
|
|
|
25
25
|
datafileNamePattern: "",
|
|
26
26
|
revisionFileName: "",
|
|
27
27
|
siteExportDirectoryPath: "",
|
|
28
|
+
environmentsDirectoryPath: "",
|
|
28
29
|
environments: ["staging", "production"],
|
|
30
|
+
splitByEnvironment: false,
|
|
29
31
|
tags: ["all"],
|
|
30
32
|
adapter: {},
|
|
31
33
|
plugins: [],
|
|
@@ -678,6 +680,25 @@ describe("featureSchema.ts :: getFeatureZodSchema (variablesSchema and variable
|
|
|
678
680
|
"not defined in",
|
|
679
681
|
);
|
|
680
682
|
});
|
|
683
|
+
|
|
684
|
+
it("rejects rule variable key when feature has no variablesSchema", () => {
|
|
685
|
+
expectParseFailure(
|
|
686
|
+
baseFeature({
|
|
687
|
+
rules: {
|
|
688
|
+
staging: [{ key: "r1", segments: "*", percentage: 100 }],
|
|
689
|
+
production: [
|
|
690
|
+
{
|
|
691
|
+
key: "r1",
|
|
692
|
+
segments: "*",
|
|
693
|
+
percentage: 80,
|
|
694
|
+
variables: { blah123: "some value" },
|
|
695
|
+
},
|
|
696
|
+
],
|
|
697
|
+
},
|
|
698
|
+
}),
|
|
699
|
+
"not defined in",
|
|
700
|
+
);
|
|
701
|
+
});
|
|
681
702
|
});
|
|
682
703
|
|
|
683
704
|
describe("force: variables", () => {
|