@featurevisor/core 0.51.2 → 0.52.1

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 (54) 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/linter/conditionSchema.js +27 -5
  12. package/lib/linter/conditionSchema.js.map +1 -1
  13. package/lib/site/exportSite.d.ts +2 -0
  14. package/lib/site/exportSite.js +34 -0
  15. package/lib/site/exportSite.js.map +1 -0
  16. package/lib/site/generateHistory.d.ts +3 -0
  17. package/lib/site/generateHistory.js +76 -0
  18. package/lib/site/generateHistory.js.map +1 -0
  19. package/lib/site/generateSiteSearchIndex.d.ts +4 -0
  20. package/lib/site/generateSiteSearchIndex.js +141 -0
  21. package/lib/site/generateSiteSearchIndex.js.map +1 -0
  22. package/lib/site/getLastModifiedFromHistory.d.ts +2 -0
  23. package/lib/site/getLastModifiedFromHistory.js +19 -0
  24. package/lib/site/getLastModifiedFromHistory.js.map +1 -0
  25. package/lib/site/getOwnerAndRepoFromUrl.d.ts +4 -0
  26. package/lib/site/getOwnerAndRepoFromUrl.js +21 -0
  27. package/lib/site/getOwnerAndRepoFromUrl.js.map +1 -0
  28. package/lib/site/getRelativePaths.d.ts +6 -0
  29. package/lib/site/getRelativePaths.js +16 -0
  30. package/lib/site/getRelativePaths.js.map +1 -0
  31. package/lib/site/getRepoDetails.d.ts +8 -0
  32. package/lib/site/getRepoDetails.js +49 -0
  33. package/lib/site/getRepoDetails.js.map +1 -0
  34. package/lib/site/index.d.ts +2 -0
  35. package/lib/site/index.js +19 -0
  36. package/lib/site/index.js.map +1 -0
  37. package/lib/site/serveSite.d.ts +2 -0
  38. package/lib/site/serveSite.js +55 -0
  39. package/lib/site/serveSite.js.map +1 -0
  40. package/package.json +7 -7
  41. package/src/linter/conditionSchema.ts +39 -10
  42. package/src/site/exportSite.ts +53 -0
  43. package/src/site/generateHistory.ts +101 -0
  44. package/src/site/generateSiteSearchIndex.ts +203 -0
  45. package/src/site/getLastModifiedFromHistory.ts +21 -0
  46. package/src/site/getOwnerAndRepoFromUrl.ts +17 -0
  47. package/src/site/getRelativePaths.ts +24 -0
  48. package/src/site/getRepoDetails.ts +62 -0
  49. package/src/site/index.ts +2 -0
  50. package/src/site/serveSite.ts +60 -0
  51. package/lib/site.d.ts +0 -16
  52. package/lib/site.js +0 -368
  53. package/lib/site.js.map +0 -1
  54. package/src/site.ts +0 -515
