@featurevisor/catalog 0.0.1 → 3.0.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.
@@ -0,0 +1,1352 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.exportCatalog = exportCatalog;
37
+ exports.serveCatalog = serveCatalog;
38
+ exports.createCatalogDevSession = createCatalogDevSession;
39
+ exports.createCatalogApi = createCatalogApi;
40
+ exports.createCatalogPlugin = createCatalogPlugin;
41
+ const childProcess = __importStar(require("child_process"));
42
+ const fs = __importStar(require("fs"));
43
+ const http = __importStar(require("http"));
44
+ const path = __importStar(require("path"));
45
+ const entityTypes_1 = require("../entityTypes");
46
+ const CATALOG_SCHEMA_VERSION = "1";
47
+ const CATALOG_HISTORY_PAGE_SIZE = 50;
48
+ const CLI_FORMAT_GREEN = "\x1b[32m%s\x1b[0m";
49
+ const CLI_FORMAT_DIM = "\x1b[2m%s\x1b[0m";
50
+ const CLI_FORMAT_BOLD = "\x1b[1m%s\x1b[0m";
51
+ class CatalogJsonWriter {
52
+ async write(filePath, value) {
53
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
54
+ await fs.promises.writeFile(filePath, JSON.stringify(value, null, 2));
55
+ }
56
+ }
57
+ function colorize(value, colorCode) {
58
+ return `\x1b[${colorCode}m${value}\x1b[0m`;
59
+ }
60
+ function prettyDuration(diffInMs) {
61
+ let diff = Math.abs(diffInMs);
62
+ if (diff === 0) {
63
+ return "0ms";
64
+ }
65
+ const ms = diff % 1000;
66
+ diff = (diff - ms) / 1000;
67
+ const secs = diff % 60;
68
+ diff = (diff - secs) / 60;
69
+ const mins = diff % 60;
70
+ const hrs = (diff - mins) / 60;
71
+ const parts = [];
72
+ if (hrs)
73
+ parts.push(`${hrs}h`);
74
+ if (mins)
75
+ parts.push(`${mins}m`);
76
+ if (secs)
77
+ parts.push(`${secs}s`);
78
+ if (ms)
79
+ parts.push(`${ms}ms`);
80
+ return parts.join(" ");
81
+ }
82
+ function pluralize(count, singular, plural = `${singular}s`) {
83
+ return `${count} ${count === 1 ? singular : plural}`;
84
+ }
85
+ function formatCatalogPath(rootDirectoryPath, filePath) {
86
+ const relativePath = path.relative(rootDirectoryPath, filePath);
87
+ if (relativePath && !relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
88
+ return relativePath;
89
+ }
90
+ return filePath;
91
+ }
92
+ class CatalogProgressReporter {
93
+ constructor(rootDirectoryPath, outputDirectoryPath) {
94
+ this.rootDirectoryPath = rootDirectoryPath;
95
+ this.outputDirectoryPath = outputDirectoryPath;
96
+ this.startedAt = Date.now();
97
+ }
98
+ start(options) {
99
+ console.log("");
100
+ console.log(CLI_FORMAT_BOLD, "Generating Featurevisor catalog");
101
+ console.log(` ${colorize("Output", 36)}: ${formatCatalogPath(this.rootDirectoryPath, this.outputDirectoryPath)}`);
102
+ console.log(` ${colorize("Router", 36)}: ${options.browserRouter ? "browser" : "hash"}`);
103
+ console.log(` ${colorize("Sets", 36)}: ${options.sets ? "enabled" : "none"}`);
104
+ console.log("");
105
+ }
106
+ step(label, detail) {
107
+ const suffix = detail ? `: ${colorize(detail, 2)}` : "";
108
+ console.log(` ${colorize("•", 36)} ${label}${suffix}`);
109
+ return Date.now();
110
+ }
111
+ done(startedAt, detail) {
112
+ const suffix = detail ? ` ${detail}` : "";
113
+ console.log(CLI_FORMAT_DIM, ` done in ${prettyDuration(Date.now() - startedAt)}${suffix}`);
114
+ }
115
+ setStart(set) {
116
+ console.log("");
117
+ console.log(CLI_FORMAT_BOLD, set ? `Set "${set}"` : "Root catalog");
118
+ return Date.now();
119
+ }
120
+ complete() {
121
+ console.log("");
122
+ console.log(CLI_FORMAT_GREEN, `Catalog exported to ${formatCatalogPath(this.rootDirectoryPath, this.outputDirectoryPath)}`);
123
+ console.log(CLI_FORMAT_BOLD, `Time: ${prettyDuration(Date.now() - this.startedAt)}`);
124
+ }
125
+ }
126
+ function encodeKey(key) {
127
+ return encodeURIComponent(key);
128
+ }
129
+ function toPosixPath(value) {
130
+ return value.split(path.sep).join("/");
131
+ }
132
+ function getRealPath(value) {
133
+ try {
134
+ return fs.realpathSync.native(value);
135
+ }
136
+ catch {
137
+ return value;
138
+ }
139
+ }
140
+ function runGit(rootDirectoryPath, args) {
141
+ return childProcess.execFileSync("git", ["-C", rootDirectoryPath, ...args], {
142
+ encoding: "utf8",
143
+ stdio: ["ignore", "pipe", "ignore"],
144
+ });
145
+ }
146
+ async function readAll(keys, read) {
147
+ const result = {};
148
+ for (const key of keys) {
149
+ result[key] = await read(key);
150
+ }
151
+ return result;
152
+ }
153
+ function sortSet(value) {
154
+ return Array.from(value || []).sort();
155
+ }
156
+ function addToSet(map, key, value) {
157
+ if (!map[key]) {
158
+ map[key] = new Set();
159
+ }
160
+ map[key].add(value);
161
+ }
162
+ function collectAttributeKeysFromConditions(condition, result) {
163
+ if (!condition || condition === "*") {
164
+ return;
165
+ }
166
+ if (Array.isArray(condition)) {
167
+ condition.forEach((item) => collectAttributeKeysFromConditions(item, result));
168
+ return;
169
+ }
170
+ if (typeof condition === "string") {
171
+ return;
172
+ }
173
+ if ("attribute" in condition) {
174
+ result.add(condition.attribute);
175
+ return;
176
+ }
177
+ if ("and" in condition)
178
+ collectAttributeKeysFromConditions(condition.and, result);
179
+ if ("or" in condition)
180
+ collectAttributeKeysFromConditions(condition.or, result);
181
+ if ("not" in condition)
182
+ collectAttributeKeysFromConditions(condition.not, result);
183
+ }
184
+ function collectSegmentKeys(segments, result) {
185
+ if (!segments || segments === "*") {
186
+ return;
187
+ }
188
+ if (typeof segments === "string") {
189
+ result.add(segments);
190
+ return;
191
+ }
192
+ if (Array.isArray(segments)) {
193
+ segments.forEach((segment) => collectSegmentKeys(segment, result));
194
+ return;
195
+ }
196
+ if ("and" in segments)
197
+ collectSegmentKeys(segments.and, result);
198
+ if ("or" in segments)
199
+ collectSegmentKeys(segments.or, result);
200
+ if ("not" in segments)
201
+ collectSegmentKeys(segments.not, result);
202
+ }
203
+ function collectFeatureKeysFromRequired(required, result) {
204
+ for (const item of required || []) {
205
+ result.add(typeof item === "string" ? item : item.key);
206
+ }
207
+ }
208
+ function collectSchemaKeysFromVariables(variablesSchema, result) {
209
+ for (const schema of Object.values(variablesSchema || {})) {
210
+ if ("schema" in schema && typeof schema.schema === "string") {
211
+ result.add(schema.schema);
212
+ }
213
+ }
214
+ }
215
+ function collectGroupKeysFromRules(rules, result) {
216
+ const ruleRows = Array.isArray(rules) ? rules : Object.values(rules || {}).flat();
217
+ for (const rule of ruleRows) {
218
+ if (rule.segments && !Array.isArray(rule.segments) && typeof rule.segments === "object") {
219
+ for (const groupKey of ["and", "or", "not"]) {
220
+ if (groupKey in rule.segments) {
221
+ result.add(JSON.stringify(rule.segments));
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+ function getFeatureRules(feature) {
228
+ return Array.isArray(feature.rules) ? feature.rules : Object.values(feature.rules || {}).flat();
229
+ }
230
+ function getFeatureForce(feature) {
231
+ return Array.isArray(feature.force) ? feature.force : Object.values(feature.force || {}).flat();
232
+ }
233
+ function matchesFeaturePatterns(featureKey, patterns) {
234
+ if (!patterns) {
235
+ return false;
236
+ }
237
+ const normalizedPatterns = patterns === "*" ? [patterns] : patterns;
238
+ return normalizedPatterns.some((pattern) => {
239
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
240
+ return new RegExp(`^${escaped}$`).test(featureKey);
241
+ });
242
+ }
243
+ function targetIncludesFeature(target, featureKey, feature) {
244
+ const featureTags = feature.tags || [];
245
+ let matchesTags = true;
246
+ if (target.tag) {
247
+ matchesTags = featureTags.includes(target.tag);
248
+ }
249
+ else if (Array.isArray(target.tags)) {
250
+ matchesTags = target.tags.some((tag) => featureTags.includes(tag));
251
+ }
252
+ else if (target.tags && "or" in target.tags) {
253
+ matchesTags = target.tags.or.some((tag) => featureTags.includes(tag));
254
+ }
255
+ else if (target.tags && "and" in target.tags) {
256
+ matchesTags = target.tags.and.every((tag) => featureTags.includes(tag));
257
+ }
258
+ const matchesIncludedFeatures = target.includeFeatures
259
+ ? matchesFeaturePatterns(featureKey, target.includeFeatures)
260
+ : true;
261
+ const matchesExcludedFeatures = matchesFeaturePatterns(featureKey, target.excludeFeatures);
262
+ return matchesTags && matchesIncludedFeatures && !matchesExcludedFeatures;
263
+ }
264
+ function getHistoryEntityKey(type, key, set) {
265
+ return `${set || ""}\x1f${type}\x1f${key}`;
266
+ }
267
+ function toLastModified(entry) {
268
+ return {
269
+ commit: entry.commit,
270
+ author: entry.author,
271
+ timestamp: entry.timestamp,
272
+ };
273
+ }
274
+ function getLastModified(historyIndex, type, key, set) {
275
+ return historyIndex.lastModifiedByEntity[getHistoryEntityKey(type, key, set)];
276
+ }
277
+ function getEntitySummary(entity, type, key, historyIndex, set, extra = {}) {
278
+ return {
279
+ key,
280
+ description: entity.description,
281
+ archived: entity.archived,
282
+ deprecated: entity.deprecated,
283
+ promotable: entity.promotable,
284
+ ...extra,
285
+ lastModified: getLastModified(historyIndex, type, key, set),
286
+ href: `entities/${type}/${encodeKey(key)}.json`,
287
+ };
288
+ }
289
+ function toSearchableScalar(value) {
290
+ if (value === undefined || value === null) {
291
+ return undefined;
292
+ }
293
+ if (typeof value === "string") {
294
+ return value;
295
+ }
296
+ if (typeof value === "number" || typeof value === "boolean") {
297
+ return String(value);
298
+ }
299
+ return undefined;
300
+ }
301
+ function getFeatureVariationValues(feature) {
302
+ return (feature.variations || [])
303
+ .map((variation) => toSearchableScalar(variation.value))
304
+ .filter((value) => Boolean(value));
305
+ }
306
+ function getFeatureVariableKeys(feature) {
307
+ return Object.keys(feature.variablesSchema || {}).sort();
308
+ }
309
+ function getFeatureEnvironmentKeys(feature, environments) {
310
+ if (!Array.isArray(environments) || environments.length === 0) {
311
+ return undefined;
312
+ }
313
+ const found = new Set();
314
+ for (const key of ["rules", "force", "expose"]) {
315
+ const value = feature[key];
316
+ if (value && !Array.isArray(value) && typeof value === "object") {
317
+ for (const environment of Object.keys(value)) {
318
+ if (environments.includes(environment)) {
319
+ found.add(environment);
320
+ }
321
+ }
322
+ }
323
+ }
324
+ return Array.from(found).sort();
325
+ }
326
+ function getFeatureRulesForEnvironment(feature, environment) {
327
+ if (Array.isArray(feature.rules)) {
328
+ return feature.rules;
329
+ }
330
+ return feature.rules?.[environment] || [];
331
+ }
332
+ function isFeatureExposedInEnvironment(feature, environment) {
333
+ const featureExpose = feature.expose;
334
+ const rules = feature.rules;
335
+ const environmentRules = !Array.isArray(rules) ? rules?.[environment] : undefined;
336
+ if (environmentRules?.expose === false) {
337
+ return false;
338
+ }
339
+ if (featureExpose === false) {
340
+ return false;
341
+ }
342
+ if (featureExpose &&
343
+ typeof featureExpose === "object" &&
344
+ !Array.isArray(featureExpose) &&
345
+ featureExpose[environment] === false) {
346
+ return false;
347
+ }
348
+ return true;
349
+ }
350
+ function isFeatureEnabledInEnvironment(feature, environment) {
351
+ if (feature.archived === true) {
352
+ return false;
353
+ }
354
+ if (!isFeatureExposedInEnvironment(feature, environment)) {
355
+ return false;
356
+ }
357
+ return getFeatureRulesForEnvironment(feature, environment).some((rule) => (rule.percentage || 0) > 0);
358
+ }
359
+ function getFeatureEnvironmentStatus(feature) {
360
+ if (Array.isArray(feature.rules)) {
361
+ return {};
362
+ }
363
+ const environments = Object.keys(feature.rules || {});
364
+ const productionEnvironment = environments.find((environment) => environment.toLowerCase().startsWith("prod"));
365
+ if (!productionEnvironment) {
366
+ return {};
367
+ }
368
+ if (isFeatureEnabledInEnvironment(feature, productionEnvironment)) {
369
+ return {
370
+ environmentStatus: "production",
371
+ environmentStatusEnvironment: productionEnvironment,
372
+ };
373
+ }
374
+ for (const environment of environments) {
375
+ if (isFeatureEnabledInEnvironment(feature, environment)) {
376
+ return {
377
+ environmentStatus: "other",
378
+ environmentStatusEnvironment: productionEnvironment,
379
+ };
380
+ }
381
+ }
382
+ return {
383
+ environmentStatus: "disabled",
384
+ environmentStatusEnvironment: productionEnvironment,
385
+ };
386
+ }
387
+ function toCatalogHistoryEntry(entry, set) {
388
+ return {
389
+ commit: entry.commit,
390
+ author: entry.author,
391
+ timestamp: entry.timestamp,
392
+ entities: entry.entities.map((entity) => ({
393
+ type: entity.type,
394
+ key: entity.key,
395
+ set,
396
+ })),
397
+ };
398
+ }
399
+ async function getGitHistoryIndex(projectConfig, datasource) {
400
+ const rootEntries = (await datasource.listHistoryEntries()).map((entry) => toCatalogHistoryEntry(entry));
401
+ const bySet = {};
402
+ const entries = [...rootEntries];
403
+ if (projectConfig.sets) {
404
+ for (const set of await datasource.listSets()) {
405
+ const setDatasource = datasource.forSet(set);
406
+ bySet[set] = (await setDatasource.listHistoryEntries()).map((entry) => toCatalogHistoryEntry(entry, set));
407
+ entries.push(...bySet[set]);
408
+ }
409
+ }
410
+ entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
411
+ const byEntity = {};
412
+ const lastModifiedByEntity = {};
413
+ for (const entry of entries) {
414
+ for (const entity of entry.entities) {
415
+ const key = getHistoryEntityKey(entity.type, entity.key, entity.set);
416
+ if (!byEntity[key]) {
417
+ byEntity[key] = [];
418
+ }
419
+ byEntity[key].push(entry);
420
+ if (!lastModifiedByEntity[key]) {
421
+ lastModifiedByEntity[key] = toLastModified(entry);
422
+ }
423
+ }
424
+ }
425
+ return {
426
+ entries,
427
+ bySet,
428
+ byEntity,
429
+ lastModifiedByEntity,
430
+ };
431
+ }
432
+ async function writeHistoryPages(writer, outputDirectoryPath, entries) {
433
+ const totalPages = Math.max(1, Math.ceil(entries.length / CATALOG_HISTORY_PAGE_SIZE));
434
+ for (let page = 1; page <= totalPages; page++) {
435
+ await writer.write(path.join(outputDirectoryPath, `page-${page}.json`), {
436
+ page,
437
+ pageSize: CATALOG_HISTORY_PAGE_SIZE,
438
+ totalPages,
439
+ entries: entries.slice((page - 1) * CATALOG_HISTORY_PAGE_SIZE, page * CATALOG_HISTORY_PAGE_SIZE),
440
+ });
441
+ }
442
+ }
443
+ function getEntityFilePath(projectConfig, datasource, type, key) {
444
+ const directoryByType = {
445
+ feature: projectConfig.featuresDirectoryPath,
446
+ segment: projectConfig.segmentsDirectoryPath,
447
+ attribute: projectConfig.attributesDirectoryPath,
448
+ target: projectConfig.targetsDirectoryPath,
449
+ group: projectConfig.groupsDirectoryPath,
450
+ schema: projectConfig.schemasDirectoryPath,
451
+ test: projectConfig.testsDirectoryPath,
452
+ };
453
+ const filePath = key.split(projectConfig.namespaceCharacter || ".").join(path.sep);
454
+ return path.join(directoryByType[type], `${filePath}.${datasource.getExtension()}`);
455
+ }
456
+ function getSourceFileInfo(repositorySourceRootDirectoryPath, projectConfig, datasource, type, key, options = {}) {
457
+ const filePath = path.resolve(getEntityFilePath(projectConfig, datasource, type, key));
458
+ const absolutePath = options.resolveAbsolutePath ? getRealPath(filePath) : filePath;
459
+ return {
460
+ sourcePath: toPosixPath(path.relative(repositorySourceRootDirectoryPath, filePath)),
461
+ absolutePath,
462
+ };
463
+ }
464
+ function isExecutableFile(filePath) {
465
+ try {
466
+ const stat = fs.statSync(filePath);
467
+ if (!stat.isFile()) {
468
+ return false;
469
+ }
470
+ if (process.platform === "win32") {
471
+ return true;
472
+ }
473
+ fs.accessSync(filePath, fs.constants.X_OK);
474
+ return true;
475
+ }
476
+ catch {
477
+ return false;
478
+ }
479
+ }
480
+ function hasCommandInPath(command) {
481
+ const pathEntries = (process.env.PATH || "").split(path.delimiter).filter(Boolean);
482
+ const extensions = process.platform === "win32"
483
+ ? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean)
484
+ : [""];
485
+ return pathEntries.some((entry) => extensions.some((extension) => isExecutableFile(path.join(entry, `${command}${extension}`))));
486
+ }
487
+ function hasKnownEditorInstall(editor) {
488
+ if (hasCommandInPath(editor === "cursor" ? "cursor" : "code")) {
489
+ return true;
490
+ }
491
+ if (process.platform === "darwin") {
492
+ const appName = editor === "cursor" ? "Cursor.app" : "Visual Studio Code.app";
493
+ return [
494
+ path.join("/Applications", appName),
495
+ path.join(process.env.HOME || "", "Applications", appName),
496
+ ].some((appPath) => fs.existsSync(appPath));
497
+ }
498
+ if (process.platform === "win32") {
499
+ const localAppData = process.env.LOCALAPPDATA || "";
500
+ const programFiles = process.env.ProgramFiles || "";
501
+ const programFilesX86 = process.env["ProgramFiles(x86)"] || "";
502
+ const candidates = editor === "cursor"
503
+ ? [
504
+ path.join(localAppData, "Programs", "Cursor", "Cursor.exe"),
505
+ path.join(programFiles, "Cursor", "Cursor.exe"),
506
+ path.join(programFilesX86, "Cursor", "Cursor.exe"),
507
+ ]
508
+ : [
509
+ path.join(localAppData, "Programs", "Microsoft VS Code", "Code.exe"),
510
+ path.join(programFiles, "Microsoft VS Code", "Code.exe"),
511
+ path.join(programFilesX86, "Microsoft VS Code", "Code.exe"),
512
+ ];
513
+ return candidates.some((candidate) => fs.existsSync(candidate));
514
+ }
515
+ return false;
516
+ }
517
+ function detectDevEditors() {
518
+ const editors = [];
519
+ if (hasKnownEditorInstall("cursor")) {
520
+ editors.push({ id: "cursor", label: "Cursor", icon: "cursor" });
521
+ }
522
+ if (hasKnownEditorInstall("vscode")) {
523
+ editors.push({ id: "vscode", label: "VS Code", icon: "vscode" });
524
+ }
525
+ return editors;
526
+ }
527
+ function encodeEditorPath(filePath) {
528
+ return encodeURI(filePath.split(path.sep).join("/")).replace(/#/g, "%23").replace(/\?/g, "%3F");
529
+ }
530
+ function getEditorUri(editor, filePath) {
531
+ return `${editor === "cursor" ? "cursor" : "vscode"}://file/${encodeEditorPath(filePath)}`;
532
+ }
533
+ function getEditorLinks(editors, sourceFileInfo) {
534
+ if (editors.length === 0) {
535
+ return undefined;
536
+ }
537
+ return Object.fromEntries(editors.map((editor) => [editor.id, getEditorUri(editor.id, sourceFileInfo.absolutePath)]));
538
+ }
539
+ function getCurrentBranch(rootDirectoryPath) {
540
+ try {
541
+ return runGit(rootDirectoryPath, ["symbolic-ref", "--short", "HEAD"]).trim() || "HEAD";
542
+ }
543
+ catch {
544
+ return "HEAD";
545
+ }
546
+ }
547
+ function getRepositoryRootDirectoryPath(rootDirectoryPath) {
548
+ try {
549
+ return (getRealPath(runGit(rootDirectoryPath, ["rev-parse", "--show-toplevel"]).trim()) ||
550
+ getRealPath(rootDirectoryPath));
551
+ }
552
+ catch {
553
+ return getRealPath(rootDirectoryPath);
554
+ }
555
+ }
556
+ function getRepositorySourceRootDirectoryPath(rootDirectoryPath) {
557
+ try {
558
+ const gitRootDirectoryPath = runGit(rootDirectoryPath, ["rev-parse", "--show-toplevel"]).trim() || rootDirectoryPath;
559
+ const realRootDirectoryPath = getRealPath(rootDirectoryPath);
560
+ if (realRootDirectoryPath !== rootDirectoryPath) {
561
+ return path.resolve(rootDirectoryPath, path.relative(realRootDirectoryPath, gitRootDirectoryPath));
562
+ }
563
+ return gitRootDirectoryPath;
564
+ }
565
+ catch {
566
+ return rootDirectoryPath;
567
+ }
568
+ }
569
+ function getOwnerAndRepoFromGitRemote(origin, host) {
570
+ const escapedHost = host.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
571
+ const match = origin.match(new RegExp(`${escapedHost}[:/]([^/]+)/(.+?)(?:\\.git)?$`));
572
+ if (!match) {
573
+ return undefined;
574
+ }
575
+ return {
576
+ owner: match[1],
577
+ repo: match[2],
578
+ };
579
+ }
580
+ function getRepoLinks(rootDirectoryPath) {
581
+ try {
582
+ const origin = runGit(rootDirectoryPath, ["config", "--get", "remote.origin.url"]).trim();
583
+ const branch = encodeURI(getCurrentBranch(rootDirectoryPath));
584
+ const providers = {
585
+ github: {
586
+ host: "github.com",
587
+ repository: (owner, repo) => `https://github.com/${owner}/${repo}`,
588
+ source: (owner, repo) => `https://github.com/${owner}/${repo}/blob/${branch}/{{path}}`,
589
+ commit: (owner, repo) => `https://github.com/${owner}/${repo}/commit/{{hash}}`,
590
+ },
591
+ gitlab: {
592
+ host: "gitlab.com",
593
+ repository: (owner, repo) => `https://gitlab.com/${owner}/${repo}`,
594
+ source: (owner, repo) => `https://gitlab.com/${owner}/${repo}/-/blob/${branch}/{{path}}`,
595
+ commit: (owner, repo) => `https://gitlab.com/${owner}/${repo}/-/commit/{{hash}}`,
596
+ },
597
+ bitbucket: {
598
+ host: "bitbucket.org",
599
+ repository: (owner, repo) => `https://bitbucket.org/${owner}/${repo}`,
600
+ source: (owner, repo) => `https://bitbucket.org/${owner}/${repo}/src/${branch}/{{path}}`,
601
+ commit: (owner, repo) => `https://bitbucket.org/${owner}/${repo}/commits/{{hash}}`,
602
+ },
603
+ };
604
+ for (const provider of Object.keys(providers)) {
605
+ const config = providers[provider];
606
+ const details = getOwnerAndRepoFromGitRemote(origin, config.host);
607
+ if (details) {
608
+ return {
609
+ provider,
610
+ repository: config.repository(details.owner, details.repo),
611
+ source: config.source(details.owner, details.repo),
612
+ commit: config.commit(details.owner, details.repo),
613
+ };
614
+ }
615
+ }
616
+ }
617
+ catch {
618
+ return undefined;
619
+ }
620
+ }
621
+ function buildRelationships(maps) {
622
+ const relationships = {
623
+ featureTargets: {},
624
+ featureTests: {},
625
+ featureRequiredBy: {},
626
+ featureSegments: {},
627
+ featureAttributes: {},
628
+ featureSchemas: {},
629
+ featureGroups: {},
630
+ segmentsUsedInFeatures: {},
631
+ attributesUsedInFeatures: {},
632
+ attributesUsedInSegments: {},
633
+ segmentTargets: {},
634
+ attributeTargets: {},
635
+ groupsUsedInFeatures: {},
636
+ schemasUsedInFeatures: {},
637
+ segmentTests: {},
638
+ targetFeatures: {},
639
+ };
640
+ for (const [featureKey, feature] of Object.entries(maps.feature)) {
641
+ const required = new Set();
642
+ const segments = new Set();
643
+ const attributes = new Set();
644
+ const schemas = new Set();
645
+ const groups = new Set();
646
+ collectFeatureKeysFromRequired(feature.required, required);
647
+ collectSchemaKeysFromVariables(feature.variablesSchema, schemas);
648
+ collectGroupKeysFromRules(feature.rules, groups);
649
+ for (const rule of getFeatureRules(feature)) {
650
+ collectSegmentKeys(rule.segments, segments);
651
+ collectAttributeKeysFromConditions(rule.conditions, attributes);
652
+ }
653
+ for (const force of getFeatureForce(feature)) {
654
+ collectSegmentKeys(force.segments, segments);
655
+ collectAttributeKeysFromConditions(force.conditions, attributes);
656
+ }
657
+ for (const variation of feature.variations || []) {
658
+ for (const overrides of Object.values(variation.variableOverrides || {})) {
659
+ for (const override of overrides) {
660
+ collectSegmentKeys(override.segments, segments);
661
+ collectAttributeKeysFromConditions(override.conditions, attributes);
662
+ }
663
+ }
664
+ }
665
+ for (const requiredKey of required) {
666
+ addToSet(relationships.featureRequiredBy, requiredKey, featureKey);
667
+ }
668
+ for (const segmentKey of segments) {
669
+ addToSet(relationships.featureSegments, featureKey, segmentKey);
670
+ addToSet(relationships.segmentsUsedInFeatures, segmentKey, featureKey);
671
+ }
672
+ for (const attributeKey of attributes) {
673
+ addToSet(relationships.featureAttributes, featureKey, attributeKey);
674
+ addToSet(relationships.attributesUsedInFeatures, attributeKey, featureKey);
675
+ }
676
+ for (const schemaKey of schemas) {
677
+ addToSet(relationships.featureSchemas, featureKey, schemaKey);
678
+ addToSet(relationships.schemasUsedInFeatures, schemaKey, featureKey);
679
+ }
680
+ for (const groupKey of groups) {
681
+ addToSet(relationships.featureGroups, featureKey, groupKey);
682
+ addToSet(relationships.groupsUsedInFeatures, groupKey, featureKey);
683
+ }
684
+ }
685
+ for (const [segmentKey, segment] of Object.entries(maps.segment)) {
686
+ const attributes = new Set();
687
+ collectAttributeKeysFromConditions(segment.conditions, attributes);
688
+ for (const attributeKey of attributes) {
689
+ addToSet(relationships.attributesUsedInSegments, attributeKey, segmentKey);
690
+ }
691
+ }
692
+ for (const [targetKey, target] of Object.entries(maps.target)) {
693
+ for (const [featureKey, feature] of Object.entries(maps.feature)) {
694
+ if (targetIncludesFeature(target, featureKey, feature)) {
695
+ addToSet(relationships.targetFeatures, targetKey, featureKey);
696
+ addToSet(relationships.featureTargets, featureKey, targetKey);
697
+ }
698
+ }
699
+ }
700
+ for (const [featureKey, targetKeys] of Object.entries(relationships.featureTargets)) {
701
+ for (const targetKey of targetKeys) {
702
+ for (const segmentKey of relationships.featureSegments[featureKey] || []) {
703
+ addToSet(relationships.segmentTargets, segmentKey, targetKey);
704
+ const segmentAttributes = new Set();
705
+ collectAttributeKeysFromConditions(maps.segment[segmentKey]?.conditions, segmentAttributes);
706
+ for (const attributeKey of segmentAttributes) {
707
+ addToSet(relationships.attributeTargets, attributeKey, targetKey);
708
+ }
709
+ }
710
+ for (const attributeKey of relationships.featureAttributes[featureKey] || []) {
711
+ addToSet(relationships.attributeTargets, attributeKey, targetKey);
712
+ }
713
+ }
714
+ }
715
+ for (const [testKey, test] of Object.entries(maps.test)) {
716
+ if ("feature" in test) {
717
+ addToSet(relationships.featureTests, test.feature, testKey);
718
+ }
719
+ if ("segment" in test) {
720
+ addToSet(relationships.segmentTests, test.segment, testKey);
721
+ }
722
+ }
723
+ return relationships;
724
+ }
725
+ function getEntityRelationships(type, key, relationships) {
726
+ if (type === "feature") {
727
+ return {
728
+ targets: sortSet(relationships.featureTargets[key]),
729
+ tests: sortSet(relationships.featureTests[key]),
730
+ requiredBy: sortSet(relationships.featureRequiredBy[key]),
731
+ segments: sortSet(relationships.featureSegments[key]),
732
+ attributes: sortSet(relationships.featureAttributes[key]),
733
+ schemas: sortSet(relationships.featureSchemas[key]),
734
+ groups: sortSet(relationships.featureGroups[key]),
735
+ };
736
+ }
737
+ if (type === "segment") {
738
+ return {
739
+ features: sortSet(relationships.segmentsUsedInFeatures[key]),
740
+ tests: sortSet(relationships.segmentTests[key]),
741
+ attributes: sortSet(relationships.attributesUsedInSegments[key]),
742
+ targets: sortSet(relationships.segmentTargets[key]),
743
+ };
744
+ }
745
+ if (type === "attribute") {
746
+ return {
747
+ features: sortSet(relationships.attributesUsedInFeatures[key]),
748
+ segments: sortSet(relationships.attributesUsedInSegments[key]),
749
+ targets: sortSet(relationships.attributeTargets[key]),
750
+ };
751
+ }
752
+ if (type === "target") {
753
+ return {
754
+ features: sortSet(relationships.targetFeatures[key]),
755
+ };
756
+ }
757
+ if (type === "schema") {
758
+ return {
759
+ features: sortSet(relationships.schemasUsedInFeatures[key]),
760
+ };
761
+ }
762
+ if (type === "group") {
763
+ return {
764
+ features: sortSet(relationships.groupsUsedInFeatures[key]),
765
+ };
766
+ }
767
+ return {};
768
+ }
769
+ async function buildSetCatalog(context, set, projectConfig, datasource, outputRelativeDirectory) {
770
+ const outputDirectoryPath = path.join(context.dataDirectoryPath, outputRelativeDirectory);
771
+ const setStartedAt = context.progress.setStart(set || undefined);
772
+ const entitiesStartedAt = context.progress.step("Processing entities");
773
+ const [featureKeys, segmentKeys, attributeKeys, targetKeys, groupKeys, schemaKeys, testKeys] = await Promise.all([
774
+ datasource.listFeatures(),
775
+ datasource.listSegments(),
776
+ datasource.listAttributes(),
777
+ datasource.listTargets(),
778
+ datasource.listGroups(),
779
+ datasource.listSchemas(),
780
+ datasource.listTests(),
781
+ ]);
782
+ const maps = {
783
+ feature: await readAll(featureKeys, (key) => datasource.readFeature(key)),
784
+ segment: await readAll(segmentKeys, (key) => datasource.readSegment(key)),
785
+ attribute: await readAll(attributeKeys, (key) => datasource.readAttribute(key)),
786
+ target: await readAll(targetKeys, (key) => datasource.readTarget(key)),
787
+ group: await readAll(groupKeys, (key) => datasource.readGroup(key)),
788
+ schema: await readAll(schemaKeys, (key) => datasource.readSchema(key)),
789
+ test: await readAll(testKeys, (key) => datasource.readTest(key)),
790
+ };
791
+ context.progress.done(entitiesStartedAt, `(${[
792
+ pluralize(featureKeys.length, "feature"),
793
+ pluralize(segmentKeys.length, "segment"),
794
+ pluralize(attributeKeys.length, "attribute"),
795
+ pluralize(targetKeys.length, "target"),
796
+ pluralize(groupKeys.length, "group"),
797
+ pluralize(schemaKeys.length, "schema"),
798
+ pluralize(testKeys.length, "test"),
799
+ ].join(", ")})`);
800
+ const relationshipsStartedAt = context.progress.step("Mapping relationships");
801
+ const relationships = buildRelationships(maps);
802
+ context.progress.done(relationshipsStartedAt);
803
+ const history = set ? context.historyIndex.bySet[set] || [] : context.historyIndex.entries;
804
+ const index = {
805
+ set,
806
+ counts: {
807
+ feature: featureKeys.length,
808
+ segment: segmentKeys.length,
809
+ attribute: attributeKeys.length,
810
+ target: targetKeys.length,
811
+ group: groupKeys.length,
812
+ schema: schemaKeys.length,
813
+ test: testKeys.length,
814
+ },
815
+ entities: {
816
+ feature: [],
817
+ segment: [],
818
+ attribute: [],
819
+ target: [],
820
+ group: [],
821
+ schema: [],
822
+ test: [],
823
+ },
824
+ };
825
+ const historyStartedAt = context.progress.step("Writing history pages");
826
+ await writeHistoryPages(context.writer, path.join(outputDirectoryPath, "history"), history);
827
+ context.progress.done(historyStartedAt, `(${pluralize(history.length, "entry", "entries")})`);
828
+ const entityPlan = [
829
+ { type: "feature", keys: featureKeys, entities: maps.feature },
830
+ { type: "segment", keys: segmentKeys, entities: maps.segment },
831
+ { type: "attribute", keys: attributeKeys, entities: maps.attribute },
832
+ { type: "target", keys: targetKeys, entities: maps.target },
833
+ { type: "group", keys: groupKeys, entities: maps.group },
834
+ { type: "schema", keys: schemaKeys, entities: maps.schema },
835
+ { type: "test", keys: testKeys, entities: maps.test },
836
+ ];
837
+ const detailsStartedAt = context.progress.step("Writing entity details");
838
+ for (const plan of entityPlan) {
839
+ for (const key of plan.keys) {
840
+ const entity = plan.entities[key];
841
+ const featureEnvironmentStatus = plan.type === "feature" ? getFeatureEnvironmentStatus(entity) : {};
842
+ const sourceFileInfo = getSourceFileInfo(context.repositorySourceRootDirectoryPath, projectConfig, datasource, plan.type, key, { resolveAbsolutePath: context.devEditors.length > 0 });
843
+ const lastModified = getLastModified(context.historyIndex, plan.type, key, set);
844
+ const entityRelationships = getEntityRelationships(plan.type, key, relationships);
845
+ const detail = {
846
+ type: plan.type,
847
+ key,
848
+ entity,
849
+ sourcePath: sourceFileInfo.sourcePath,
850
+ editLinks: getEditorLinks(context.devEditors, sourceFileInfo),
851
+ lastModified,
852
+ relationships: entityRelationships,
853
+ environments: projectConfig.environments,
854
+ historyPath: `${path.posix.join("data", outputRelativeDirectory.split(path.sep).join(path.posix.sep), "entities", plan.type, encodeKey(key), "history")}`,
855
+ };
856
+ await context.writer.write(path.join(outputDirectoryPath, "entities", plan.type, `${encodeKey(key)}.json`), detail);
857
+ await writeHistoryPages(context.writer, path.join(outputDirectoryPath, "entities", plan.type, encodeKey(key), "history"), context.historyIndex.byEntity[getHistoryEntityKey(plan.type, key, set)] || []);
858
+ index.entities[plan.type].push(getEntitySummary(entity, plan.type, key, context.historyIndex, set, {
859
+ tags: entity.tags ? entity.tags : undefined,
860
+ targets: plan.type === "feature"
861
+ ? sortSet(relationships.featureTargets[key])
862
+ : plan.type === "segment"
863
+ ? sortSet(relationships.segmentTargets[key])
864
+ : plan.type === "attribute"
865
+ ? sortSet(relationships.attributeTargets[key])
866
+ : undefined,
867
+ usedInFeatureCount: plan.type === "segment"
868
+ ? sortSet(relationships.segmentsUsedInFeatures[key]).length
869
+ : undefined,
870
+ usedInSegmentCount: plan.type === "attribute"
871
+ ? sortSet(relationships.attributesUsedInSegments[key]).length
872
+ : undefined,
873
+ environments: plan.type === "feature"
874
+ ? getFeatureEnvironmentKeys(entity, projectConfig.environments)
875
+ : projectConfig.environments,
876
+ variationValues: plan.type === "feature" ? getFeatureVariationValues(entity) : undefined,
877
+ variableKeys: plan.type === "feature" ? getFeatureVariableKeys(entity) : undefined,
878
+ hasVariations: plan.type === "feature" ? Boolean(entity.variations?.length) : undefined,
879
+ hasVariables: plan.type === "feature"
880
+ ? Object.keys(entity.variablesSchema || {}).length > 0
881
+ : undefined,
882
+ ...featureEnvironmentStatus,
883
+ }));
884
+ }
885
+ index.entities[plan.type].sort((a, b) => a.key.localeCompare(b.key));
886
+ }
887
+ context.progress.done(detailsStartedAt);
888
+ await context.writer.write(path.join(outputDirectoryPath, "index.json"), index);
889
+ context.progress.done(setStartedAt);
890
+ return index;
891
+ }
892
+ async function copyCatalogAssets(outputDirectoryPath) {
893
+ const catalogPackagePath = path.dirname(require.resolve("@featurevisor/catalog/package.json"));
894
+ const catalogDistPath = path.join(catalogPackagePath, "dist");
895
+ if (!fs.existsSync(catalogDistPath)) {
896
+ throw new Error("Catalog UI assets are missing. Run `npm run build --workspace @featurevisor/catalog` first.");
897
+ }
898
+ await fs.promises.cp(catalogDistPath, outputDirectoryPath, { recursive: true });
899
+ }
900
+ async function exportCatalog(runtime, rootDirectoryPath, projectConfig, datasource, options = {}) {
901
+ const outputDirectoryPath = options.outDir
902
+ ? path.resolve(rootDirectoryPath, options.outDir)
903
+ : projectConfig.catalogDirectoryPath;
904
+ const dataDirectoryPath = path.join(outputDirectoryPath, "data");
905
+ const progress = new CatalogProgressReporter(rootDirectoryPath, outputDirectoryPath);
906
+ const writer = new CatalogJsonWriter();
907
+ progress.start({
908
+ browserRouter: options.browserRouter !== false,
909
+ sets: projectConfig.sets === true,
910
+ });
911
+ let stepStartedAt = progress.step("Preparing output directory");
912
+ if (options.preserveAssets) {
913
+ await fs.promises.rm(dataDirectoryPath, { recursive: true, force: true });
914
+ }
915
+ else {
916
+ await fs.promises.rm(outputDirectoryPath, { recursive: true, force: true });
917
+ }
918
+ await fs.promises.mkdir(dataDirectoryPath, { recursive: true });
919
+ progress.done(stepStartedAt);
920
+ if (options.copyAssets !== false) {
921
+ stepStartedAt = progress.step("Copying Catalog UI assets");
922
+ await copyCatalogAssets(outputDirectoryPath);
923
+ progress.done(stepStartedAt);
924
+ }
925
+ const devEditors = options.dev
926
+ ? options.devSession?.devEditors || options.devEditors || detectDevEditors()
927
+ : [];
928
+ stepStartedAt = progress.step("Resolving repository links");
929
+ const repositoryRootDirectoryPath = options.devSession?.repositoryRootDirectoryPath ||
930
+ getRepositoryRootDirectoryPath(rootDirectoryPath);
931
+ const repositorySourceRootDirectoryPath = options.devSession?.repositorySourceRootDirectoryPath ||
932
+ getRepositorySourceRootDirectoryPath(rootDirectoryPath);
933
+ const links = options.devSession?.links || getRepoLinks(repositoryRootDirectoryPath);
934
+ progress.done(stepStartedAt, links?.repository ? `(${links.repository})` : "(none)");
935
+ stepStartedAt = progress.step("Reading Git history");
936
+ const historyIndex = options.devSession?.historyIndex || (await getGitHistoryIndex(projectConfig, datasource));
937
+ progress.done(stepStartedAt, `(${pluralize(historyIndex.entries.length, "commit")})`);
938
+ const context = {
939
+ rootDirectoryPath,
940
+ repositorySourceRootDirectoryPath,
941
+ outputDirectoryPath,
942
+ dataDirectoryPath,
943
+ historyIndex,
944
+ devEditors,
945
+ progress,
946
+ writer,
947
+ };
948
+ stepStartedAt = progress.step("Discovering project sets");
949
+ const executions = await runtime.getProjectSetExecutions(projectConfig, datasource);
950
+ progress.done(stepStartedAt, projectConfig.sets
951
+ ? `(${executions.map((execution) => execution.set).join(", ") || "none"})`
952
+ : "(root)");
953
+ stepStartedAt = progress.step("Writing project history");
954
+ await writeHistoryPages(writer, path.join(dataDirectoryPath, "project", "history"), historyIndex.entries);
955
+ progress.done(stepStartedAt, `(${pluralize(historyIndex.entries.length, "entry", "entries")})`);
956
+ const setIndexes = {};
957
+ for (const execution of executions) {
958
+ const outputRelativeDirectory = projectConfig.sets ? path.join("sets", execution.set) : "root";
959
+ setIndexes[execution.set || "root"] = await buildSetCatalog(context, execution.set, execution.projectConfig, execution.datasource, outputRelativeDirectory);
960
+ }
961
+ stepStartedAt = progress.step("Writing manifest");
962
+ const manifest = {
963
+ schemaVersion: CATALOG_SCHEMA_VERSION,
964
+ generatedAt: new Date().toISOString(),
965
+ router: options.browserRouter === false ? "hash" : "browser",
966
+ sets: projectConfig.sets,
967
+ setKeys: projectConfig.sets ? (0, entityTypes_1.sortSetKeys)(executions.map((execution) => execution.set)) : [],
968
+ projectConfig: {
969
+ tags: projectConfig.tags,
970
+ environments: projectConfig.environments,
971
+ },
972
+ dev: options.dev ? { editors: devEditors } : undefined,
973
+ links,
974
+ paths: {
975
+ projectHistory: "data/project/history/page-1.json",
976
+ root: projectConfig.sets ? undefined : "data/root/index.json",
977
+ sets: projectConfig.sets
978
+ ? Object.fromEntries(executions.map((execution) => [
979
+ execution.set,
980
+ `data/sets/${encodeURIComponent(execution.set)}/index.json`,
981
+ ]))
982
+ : undefined,
983
+ },
984
+ counts: Object.fromEntries(Object.keys(setIndexes).map((key) => [key, setIndexes[key].counts])),
985
+ };
986
+ await writer.write(path.join(dataDirectoryPath, "manifest.json"), manifest);
987
+ progress.done(stepStartedAt);
988
+ progress.complete();
989
+ return {
990
+ outputDirectoryPath,
991
+ manifest,
992
+ };
993
+ }
994
+ function getContentType(filePath) {
995
+ const extension = path.extname(filePath);
996
+ switch (extension) {
997
+ case ".js":
998
+ return "text/javascript";
999
+ case ".css":
1000
+ return "text/css";
1001
+ case ".json":
1002
+ return "application/json";
1003
+ case ".png":
1004
+ return "image/png";
1005
+ case ".svg":
1006
+ return "image/svg+xml";
1007
+ case ".ico":
1008
+ return "image/x-icon";
1009
+ default:
1010
+ return "text/html";
1011
+ }
1012
+ }
1013
+ function getCatalogLiveReloadClientScript() {
1014
+ return [
1015
+ "<script>",
1016
+ "(() => {",
1017
+ ' const source = new EventSource("/__featurevisor_catalog_reload");',
1018
+ ' source.addEventListener("reload", () => window.location.reload());',
1019
+ " source.onerror = () => {",
1020
+ " source.close();",
1021
+ " setTimeout(() => window.location.reload(), 1000);",
1022
+ " };",
1023
+ "})();",
1024
+ "</script>",
1025
+ ].join("");
1026
+ }
1027
+ function injectCatalogLiveReloadClient(html) {
1028
+ const script = getCatalogLiveReloadClientScript();
1029
+ if (html.includes("</body>")) {
1030
+ return html.replace("</body>", `${script}</body>`);
1031
+ }
1032
+ return `${html}${script}`;
1033
+ }
1034
+ function getCatalogInputWatchPaths(rootDirectoryPath, projectConfig) {
1035
+ const paths = [path.join(rootDirectoryPath, "featurevisor.config.js")];
1036
+ if (projectConfig.sets) {
1037
+ paths.push(projectConfig.setsDirectoryPath);
1038
+ return paths;
1039
+ }
1040
+ paths.push(projectConfig.featuresDirectoryPath, projectConfig.segmentsDirectoryPath, projectConfig.attributesDirectoryPath, projectConfig.targetsDirectoryPath, projectConfig.groupsDirectoryPath, projectConfig.schemasDirectoryPath, projectConfig.testsDirectoryPath);
1041
+ return paths.filter((entry) => typeof entry === "string" && entry.length > 0);
1042
+ }
1043
+ function createCatalogInputWatcher(rootDirectoryPath, projectConfig, ignoredDirectoryPaths, onChange) {
1044
+ const watchPaths = getCatalogInputWatchPaths(rootDirectoryPath, projectConfig);
1045
+ function shouldIgnore(targetPath) {
1046
+ const resolvedTargetPath = path.resolve(targetPath);
1047
+ return ignoredDirectoryPaths.some((ignoredDirectoryPath) => {
1048
+ const resolvedIgnoredPath = path.resolve(ignoredDirectoryPath);
1049
+ return (resolvedTargetPath === resolvedIgnoredPath ||
1050
+ resolvedTargetPath.startsWith(`${resolvedIgnoredPath}${path.sep}`));
1051
+ });
1052
+ }
1053
+ function collectSnapshotEntries(directoryPath, snapshotEntries) {
1054
+ if (shouldIgnore(directoryPath)) {
1055
+ return;
1056
+ }
1057
+ let entries = [];
1058
+ try {
1059
+ entries = fs.readdirSync(directoryPath, { withFileTypes: true });
1060
+ }
1061
+ catch {
1062
+ return;
1063
+ }
1064
+ for (const entry of entries) {
1065
+ const entryPath = path.join(directoryPath, entry.name);
1066
+ if (shouldIgnore(entryPath)) {
1067
+ continue;
1068
+ }
1069
+ if (entry.isDirectory()) {
1070
+ collectSnapshotEntries(entryPath, snapshotEntries);
1071
+ continue;
1072
+ }
1073
+ if (!entry.isFile()) {
1074
+ continue;
1075
+ }
1076
+ try {
1077
+ const stat = fs.statSync(entryPath);
1078
+ snapshotEntries.set(entryPath, `${stat.size}:${stat.mtimeMs}`);
1079
+ }
1080
+ catch {
1081
+ // Ignore transient editor save races.
1082
+ }
1083
+ }
1084
+ }
1085
+ function createSnapshot() {
1086
+ const snapshotEntries = new Map();
1087
+ for (const watchPath of watchPaths) {
1088
+ if (!fs.existsSync(watchPath)) {
1089
+ continue;
1090
+ }
1091
+ const stat = fs.statSync(watchPath);
1092
+ if (stat.isFile()) {
1093
+ snapshotEntries.set(watchPath, `${stat.size}:${stat.mtimeMs}`);
1094
+ continue;
1095
+ }
1096
+ collectSnapshotEntries(watchPath, snapshotEntries);
1097
+ }
1098
+ return snapshotEntries;
1099
+ }
1100
+ function getSnapshotChanges(previous, next) {
1101
+ const changedPaths = new Set();
1102
+ for (const [filePath, signature] of Array.from(next.entries())) {
1103
+ if (previous.get(filePath) !== signature) {
1104
+ changedPaths.add(filePath);
1105
+ }
1106
+ }
1107
+ for (const filePath of Array.from(previous.keys())) {
1108
+ if (!next.has(filePath)) {
1109
+ changedPaths.add(filePath);
1110
+ }
1111
+ }
1112
+ return Array.from(changedPaths);
1113
+ }
1114
+ let previousSnapshot = createSnapshot();
1115
+ const interval = setInterval(() => {
1116
+ const nextSnapshot = createSnapshot();
1117
+ const changedPaths = getSnapshotChanges(previousSnapshot, nextSnapshot);
1118
+ previousSnapshot = nextSnapshot;
1119
+ if (changedPaths.length > 0) {
1120
+ onChange(changedPaths);
1121
+ }
1122
+ }, 1000);
1123
+ return () => clearInterval(interval);
1124
+ }
1125
+ async function serveCatalog(runtime, rootDirectoryPath, projectConfig, datasource, options = {}) {
1126
+ const outputDirectoryPath = options.outDir
1127
+ ? path.resolve(rootDirectoryPath, options.outDir)
1128
+ : projectConfig.catalogDirectoryPath;
1129
+ if (!fs.existsSync(outputDirectoryPath)) {
1130
+ await exportCatalog(runtime, rootDirectoryPath, projectConfig, datasource, {
1131
+ outDir: outputDirectoryPath,
1132
+ browserRouter: options.browserRouter,
1133
+ });
1134
+ }
1135
+ const port = Number(options.port || 3000);
1136
+ const liveReloadClients = new Set();
1137
+ function triggerReload() {
1138
+ liveReloadClients.forEach((client) => {
1139
+ client.write("event: reload\n");
1140
+ client.write("data: reload\n\n");
1141
+ });
1142
+ }
1143
+ const server = http.createServer((request, response) => {
1144
+ const requestedUrl = decodeURIComponent((request.url || "/").split("?")[0]);
1145
+ if (options.liveReload && requestedUrl === "/__featurevisor_catalog_reload") {
1146
+ response.writeHead(200, {
1147
+ "Content-Type": "text/event-stream",
1148
+ "Cache-Control": "no-cache, no-transform",
1149
+ Connection: "keep-alive",
1150
+ });
1151
+ response.write("\n");
1152
+ liveReloadClients.add(response);
1153
+ request.on("close", () => liveReloadClients.delete(response));
1154
+ return;
1155
+ }
1156
+ const requestedPath = requestedUrl === "/" ? "/index.html" : requestedUrl;
1157
+ const assetPath = requestedPath === "/favicon.ico" ? "/favicon.png" : requestedPath;
1158
+ const filePath = path.join(outputDirectoryPath, assetPath);
1159
+ const safeFilePath = filePath.startsWith(outputDirectoryPath)
1160
+ ? filePath
1161
+ : path.join(outputDirectoryPath, "index.html");
1162
+ fs.readFile(safeFilePath, (error, content) => {
1163
+ if (!error) {
1164
+ if (options.liveReload && path.basename(safeFilePath) === "index.html") {
1165
+ response.writeHead(200, { "Content-Type": "text/html" });
1166
+ response.end(injectCatalogLiveReloadClient(content.toString("utf8")));
1167
+ return;
1168
+ }
1169
+ response.writeHead(200, { "Content-Type": getContentType(safeFilePath) });
1170
+ response.end(content);
1171
+ return;
1172
+ }
1173
+ if (requestedPath.startsWith("/assets/") ||
1174
+ requestedPath.startsWith("/data/") ||
1175
+ requestedPath === "/favicon.ico") {
1176
+ response.writeHead(404, { "Content-Type": "text/plain" });
1177
+ response.end("404 Not Found");
1178
+ return;
1179
+ }
1180
+ fs.readFile(path.join(outputDirectoryPath, "index.html"), (indexError, indexContent) => {
1181
+ if (indexError) {
1182
+ response.writeHead(500, { "Content-Type": "text/plain" });
1183
+ response.end("Catalog index.html not found.");
1184
+ return;
1185
+ }
1186
+ response.writeHead(200, { "Content-Type": "text/html" });
1187
+ response.end(options.liveReload
1188
+ ? injectCatalogLiveReloadClient(indexContent.toString("utf8"))
1189
+ : indexContent);
1190
+ });
1191
+ });
1192
+ });
1193
+ server.on("error", (error) => {
1194
+ console.error(`Unable to serve catalog on http://127.0.0.1:${port}/`);
1195
+ console.error(error);
1196
+ process.exitCode = 1;
1197
+ });
1198
+ server.listen(port, "127.0.0.1", () => {
1199
+ console.log(`Catalog running at http://127.0.0.1:${port}/`);
1200
+ });
1201
+ return {
1202
+ close: () => new Promise((resolve, reject) => {
1203
+ server.close((error) => {
1204
+ if (error) {
1205
+ reject(error);
1206
+ return;
1207
+ }
1208
+ resolve();
1209
+ });
1210
+ }),
1211
+ triggerReload,
1212
+ };
1213
+ }
1214
+ async function createCatalogDevSession(rootDirectoryPath, projectConfig, datasource, options = {}) {
1215
+ const outputDirectoryPath = options.outDir
1216
+ ? path.resolve(rootDirectoryPath, options.outDir)
1217
+ : projectConfig.catalogDirectoryPath;
1218
+ const repositoryRootDirectoryPath = getRepositoryRootDirectoryPath(rootDirectoryPath);
1219
+ const repositorySourceRootDirectoryPath = getRepositorySourceRootDirectoryPath(rootDirectoryPath);
1220
+ return {
1221
+ outputDirectoryPath,
1222
+ devEditors: options.devEditors || detectDevEditors(),
1223
+ historyIndex: await getGitHistoryIndex(projectConfig, datasource),
1224
+ links: getRepoLinks(repositoryRootDirectoryPath),
1225
+ repositoryRootDirectoryPath,
1226
+ repositorySourceRootDirectoryPath,
1227
+ };
1228
+ }
1229
+ function createCatalogApi(runtime) {
1230
+ return {
1231
+ exportCatalog: (rootDirectoryPath, projectConfig, datasource, options = {}) => exportCatalog(runtime, rootDirectoryPath, projectConfig, datasource, options),
1232
+ serveCatalog: (rootDirectoryPath, projectConfig, datasource, options = {}) => serveCatalog(runtime, rootDirectoryPath, projectConfig, datasource, options),
1233
+ };
1234
+ }
1235
+ function shouldCopyAssets(parsed) {
1236
+ return parsed.assets !== false && parsed.noAssets !== true && parsed["no-assets"] !== true;
1237
+ }
1238
+ function createCatalogPlugin(runtime, api = createCatalogApi(runtime)) {
1239
+ return {
1240
+ command: "catalog [subcommand]",
1241
+ handler: async ({ rootDirectoryPath, projectConfig, datasource, parsed }) => {
1242
+ const allowedSubcommands = ["export", "serve"];
1243
+ const browserRouter = !(parsed.hashRouter || parsed["hash-router"]);
1244
+ if (!parsed.subcommand) {
1245
+ const outputDirectoryPath = parsed.outDir
1246
+ ? path.resolve(rootDirectoryPath, parsed.outDir)
1247
+ : projectConfig.catalogDirectoryPath;
1248
+ const devSession = await createCatalogDevSession(rootDirectoryPath, projectConfig, datasource, {
1249
+ outDir: parsed.outDir,
1250
+ });
1251
+ await api.exportCatalog(rootDirectoryPath, projectConfig, datasource, {
1252
+ outDir: parsed.outDir,
1253
+ copyAssets: shouldCopyAssets(parsed),
1254
+ browserRouter,
1255
+ dev: true,
1256
+ devSession,
1257
+ });
1258
+ const server = await api.serveCatalog(rootDirectoryPath, projectConfig, datasource, {
1259
+ outDir: parsed.outDir,
1260
+ port: parsed.port || parsed.p,
1261
+ browserRouter,
1262
+ liveReload: true,
1263
+ });
1264
+ const ignoredDirectoryPaths = [
1265
+ path.join(rootDirectoryPath, ".git"),
1266
+ path.join(rootDirectoryPath, "node_modules"),
1267
+ path.join(rootDirectoryPath, ".featurevisor"),
1268
+ path.join(rootDirectoryPath, "datafiles"),
1269
+ path.join(rootDirectoryPath, "catalog"),
1270
+ outputDirectoryPath,
1271
+ ];
1272
+ let exportInFlight = false;
1273
+ let exportQueued = false;
1274
+ let debounceTimer = null;
1275
+ const runRebuildAndReload = async () => {
1276
+ if (exportInFlight) {
1277
+ exportQueued = true;
1278
+ return;
1279
+ }
1280
+ exportInFlight = true;
1281
+ console.log("\n[catalog] Rebuilding because project files changed");
1282
+ try {
1283
+ await api.exportCatalog(rootDirectoryPath, projectConfig, datasource, {
1284
+ outDir: parsed.outDir,
1285
+ copyAssets: false,
1286
+ preserveAssets: true,
1287
+ browserRouter,
1288
+ dev: true,
1289
+ devSession,
1290
+ });
1291
+ server.triggerReload();
1292
+ }
1293
+ catch (error) {
1294
+ console.error("[catalog] Export failed during watch mode");
1295
+ console.error(error);
1296
+ }
1297
+ finally {
1298
+ exportInFlight = false;
1299
+ if (exportQueued) {
1300
+ exportQueued = false;
1301
+ void runRebuildAndReload();
1302
+ }
1303
+ }
1304
+ };
1305
+ const stopWatchingProject = createCatalogInputWatcher(rootDirectoryPath, projectConfig, ignoredDirectoryPaths, () => {
1306
+ if (debounceTimer) {
1307
+ clearTimeout(debounceTimer);
1308
+ }
1309
+ debounceTimer = setTimeout(() => {
1310
+ debounceTimer = null;
1311
+ void runRebuildAndReload();
1312
+ }, 250);
1313
+ });
1314
+ process.on("exit", stopWatchingProject);
1315
+ return;
1316
+ }
1317
+ if (allowedSubcommands.indexOf(parsed.subcommand) === -1) {
1318
+ console.log("Please specify a subcommand: `export` or `serve`");
1319
+ return false;
1320
+ }
1321
+ if (parsed.subcommand === "export") {
1322
+ await api.exportCatalog(rootDirectoryPath, projectConfig, datasource, {
1323
+ outDir: parsed.outDir,
1324
+ copyAssets: shouldCopyAssets(parsed),
1325
+ browserRouter,
1326
+ });
1327
+ }
1328
+ if (parsed.subcommand === "serve") {
1329
+ await api.serveCatalog(rootDirectoryPath, projectConfig, datasource, {
1330
+ outDir: parsed.outDir,
1331
+ port: parsed.port || parsed.p,
1332
+ browserRouter,
1333
+ });
1334
+ }
1335
+ },
1336
+ examples: [
1337
+ {
1338
+ command: "catalog",
1339
+ description: "generate and serve the static catalog locally",
1340
+ },
1341
+ {
1342
+ command: "catalog export",
1343
+ description: "generate static catalog with project data",
1344
+ },
1345
+ {
1346
+ command: "catalog serve",
1347
+ description: "serve the generated catalog locally",
1348
+ },
1349
+ ],
1350
+ };
1351
+ }
1352
+ //# sourceMappingURL=index.js.map