@featurevisor/core 0.52.0 → 0.53.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 (63) hide show
  1. package/.eslintcache +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/coverage/clover.xml +2 -2
  4. package/coverage/lcov-report/index.html +1 -1
  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/traffic.js.html +1 -1
  8. package/coverage/lcov-report/src/builder/allocator.ts.html +1 -1
  9. package/coverage/lcov-report/src/builder/index.html +1 -1
  10. package/coverage/lcov-report/src/builder/traffic.ts.html +1 -1
  11. package/lib/find-duplicate-segments/findDuplicateSegments.d.ts +3 -0
  12. package/lib/find-duplicate-segments/findDuplicateSegments.js +27 -0
  13. package/lib/find-duplicate-segments/findDuplicateSegments.js.map +1 -0
  14. package/lib/find-duplicate-segments/index.d.ts +2 -0
  15. package/lib/find-duplicate-segments/index.js +19 -0
  16. package/lib/find-duplicate-segments/index.js.map +1 -0
  17. package/lib/index.d.ts +1 -0
  18. package/lib/index.js +1 -0
  19. package/lib/index.js.map +1 -1
  20. package/lib/site/exportSite.d.ts +2 -0
  21. package/lib/site/exportSite.js +34 -0
  22. package/lib/site/exportSite.js.map +1 -0
  23. package/lib/site/generateHistory.d.ts +3 -0
  24. package/lib/site/generateHistory.js +76 -0
  25. package/lib/site/generateHistory.js.map +1 -0
  26. package/lib/site/generateSiteSearchIndex.d.ts +4 -0
  27. package/lib/site/generateSiteSearchIndex.js +141 -0
  28. package/lib/site/generateSiteSearchIndex.js.map +1 -0
  29. package/lib/site/getLastModifiedFromHistory.d.ts +2 -0
  30. package/lib/site/getLastModifiedFromHistory.js +19 -0
  31. package/lib/site/getLastModifiedFromHistory.js.map +1 -0
  32. package/lib/site/getOwnerAndRepoFromUrl.d.ts +4 -0
  33. package/lib/site/getOwnerAndRepoFromUrl.js +21 -0
  34. package/lib/site/getOwnerAndRepoFromUrl.js.map +1 -0
  35. package/lib/site/getRelativePaths.d.ts +6 -0
  36. package/lib/site/getRelativePaths.js +16 -0
  37. package/lib/site/getRelativePaths.js.map +1 -0
  38. package/lib/site/getRepoDetails.d.ts +8 -0
  39. package/lib/site/getRepoDetails.js +49 -0
  40. package/lib/site/getRepoDetails.js.map +1 -0
  41. package/lib/site/index.d.ts +2 -0
  42. package/lib/site/index.js +19 -0
  43. package/lib/site/index.js.map +1 -0
  44. package/lib/site/serveSite.d.ts +2 -0
  45. package/lib/site/serveSite.js +55 -0
  46. package/lib/site/serveSite.js.map +1 -0
  47. package/package.json +5 -5
  48. package/src/find-duplicate-segments/findDuplicateSegments.ts +33 -0
  49. package/src/find-duplicate-segments/index.ts +21 -0
  50. package/src/index.ts +1 -0
  51. package/src/site/exportSite.ts +53 -0
  52. package/src/site/generateHistory.ts +101 -0
  53. package/src/site/generateSiteSearchIndex.ts +203 -0
  54. package/src/site/getLastModifiedFromHistory.ts +21 -0
  55. package/src/site/getOwnerAndRepoFromUrl.ts +17 -0
  56. package/src/site/getRelativePaths.ts +24 -0
  57. package/src/site/getRepoDetails.ts +62 -0
  58. package/src/site/index.ts +2 -0
  59. package/src/site/serveSite.ts +60 -0
  60. package/lib/site.d.ts +0 -16
  61. package/lib/site.js +0 -368
  62. package/lib/site.js.map +0 -1
  63. package/src/site.ts +0 -515