package/src/site.ts DELETED
@@ -1,515 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import * as http from "http";
4
- import { execSync } from "child_process";
5
-
6
- import * as mkdirp from "mkdirp";
7
-
8
- import {
9
- HistoryEntry,
10
- LastModified,
11
- SearchIndex,
12
- FeatureKey,
13
- SegmentKey,
14
- AttributeKey,
15
- Condition,
16
- } from "@featurevisor/types";
17
-
18
- import { ProjectConfig } from "./config";
19
- import { Datasource } from "./datasource";
20
- import { extractAttributeKeysFromConditions, extractSegmentKeysFromGroupSegments } from "./utils";
21
-
22
- function getRelativePaths(rootDirectoryPath, projectConfig: ProjectConfig) {
23
- const relativeFeaturesPath = path.relative(
24
- rootDirectoryPath,
25
- projectConfig.featuresDirectoryPath,
26
- );
27
- const relativeSegmentsPath = path.relative(
28
- rootDirectoryPath,
29
- projectConfig.segmentsDirectoryPath,
30
- );
31
- const relativeAttributesPath = path.relative(
32
- rootDirectoryPath,
33
- projectConfig.attributesDirectoryPath,
34
- );
35
-
36
- return {
37
- relativeFeaturesPath,
38
- relativeSegmentsPath,
39
- relativeAttributesPath,
40
- };
41
- }
42
-
43
- export function generateHistory(rootDirectoryPath, projectConfig: ProjectConfig): HistoryEntry[] {
44
- try {
45
- // raw history
46
- const rawHistoryFilePath = path.join(projectConfig.siteExportDirectoryPath, "history-raw.txt");
47
-
48
- const { relativeFeaturesPath, relativeSegmentsPath, relativeAttributesPath } = getRelativePaths(
49
- rootDirectoryPath,
50
- projectConfig,
51
- );
52
-
53
- const separator = "|";
54
-
55
- const cmd = `git log --name-only --pretty=format:"%h${separator}%an${separator}%aI" --no-merges --relative -- ${relativeFeaturesPath} ${relativeSegmentsPath} ${relativeAttributesPath} > ${rawHistoryFilePath}`;
56
- execSync(cmd);
57
-
58
- console.log(`History (raw) generated at: ${rawHistoryFilePath}`);
59
-
60
- // structured history
61
- const rawHistory = fs.readFileSync(rawHistoryFilePath, "utf8");
62
-
63
- const fullHistory: HistoryEntry[] = [];
64
-
65
- let entry: HistoryEntry = {
66
- commit: "",
67
- author: "",
68
- timestamp: "",
69
- entities: [],
70
- };
71
-
72
- rawHistory.split("\n").forEach((line, index) => {
73
- if (index === 0 && line.length === 0) {
74
- // no history found
75
- return;
76
- }
77
-
78
- if (index > 0 && line.length === 0) {
79
- // commit finished
80
- fullHistory.push(entry);
81
-
82
- return;
83
- }
84
-
85
- if (line.indexOf(separator) > -1) {
86
- // commit line
87
- const parts = line.split("|");
88
-
89
- entry = {
90
- commit: parts[0],
91
- author: parts[1],
92
- timestamp: parts[2],
93
- entities: [],
94
- };
95
- } else {
96
- // file line
97
- const lineSplit = line.split(path.sep);
98
- const fileName = lineSplit.pop() as string;
99
- const relativeDir = lineSplit.join(path.sep);
100
-
101
- const key = fileName.replace("." + projectConfig.parser, "");
102
-
103
- let type = "feature" as "attribute" | "segment" | "feature";
104
-
105
- if (relativeDir === relativeSegmentsPath) {
106
- type = "segment";
107
- } else if (relativeDir === relativeAttributesPath) {
108
- type = "attribute";
109
- }
110
-
111
- entry.entities.push({
112
- type,
113
- key,
114
- });
115
- }
116
- });
117
-
118
- const fullHistoryFilePath = path.join(
119
- projectConfig.siteExportDirectoryPath,
120
- "history-full.json",
121
- );
122
- fs.writeFileSync(fullHistoryFilePath, JSON.stringify(fullHistory));
123
- console.log(`History (full) generated at: ${fullHistoryFilePath}`);
124
-
125
- return fullHistory;
126
- } catch (error) {
127
- console.error(
128
- `Error when generating history from git: ${error.status}\n${error.stderr.toString()}`,
129
- );
130
-
131
- return [];
132
- }
133
- }
134
-
135
- export function getLastModifiedFromHistory(
136
- fullHistory: HistoryEntry[],
137
- type,
138
- key,
139
- ): LastModified | undefined {
140
- const lastModified = fullHistory.find((entry) => {
141
- return entry.entities.find((entity) => {
142
- return entity.type === type && entity.key === key;
143
- });
144
- });
145
-
146
- if (lastModified) {
147
- return {
148
- commit: lastModified.commit,
149
- timestamp: lastModified.timestamp,
150
- author: lastModified.author,
151
- };
152
- }
153
- }
154
-
155
- function getOwnerAndRepoFromUrl(url: string): { owner: string; repo: string } {
156
- let owner;
157
- let repo;
158
-
159
- if (url.startsWith("https://")) {
160
- const parts = url.split("/");
161
- repo = (parts.pop() as string).replace(".git", "");
162
- owner = parts.pop();
163
- } else if (url.startsWith("git@")) {
164
- const urlParts = url.split(":");
165
- const parts = urlParts[1].split("/");
166
- repo = (parts.pop() as string).replace(".git", "");
167
- owner = parts.pop();
168
- }
169
-
170
- return { owner, repo };
171
- }
172
-
173
- interface RepoDetails {
174
- branch: string;
175
- remoteUrl: string;
176
- blobUrl: string;
177
- commitUrl: string;
178
- topLevelPath: string;
179
- }
180
-
181
- export function getDetailsFromRepo(): RepoDetails | undefined {
182
- try {
183
- const topLevelPathOutput = execSync(`git rev-parse --show-toplevel`);
184
- const topLevelPath = topLevelPathOutput.toString().trim();
185
-
186
- const remoteUrlOutput = execSync(`git remote get-url origin`);
187
- const remoteUrl = remoteUrlOutput.toString().trim();
188
-
189
- const branchOutput = execSync(`git rev-parse --abbrev-ref HEAD`);
190
- const branch = branchOutput.toString().trim();
191
-
192
- if (!remoteUrl || !branch) {
193
- return;
194
- }
195
-
196
- const { owner, repo } = getOwnerAndRepoFromUrl(remoteUrl);
197
-
198
- if (!owner || !repo) {
199
- return;
200
- }
201
-
202
- let blobUrl;
203
- let commitUrl;
204
-
205
- if (remoteUrl.indexOf("github.com") > -1) {
206
- blobUrl = `https://github.com/${owner}/${repo}/blob/${branch}/{{blobPath}}`;
207
- commitUrl = `https://github.com/${owner}/${repo}/commit/{{hash}}`;
208
- } else if (remoteUrl.indexOf("bitbucket.org") > -1) {
209
- blobUrl = `https://bitbucket.org/${owner}/${repo}/src/${branch}/{{blobPath}}`;
210
- commitUrl = `https://bitbucket.org/${owner}/${repo}/commits/{{hash}}`;
211
- }
212
-
213
- if (!blobUrl || !commitUrl) {
214
- return;
215
- }
216
-
217
- return {
218
- branch,
219
- remoteUrl,
220
- blobUrl,
221
- commitUrl,
222
- topLevelPath,
223
- };
224
- } catch (e) {
225
- console.error(e);
226
- return;
227
- }
228
-
229
- return;
230
- }
231
-
232
- export function generateSiteSearchIndex(
233
- rootDirectoryPath: string,
234
- projectConfig: ProjectConfig,
235
- fullHistory: HistoryEntry[],
236
- repoDetails: RepoDetails | undefined,
237
- ): SearchIndex {
238
- const result: SearchIndex = {
239
- links: undefined,
240
- entities: {
241
- attributes: [],
242
- segments: [],
243
- features: [],
244
- },
245
- };
246
- const datasource = new Datasource(projectConfig);
247
-
248
- /**
249
- * Links
250
- */
251
- if (repoDetails) {
252
- const { relativeAttributesPath, relativeSegmentsPath, relativeFeaturesPath } = getRelativePaths(
253
- rootDirectoryPath,
254
- projectConfig,
255
- );
256
-
257
- let prefix = "";
258
- if (repoDetails.topLevelPath !== rootDirectoryPath) {
259
- prefix = rootDirectoryPath.replace(repoDetails.topLevelPath + "/", "") + "/";
260
- }
261
-
262
- result.links = {
263
- attribute: repoDetails.blobUrl.replace(
264
- "{{blobPath}}",
265
- prefix + relativeAttributesPath + "/{{key}}." + datasource.getExtension(),
266
- ),
267
- segment: repoDetails.blobUrl.replace(
268
- "{{blobPath}}",
269
- prefix + relativeSegmentsPath + "/{{key}}." + datasource.getExtension(),
270
- ),
271
- feature: repoDetails.blobUrl.replace(
272
- "{{blobPath}}",
273
- prefix + relativeFeaturesPath + "/{{key}}." + datasource.getExtension(),
274
- ),
275
- commit: repoDetails.commitUrl,
276
- };
277
- }
278
-
279
- /**
280
- * Entities
281
- */
282
- // usage
283
- const attributesUsedInFeatures: {
284
- [key: AttributeKey]: Set<FeatureKey>;
285
- } = {};
286
- const attributesUsedInSegments: {
287
- [key: AttributeKey]: Set<SegmentKey>;
288
- } = {};
289
- const segmentsUsedInFeatures: {
290
- [key: SegmentKey]: Set<FeatureKey>;
291
- } = {};
292
-
293
- // features
294
- const featureFiles = datasource.listFeatures();
295
- featureFiles.forEach((entityName) => {
296
- const parsed = datasource.readFeature(entityName);
297
-
298
- if (Array.isArray(parsed.variations)) {
299
- parsed.variations.forEach((variation) => {
300
- if (!variation.variables) {
301
- return;
302
- }
303
-
304
- variation.variables.forEach((v) => {
305
- if (v.overrides) {
306
- v.overrides.forEach((o) => {
307
- if (o.conditions) {
308
- extractAttributeKeysFromConditions(o.conditions).forEach((attributeKey) => {
309
- if (!attributesUsedInFeatures[attributeKey]) {
310
- attributesUsedInFeatures[attributeKey] = new Set();
311
- }
312
-
313
- attributesUsedInFeatures[attributeKey].add(entityName);
314
- });
315
- }
316
-
317
- if (o.segments && o.segments !== "*") {
318
- extractSegmentKeysFromGroupSegments(o.segments).forEach((segmentKey) => {
319
- if (!segmentsUsedInFeatures[segmentKey]) {
320
- segmentsUsedInFeatures[segmentKey] = new Set();
321
- }
322
-
323
- segmentsUsedInFeatures[segmentKey].add(entityName);
324
- });
325
- }
326
- });
327
- }
328
- });
329
- });
330
- }
331
-
332
- Object.keys(parsed.environments).forEach((environmentKey) => {
333
- const env = parsed.environments[environmentKey];
334
-
335
- env.rules.forEach((rule) => {
336
- if (rule.segments && rule.segments !== "*") {
337
- extractSegmentKeysFromGroupSegments(rule.segments).forEach((segmentKey) => {
338
- if (!segmentsUsedInFeatures[segmentKey]) {
339
- segmentsUsedInFeatures[segmentKey] = new Set();
340
- }
341
-
342
- segmentsUsedInFeatures[segmentKey].add(entityName);
343
- });
344
- }
345
- });
346
-
347
- if (env.force) {
348
- env.force.forEach((force) => {
349
- if (force.segments && force.segments !== "*") {
350
- extractSegmentKeysFromGroupSegments(force.segments).forEach((segmentKey) => {
351
- if (!segmentsUsedInFeatures[segmentKey]) {
352
- segmentsUsedInFeatures[segmentKey] = new Set();
353
- }
354
-
355
- segmentsUsedInFeatures[segmentKey].add(entityName);
356
- });
357
- }
358
-
359
- if (force.conditions) {
360
- extractAttributeKeysFromConditions(force.conditions).forEach((attributeKey) => {
361
- if (!attributesUsedInFeatures[attributeKey]) {
362
- attributesUsedInFeatures[attributeKey] = new Set();
363
- }
364
-
365
- attributesUsedInFeatures[attributeKey].add(entityName);
366
- });
367
- }
368
- });
369
- }
370
- });
371
-
372
- result.entities.features.push({
373
- ...parsed,
374
- key: entityName,
375
- lastModified: getLastModifiedFromHistory(fullHistory, "feature", entityName),
376
- });
377
- });
378
-
379
- // segments
380
- const segmentFiles = datasource.listSegments();
381
- segmentFiles.forEach((entityName) => {
382
- const parsed = datasource.readSegment(entityName);
383
-
384
- extractAttributeKeysFromConditions(parsed.conditions as Condition | Condition[]).forEach(
385
- (attributeKey) => {
386
- if (!attributesUsedInSegments[attributeKey]) {
387
- attributesUsedInSegments[attributeKey] = new Set();
388
- }
389
-
390
- attributesUsedInSegments[attributeKey].add(entityName);
391
- },
392
- );
393
-
394
- result.entities.segments.push({
395
- ...parsed,
396
- key: entityName,
397
- lastModified: getLastModifiedFromHistory(fullHistory, "segment", entityName),
398
- usedInFeatures: Array.from(segmentsUsedInFeatures[entityName] || []),
399
- });
400
- });
401
-
402
- // attributes
403
- const attributeFiles = datasource.listAttributes();
404
- attributeFiles.forEach((entityName) => {
405
- const parsed = datasource.readAttribute(entityName);
406
-
407
- result.entities.attributes.push({
408
- ...parsed,
409
- key: entityName,
410
- lastModified: getLastModifiedFromHistory(fullHistory, "attribute", entityName),
411
- usedInFeatures: Array.from(attributesUsedInFeatures[entityName] || []),
412
- usedInSegments: Array.from(attributesUsedInSegments[entityName] || []),
413
- });
414
- });
415
-
416
- return result;
417
- }
418
-
419
- export function exportSite(rootDirectoryPath: string, projectConfig: ProjectConfig) {
420
- const hasError = false;
421
-
422
- mkdirp.sync(projectConfig.siteExportDirectoryPath);
423
-
424
- const sitePackagePath = path.dirname(require.resolve("@featurevisor/site/package.json"));
425
-
426
- // copy site dist
427
- const siteDistPath = path.join(sitePackagePath, "dist");
428
- fs.cpSync(siteDistPath, projectConfig.siteExportDirectoryPath, { recursive: true });
429
-
430
- const sitePublicPath = path.join(sitePackagePath, "public");
431
- fs.cpSync(sitePublicPath, projectConfig.siteExportDirectoryPath, { recursive: true });
432
-
433
- console.log("Site dist copied to:", projectConfig.siteExportDirectoryPath);
434
-
435
- // generate history
436
- const fullHistory = generateHistory(rootDirectoryPath, projectConfig);
437
-
438
- // site search index
439
- const repoDetails = getDetailsFromRepo();
440
- const searchIndex = generateSiteSearchIndex(
441
- rootDirectoryPath,
442
- projectConfig,
443
- fullHistory,
444
- repoDetails,
445
- );
446
- const searchIndexFilePath = path.join(projectConfig.siteExportDirectoryPath, "search-index.json");
447
- fs.writeFileSync(searchIndexFilePath, JSON.stringify(searchIndex));
448
- console.log(`Site search index written at: ${searchIndexFilePath}`);
449
-
450
- // copy datafiles
451
- fs.cpSync(
452
- projectConfig.outputDirectoryPath,
453
- path.join(projectConfig.siteExportDirectoryPath, "datafiles"),
454
- { recursive: true },
455
- );
456
-
457
- // @TODO: replace placeoholders in index.html
458
-
459
- return hasError;
460
- }
461
-
462
- export function serveSite(
463
- rootDirectoryPath: string,
464
- projectConfig: ProjectConfig,
465
- options: any = {},
466
- ) {
467
- const port = options.p || 3000;
468
-
469
- http
470
- .createServer(function (request, response) {
471
- const requestedUrl = request.url;
472
- const filePath =
473
- requestedUrl === "/"
474
- ? path.join(projectConfig.siteExportDirectoryPath, "index.html")
475
- : path.join(projectConfig.siteExportDirectoryPath, requestedUrl as string);
476
-
477
- console.log("requesting: " + filePath + "");
478
-
479
- const extname = path.extname(filePath);
480
- let contentType = "text/html";
481
- switch (extname) {
482
- case ".js":
483
- contentType = "text/javascript";
484
- break;
485
- case ".css":
486
- contentType = "text/css";
487
- break;
488
- case ".json":
489
- contentType = "application/json";
490
- break;
491
- case ".png":
492
- contentType = "image/png";
493
- break;
494
- }
495
-
496
- fs.readFile(filePath, function (error, content) {
497
- if (error) {
498
- if (error.code == "ENOENT") {
499
- response.writeHead(404, { "Content-Type": "text/html" });
500
- response.end("404 Not Found", "utf-8");
501
- } else {
502
- response.writeHead(500);
503
- response.end("Error 500: " + error.code);
504
- response.end();
505
- }
506
- } else {
507
- response.writeHead(200, { "Content-Type": contentType });
508
- response.end(content, "utf-8");
509
- }
510
- });
511
- })
512
- .listen(port);
513
-
514
- console.log(`Server running at http://127.0.0.1:${port}/`);
515
- }