@@ -0,0 +1,203 @@
1
+ import {
2
+ HistoryEntry,
3
+ SearchIndex,
4
+ AttributeKey,
5
+ FeatureKey,
6
+ SegmentKey,
7
+ Condition,
8
+ } from "@featurevisor/types";
9
+
10
+ import { Datasource } from "../datasource";
11
+ import { ProjectConfig } from "../config";
12
+ import { extractAttributeKeysFromConditions, extractSegmentKeysFromGroupSegments } from "../utils";
13
+
14
+ import { getRelativePaths } from "./getRelativePaths";
15
+ import { getLastModifiedFromHistory } from "./getLastModifiedFromHistory";
16
+ import { RepoDetails } from "./getRepoDetails";
17
+
18
+ export function generateSiteSearchIndex(
19
+ rootDirectoryPath: string,
20
+ projectConfig: ProjectConfig,
21
+ fullHistory: HistoryEntry[],
22
+ repoDetails: RepoDetails | undefined,
23
+ ): SearchIndex {
24
+ const result: SearchIndex = {
25
+ links: undefined,
26
+ entities: {
27
+ attributes: [],
28
+ segments: [],
29
+ features: [],
30
+ },
31
+ };
32
+ const datasource = new Datasource(projectConfig);
33
+
34
+ /**
35
+ * Links
36
+ */
37
+ if (repoDetails) {
38
+ const { relativeAttributesPath, relativeSegmentsPath, relativeFeaturesPath } = getRelativePaths(
39
+ rootDirectoryPath,
40
+ projectConfig,
41
+ );
42
+
43
+ let prefix = "";
44
+ if (repoDetails.topLevelPath !== rootDirectoryPath) {
45
+ prefix = rootDirectoryPath.replace(repoDetails.topLevelPath + "/", "") + "/";
46
+ }
47
+
48
+ result.links = {
49
+ attribute: repoDetails.blobUrl.replace(
50
+ "{{blobPath}}",
51
+ prefix + relativeAttributesPath + "/{{key}}." + datasource.getExtension(),
52
+ ),
53
+ segment: repoDetails.blobUrl.replace(
54
+ "{{blobPath}}",
55
+ prefix + relativeSegmentsPath + "/{{key}}." + datasource.getExtension(),
56
+ ),
57
+ feature: repoDetails.blobUrl.replace(
58
+ "{{blobPath}}",
59
+ prefix + relativeFeaturesPath + "/{{key}}." + datasource.getExtension(),
60
+ ),
61
+ commit: repoDetails.commitUrl,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Entities
67
+ */
68
+ // usage
69
+ const attributesUsedInFeatures: {
70
+ [key: AttributeKey]: Set<FeatureKey>;
71
+ } = {};
72
+ const attributesUsedInSegments: {
73
+ [key: AttributeKey]: Set<SegmentKey>;
74
+ } = {};
75
+ const segmentsUsedInFeatures: {
76
+ [key: SegmentKey]: Set<FeatureKey>;
77
+ } = {};
78
+
79
+ // features
80
+ const featureFiles = datasource.listFeatures();
81
+ featureFiles.forEach((entityName) => {
82
+ const parsed = datasource.readFeature(entityName);
83
+
84
+ if (Array.isArray(parsed.variations)) {
85
+ parsed.variations.forEach((variation) => {
86
+ if (!variation.variables) {
87
+ return;
88
+ }
89
+
90
+ variation.variables.forEach((v) => {
91
+ if (v.overrides) {
92
+ v.overrides.forEach((o) => {
93
+ if (o.conditions) {
94
+ extractAttributeKeysFromConditions(o.conditions).forEach((attributeKey) => {
95
+ if (!attributesUsedInFeatures[attributeKey]) {
96
+ attributesUsedInFeatures[attributeKey] = new Set();
97
+ }
98
+
99
+ attributesUsedInFeatures[attributeKey].add(entityName);
100
+ });
101
+ }
102
+
103
+ if (o.segments && o.segments !== "*") {
104
+ extractSegmentKeysFromGroupSegments(o.segments).forEach((segmentKey) => {
105
+ if (!segmentsUsedInFeatures[segmentKey]) {
106
+ segmentsUsedInFeatures[segmentKey] = new Set();
107
+ }
108
+
109
+ segmentsUsedInFeatures[segmentKey].add(entityName);
110
+ });
111
+ }
112
+ });
113
+ }
114
+ });
115
+ });
116
+ }
117
+
118
+ Object.keys(parsed.environments).forEach((environmentKey) => {
119
+ const env = parsed.environments[environmentKey];
120
+
121
+ env.rules.forEach((rule) => {
122
+ if (rule.segments && rule.segments !== "*") {
123
+ extractSegmentKeysFromGroupSegments(rule.segments).forEach((segmentKey) => {
124
+ if (!segmentsUsedInFeatures[segmentKey]) {
125
+ segmentsUsedInFeatures[segmentKey] = new Set();
126
+ }
127
+
128
+ segmentsUsedInFeatures[segmentKey].add(entityName);
129
+ });
130
+ }
131
+ });
132
+
133
+ if (env.force) {
134
+ env.force.forEach((force) => {
135
+ if (force.segments && force.segments !== "*") {
136
+ extractSegmentKeysFromGroupSegments(force.segments).forEach((segmentKey) => {
137
+ if (!segmentsUsedInFeatures[segmentKey]) {
138
+ segmentsUsedInFeatures[segmentKey] = new Set();
139
+ }
140
+
141
+ segmentsUsedInFeatures[segmentKey].add(entityName);
142
+ });
143
+ }
144
+
145
+ if (force.conditions) {
146
+ extractAttributeKeysFromConditions(force.conditions).forEach((attributeKey) => {
147
+ if (!attributesUsedInFeatures[attributeKey]) {
148
+ attributesUsedInFeatures[attributeKey] = new Set();
149
+ }
150
+
151
+ attributesUsedInFeatures[attributeKey].add(entityName);
152
+ });
153
+ }
154
+ });
155
+ }
156
+ });
157
+
158
+ result.entities.features.push({
159
+ ...parsed,
160
+ key: entityName,
161
+ lastModified: getLastModifiedFromHistory(fullHistory, "feature", entityName),
162
+ });
163
+ });
164
+
165
+ // segments
166
+ const segmentFiles = datasource.listSegments();
167
+ segmentFiles.forEach((entityName) => {
168
+ const parsed = datasource.readSegment(entityName);
169
+
170
+ extractAttributeKeysFromConditions(parsed.conditions as Condition | Condition[]).forEach(
171
+ (attributeKey) => {
172
+ if (!attributesUsedInSegments[attributeKey]) {
173
+ attributesUsedInSegments[attributeKey] = new Set();
174
+ }
175
+
176
+ attributesUsedInSegments[attributeKey].add(entityName);
177
+ },
178
+ );
179
+
180
+ result.entities.segments.push({
181
+ ...parsed,
182
+ key: entityName,
183
+ lastModified: getLastModifiedFromHistory(fullHistory, "segment", entityName),
184
+ usedInFeatures: Array.from(segmentsUsedInFeatures[entityName] || []),
185
+ });
186
+ });
187
+
188
+ // attributes
189
+ const attributeFiles = datasource.listAttributes();
190
+ attributeFiles.forEach((entityName) => {
191
+ const parsed = datasource.readAttribute(entityName);
192
+
193
+ result.entities.attributes.push({
194
+ ...parsed,
195
+ key: entityName,
196
+ lastModified: getLastModifiedFromHistory(fullHistory, "attribute", entityName),
197
+ usedInFeatures: Array.from(attributesUsedInFeatures[entityName] || []),
198
+ usedInSegments: Array.from(attributesUsedInSegments[entityName] || []),
199
+ });
200
+ });
201
+
202
+ return result;
203
+ }
@@ -0,0 +1,21 @@
1
+ import { HistoryEntry, LastModified } from "@featurevisor/types";
2
+
3
+ export function getLastModifiedFromHistory(
4
+ fullHistory: HistoryEntry[],
5
+ type,
6
+ key,
7
+ ): LastModified | undefined {
8
+ const lastModified = fullHistory.find((entry) => {
9
+ return entry.entities.find((entity) => {
10
+ return entity.type === type && entity.key === key;
11
+ });
12
+ });
13
+
14
+ if (lastModified) {
15
+ return {
16
+ commit: lastModified.commit,
17
+ timestamp: lastModified.timestamp,
18
+ author: lastModified.author,
19
+ };
20
+ }
21
+ }
@@ -0,0 +1,17 @@
1
+ export function getOwnerAndRepoFromUrl(url: string): { owner: string; repo: string } {
2
+ let owner;
3
+ let repo;
4
+
5
+ if (url.startsWith("https://")) {
6
+ const parts = url.split("/");
7
+ repo = (parts.pop() as string).replace(".git", "");
8
+ owner = parts.pop();
9
+ } else if (url.startsWith("git@")) {
10
+ const urlParts = url.split(":");
11
+ const parts = urlParts[1].split("/");
12
+ repo = (parts.pop() as string).replace(".git", "");
13
+ owner = parts.pop();
14
+ }
15
+
16
+ return { owner, repo };
17
+ }
@@ -0,0 +1,24 @@
1
+ import * as path from "path";
2
+
3
+ import { ProjectConfig } from "../config";
4
+
5
+ export function getRelativePaths(rootDirectoryPath, projectConfig: ProjectConfig) {
6
+ const relativeFeaturesPath = path.relative(
7
+ rootDirectoryPath,
8
+ projectConfig.featuresDirectoryPath,
9
+ );
10
+ const relativeSegmentsPath = path.relative(
11
+ rootDirectoryPath,
12
+ projectConfig.segmentsDirectoryPath,
13
+ );
14
+ const relativeAttributesPath = path.relative(
15
+ rootDirectoryPath,
16
+ projectConfig.attributesDirectoryPath,
17
+ );
18
+
19
+ return {
20
+ relativeFeaturesPath,
21
+ relativeSegmentsPath,
22
+ relativeAttributesPath,
23
+ };
24
+ }
@@ -0,0 +1,62 @@
1
+ import { execSync } from "child_process";
2
+
3
+ import { getOwnerAndRepoFromUrl } from "./getOwnerAndRepoFromUrl";
4
+
5
+ export interface RepoDetails {
6
+ branch: string;
7
+ remoteUrl: string;
8
+ blobUrl: string;
9
+ commitUrl: string;
10
+ topLevelPath: string;
11
+ }
12
+
13
+ export function getRepoDetails(): RepoDetails | undefined {
14
+ try {
15
+ const topLevelPathOutput = execSync(`git rev-parse --show-toplevel`);
16
+ const topLevelPath = topLevelPathOutput.toString().trim();
17
+
18
+ const remoteUrlOutput = execSync(`git remote get-url origin`);
19
+ const remoteUrl = remoteUrlOutput.toString().trim();
20
+
21
+ const branchOutput = execSync(`git rev-parse --abbrev-ref HEAD`);
22
+ const branch = branchOutput.toString().trim();
23
+
24
+ if (!remoteUrl || !branch) {
25
+ return;
26
+ }
27
+
28
+ const { owner, repo } = getOwnerAndRepoFromUrl(remoteUrl);
29
+
30
+ if (!owner || !repo) {
31
+ return;
32
+ }
33
+
34
+ let blobUrl;
35
+ let commitUrl;
36
+
37
+ if (remoteUrl.indexOf("github.com") > -1) {
38
+ blobUrl = `https://github.com/${owner}/${repo}/blob/${branch}/{{blobPath}}`;
39
+ commitUrl = `https://github.com/${owner}/${repo}/commit/{{hash}}`;
40
+ } else if (remoteUrl.indexOf("bitbucket.org") > -1) {
41
+ blobUrl = `https://bitbucket.org/${owner}/${repo}/src/${branch}/{{blobPath}}`;
42
+ commitUrl = `https://bitbucket.org/${owner}/${repo}/commits/{{hash}}`;
43
+ }
44
+
45
+ if (!blobUrl || !commitUrl) {
46
+ return;
47
+ }
48
+
49
+ return {
50
+ branch,
51
+ remoteUrl,
52
+ blobUrl,
53
+ commitUrl,
54
+ topLevelPath,
55
+ };
56
+ } catch (e) {
57
+ console.error(e);
58
+ return;
59
+ }
60
+
61
+ return;
62
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./exportSite";
2
+ export * from "./serveSite";
@@ -0,0 +1,60 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as http from "http";
4
+
5
+ import { ProjectConfig } from "../config";
6
+
7
+ export function serveSite(
8
+ rootDirectoryPath: string,
9
+ projectConfig: ProjectConfig,
10
+ options: any = {},
11
+ ) {
12
+ const port = options.p || 3000;
13
+
14
+ http
15
+ .createServer(function (request, response) {
16
+ const requestedUrl = request.url;
17
+ const filePath =
18
+ requestedUrl === "/"
19
+ ? path.join(projectConfig.siteExportDirectoryPath, "index.html")
20
+ : path.join(projectConfig.siteExportDirectoryPath, requestedUrl as string);
21
+
22
+ console.log("requesting: " + filePath + "");
23
+
24
+ const extname = path.extname(filePath);
25
+ let contentType = "text/html";
26
+ switch (extname) {
27
+ case ".js":
28
+ contentType = "text/javascript";
29
+ break;
30
+ case ".css":
31
+ contentType = "text/css";
32
+ break;
33
+ case ".json":
34
+ contentType = "application/json";
35
+ break;
36
+ case ".png":
37
+ contentType = "image/png";
38
+ break;
39
+ }
40
+
41
+ fs.readFile(filePath, function (error, content) {
42
+ if (error) {
43
+ if (error.code == "ENOENT") {
44
+ response.writeHead(404, { "Content-Type": "text/html" });
45
+ response.end("404 Not Found", "utf-8");
46
+ } else {
47
+ response.writeHead(500);
48
+ response.end("Error 500: " + error.code);
49
+ response.end();
50
+ }
51
+ } else {
52
+ response.writeHead(200, { "Content-Type": contentType });
53
+ response.end(content, "utf-8");
54
+ }
55
+ });
56
+ })
57
+ .listen(port);
58
+
59
+ console.log(`Server running at http://127.0.0.1:${port}/`);
60
+ }
package/lib/site.d.ts DELETED
@@ -1,16 +0,0 @@
1
- import { HistoryEntry, LastModified, SearchIndex } from "@featurevisor/types";
2
- import { ProjectConfig } from "./config";
3
- export declare function generateHistory(rootDirectoryPath: any, projectConfig: ProjectConfig): HistoryEntry[];
4
- export declare function getLastModifiedFromHistory(fullHistory: HistoryEntry[], type: any, key: any): LastModified | undefined;
5
- interface RepoDetails {
6
- branch: string;
7
- remoteUrl: string;
8
- blobUrl: string;
9
- commitUrl: string;
10
- topLevelPath: string;
11
- }
12
- export declare function getDetailsFromRepo(): RepoDetails | undefined;
13
- export declare function generateSiteSearchIndex(rootDirectoryPath: string, projectConfig: ProjectConfig, fullHistory: HistoryEntry[], repoDetails: RepoDetails | undefined): SearchIndex;
14
- export declare function exportSite(rootDirectoryPath: string, projectConfig: ProjectConfig): boolean;
15
- export declare function serveSite(rootDirectoryPath: string, projectConfig: ProjectConfig, options?: any): void;
16
- export {};