@mergedapp/feature-flags 0.1.3

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 (114) hide show
  1. package/README.md +651 -0
  2. package/dist/cjs/cli/audit.js +117 -0
  3. package/dist/cjs/cli/cleanup.js +105 -0
  4. package/dist/cjs/cli/config-loader.js +102 -0
  5. package/dist/cjs/cli/generate.js +194 -0
  6. package/dist/cjs/cli/parse-args.js +18 -0
  7. package/dist/cjs/cli.js +46 -0
  8. package/dist/cjs/client.js +505 -0
  9. package/dist/cjs/errors.js +24 -0
  10. package/dist/cjs/index.js +13 -0
  11. package/dist/cjs/jwt.js +85 -0
  12. package/dist/cjs/nestjs/bindings.js +36 -0
  13. package/dist/cjs/nestjs/constants.js +7 -0
  14. package/dist/cjs/nestjs/context.js +28 -0
  15. package/dist/cjs/nestjs/decorators.js +50 -0
  16. package/dist/cjs/nestjs/errors.js +25 -0
  17. package/dist/cjs/nestjs/evaluator.js +87 -0
  18. package/dist/cjs/nestjs/guard.js +67 -0
  19. package/dist/cjs/nestjs/interceptor.js +56 -0
  20. package/dist/cjs/nestjs/module.js +70 -0
  21. package/dist/cjs/nestjs/service.js +54 -0
  22. package/dist/cjs/nestjs/types.js +2 -0
  23. package/dist/cjs/nestjs.js +26 -0
  24. package/dist/cjs/openfeature/context.js +166 -0
  25. package/dist/cjs/openfeature/hooks.js +31 -0
  26. package/dist/cjs/openfeature/server-provider.js +107 -0
  27. package/dist/cjs/openfeature/server.js +13 -0
  28. package/dist/cjs/openfeature/shared.js +83 -0
  29. package/dist/cjs/openfeature/web-provider.js +156 -0
  30. package/dist/cjs/openfeature/web.js +13 -0
  31. package/dist/cjs/package.json +3 -0
  32. package/dist/cjs/persistence.js +249 -0
  33. package/dist/cjs/react/hooks.js +86 -0
  34. package/dist/cjs/react/provider.js +106 -0
  35. package/dist/cjs/react.js +7 -0
  36. package/dist/cjs/remote-evaluator.js +162 -0
  37. package/dist/cjs/types.js +2 -0
  38. package/dist/cli/audit.d.ts +3 -0
  39. package/dist/cli/audit.js +114 -0
  40. package/dist/cli/cleanup.d.ts +3 -0
  41. package/dist/cli/cleanup.js +102 -0
  42. package/dist/cli/config-loader.d.ts +26 -0
  43. package/dist/cli/config-loader.js +66 -0
  44. package/dist/cli/generate.d.ts +3 -0
  45. package/dist/cli/generate.js +191 -0
  46. package/dist/cli/parse-args.d.ts +1 -0
  47. package/dist/cli/parse-args.js +15 -0
  48. package/dist/cli.d.ts +1 -0
  49. package/dist/cli.js +45 -0
  50. package/dist/client.d.ts +67 -0
  51. package/dist/client.js +501 -0
  52. package/dist/errors.d.ts +15 -0
  53. package/dist/errors.js +18 -0
  54. package/dist/index.cjs +1 -0
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.js +3 -0
  57. package/dist/jwt.d.ts +20 -0
  58. package/dist/jwt.js +78 -0
  59. package/dist/nestjs/bindings.d.ts +5 -0
  60. package/dist/nestjs/bindings.js +33 -0
  61. package/dist/nestjs/constants.d.ts +4 -0
  62. package/dist/nestjs/constants.js +4 -0
  63. package/dist/nestjs/context.d.ts +12 -0
  64. package/dist/nestjs/context.js +24 -0
  65. package/dist/nestjs/decorators.d.ts +4 -0
  66. package/dist/nestjs/decorators.js +45 -0
  67. package/dist/nestjs/errors.d.ts +12 -0
  68. package/dist/nestjs/errors.js +20 -0
  69. package/dist/nestjs/evaluator.d.ts +17 -0
  70. package/dist/nestjs/evaluator.js +83 -0
  71. package/dist/nestjs/guard.d.ts +19 -0
  72. package/dist/nestjs/guard.js +63 -0
  73. package/dist/nestjs/interceptor.d.ts +10 -0
  74. package/dist/nestjs/interceptor.js +53 -0
  75. package/dist/nestjs/module.d.ts +6 -0
  76. package/dist/nestjs/module.js +67 -0
  77. package/dist/nestjs/service.d.ts +30 -0
  78. package/dist/nestjs/service.js +51 -0
  79. package/dist/nestjs/types.d.ts +100 -0
  80. package/dist/nestjs/types.js +1 -0
  81. package/dist/nestjs.cjs +1 -0
  82. package/dist/nestjs.d.ts +10 -0
  83. package/dist/nestjs.js +9 -0
  84. package/dist/openfeature/context.d.ts +10 -0
  85. package/dist/openfeature/context.js +160 -0
  86. package/dist/openfeature/hooks.d.ts +6 -0
  87. package/dist/openfeature/hooks.js +27 -0
  88. package/dist/openfeature/server-provider.d.ts +20 -0
  89. package/dist/openfeature/server-provider.js +102 -0
  90. package/dist/openfeature/server.cjs +1 -0
  91. package/dist/openfeature/server.d.ts +3 -0
  92. package/dist/openfeature/server.js +3 -0
  93. package/dist/openfeature/shared.d.ts +37 -0
  94. package/dist/openfeature/shared.js +74 -0
  95. package/dist/openfeature/web-provider.d.ts +27 -0
  96. package/dist/openfeature/web-provider.js +151 -0
  97. package/dist/openfeature/web.cjs +1 -0
  98. package/dist/openfeature/web.d.ts +3 -0
  99. package/dist/openfeature/web.js +3 -0
  100. package/dist/persistence.d.ts +39 -0
  101. package/dist/persistence.js +203 -0
  102. package/dist/react/hooks.d.ts +52 -0
  103. package/dist/react/hooks.js +78 -0
  104. package/dist/react/provider.d.ts +71 -0
  105. package/dist/react/provider.js +99 -0
  106. package/dist/react.cjs +1 -0
  107. package/dist/react.d.ts +2 -0
  108. package/dist/react.js +2 -0
  109. package/dist/remote-evaluator.d.ts +28 -0
  110. package/dist/remote-evaluator.js +158 -0
  111. package/dist/types.d.ts +56 -0
  112. package/dist/types.js +1 -0
  113. package/featureflags.config.schema.json +38 -0
  114. package/package.json +107 -0
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.audit = audit;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_path_1 = require("node:path");
6
+ const config_loader_js_1 = require("./config-loader.js");
7
+ const CODE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
8
+ async function collectFiles(dir) {
9
+ const files = [];
10
+ const entries = await (0, promises_1.readdir)(dir, { withFileTypes: true });
11
+ for (const entry of entries) {
12
+ const fullPath = (0, node_path_1.join)(dir, entry.name);
13
+ if (entry.isDirectory()) {
14
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") {
15
+ continue;
16
+ }
17
+ files.push(...(await collectFiles(fullPath)));
18
+ }
19
+ else if (CODE_EXTENSIONS.has((0, node_path_1.extname)(entry.name))) {
20
+ files.push(fullPath);
21
+ }
22
+ }
23
+ return files;
24
+ }
25
+ async function fetchDefinitions(params) {
26
+ const url = new URL("/api/feature-flags/definitions", params.apiUrl);
27
+ url.searchParams.set("organizationId", params.organizationId);
28
+ const response = await fetch(url.toString(), {
29
+ headers: { Authorization: `Bearer ${params.clientKey}` },
30
+ });
31
+ if (!response.ok) {
32
+ throw new Error(`Failed to fetch definitions: ${response.status} ${response.statusText}`);
33
+ }
34
+ const body = (await response.json());
35
+ return body.definitions;
36
+ }
37
+ async function audit(args) {
38
+ const dir = (0, node_path_1.resolve)(args.dir ?? "./src");
39
+ const loadedConfig = await config_loader_js_1.FeatureFlagCliConfigLoader.load({ configPath: args.config });
40
+ const config = loadedConfig.config;
41
+ const apiUrl = args["api-url"] ?? config.apiUrl ?? process.env.FEATURE_FLAG_API_URL;
42
+ const clientKey = args["client-key"] ?? config.clientKey ?? process.env.FEATURE_FLAG_CLIENT_KEY;
43
+ const organizationId = args["organization-id"] ?? config.organizationId ?? process.env.FEATURE_FLAG_ORGANIZATION_ID;
44
+ if (!apiUrl || !clientKey || !organizationId) {
45
+ throw new Error("Missing --api-url, --client-key, --organization-id, or a discoverable featureflags config.");
46
+ }
47
+ console.log(`Fetching flag definitions from ${apiUrl}...`);
48
+ const definitions = await fetchDefinitions({ apiUrl, clientKey, organizationId });
49
+ console.log(`Scanning ${dir} for flag references...`);
50
+ const files = await collectFiles(dir);
51
+ const flagIdSet = new Set(definitions.map((d) => d.id));
52
+ const flagCodeKeySet = new Set(definitions.map((d) => d.codeKey));
53
+ const archivedIds = new Set(definitions.filter((d) => d.isArchived).map((d) => d.id));
54
+ const referencedIds = new Set();
55
+ const referencedCodeKeys = new Set();
56
+ for (const file of files) {
57
+ const content = await (0, promises_1.readFile)(file, "utf-8");
58
+ for (const id of flagIdSet) {
59
+ if (content.includes(id)) {
60
+ referencedIds.add(id);
61
+ }
62
+ }
63
+ for (const codeKey of flagCodeKeySet) {
64
+ if (content.includes(`"${codeKey}"`) || content.includes(`'${codeKey}'`)) {
65
+ referencedCodeKeys.add(codeKey);
66
+ }
67
+ }
68
+ }
69
+ const codeKeyToDefinition = new Map(definitions.map((d) => [d.codeKey, d]));
70
+ const allReferenced = new Set();
71
+ for (const id of referencedIds)
72
+ allReferenced.add(id);
73
+ for (const codeKey of referencedCodeKeys) {
74
+ const def = codeKeyToDefinition.get(codeKey);
75
+ if (def)
76
+ allReferenced.add(def.id);
77
+ }
78
+ const used = [];
79
+ const unused = [];
80
+ const archivedButReferenced = [];
81
+ for (const def of definitions) {
82
+ if (allReferenced.has(def.id)) {
83
+ if (archivedIds.has(def.id)) {
84
+ archivedButReferenced.push(def);
85
+ }
86
+ else {
87
+ used.push(def);
88
+ }
89
+ }
90
+ else if (!def.isArchived) {
91
+ unused.push(def);
92
+ }
93
+ }
94
+ console.log("\n--- Feature Flag Audit Report ---\n");
95
+ if (used.length > 0) {
96
+ console.log(`Active and referenced (${used.length}):`);
97
+ for (const d of used) {
98
+ console.log(` [OK] ${d.codeKey} (${d.id})`);
99
+ }
100
+ }
101
+ if (unused.length > 0) {
102
+ console.log(`\nDefined in API but not referenced in code (${unused.length}):`);
103
+ for (const d of unused) {
104
+ console.log(` [UNUSED] ${d.codeKey} (${d.id})`);
105
+ }
106
+ }
107
+ if (archivedButReferenced.length > 0) {
108
+ console.log(`\nArchived but still referenced in code (${archivedButReferenced.length}):`);
109
+ for (const d of archivedButReferenced) {
110
+ console.log(` [STALE] ${d.codeKey} (${d.id}) — archived, needs cleanup`);
111
+ }
112
+ }
113
+ if (unused.length === 0 && archivedButReferenced.length === 0) {
114
+ console.log("\nAll flags are clean.");
115
+ }
116
+ console.log(`\nScanned ${files.length} files in ${dir}.`);
117
+ }
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cleanup = cleanup;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_path_1 = require("node:path");
6
+ const CODE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
7
+ async function collectFiles(dir) {
8
+ const files = [];
9
+ const entries = await (0, promises_1.readdir)(dir, { withFileTypes: true });
10
+ for (const entry of entries) {
11
+ const fullPath = (0, node_path_1.join)(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") {
14
+ continue;
15
+ }
16
+ files.push(...(await collectFiles(fullPath)));
17
+ }
18
+ else if (CODE_EXTENSIONS.has((0, node_path_1.extname)(entry.name))) {
19
+ files.push(fullPath);
20
+ }
21
+ }
22
+ return files;
23
+ }
24
+ async function fetchDefinitions(params) {
25
+ const url = new URL("/api/feature-flags/definitions", params.apiUrl);
26
+ url.searchParams.set("organizationId", params.organizationId);
27
+ const response = await fetch(url.toString(), {
28
+ headers: { Authorization: `Bearer ${params.clientKey}` },
29
+ });
30
+ if (!response.ok) {
31
+ throw new Error(`Failed to fetch definitions: ${response.status} ${response.statusText}`);
32
+ }
33
+ const body = (await response.json());
34
+ return body.definitions;
35
+ }
36
+ async function cleanup(args) {
37
+ const dir = (0, node_path_1.resolve)(args.dir ?? "./src");
38
+ const dryRun = "dry-run" in args;
39
+ const apiUrl = args["api-url"] ?? process.env.FEATURE_FLAG_API_URL;
40
+ const clientKey = args["client-key"] ?? process.env.FEATURE_FLAG_CLIENT_KEY;
41
+ const organizationId = args["organization-id"];
42
+ if (!apiUrl || !clientKey || !organizationId) {
43
+ throw new Error("Missing --api-url, --client-key, or --organization-id.");
44
+ }
45
+ console.log(`Fetching flag definitions from ${apiUrl}...`);
46
+ const definitions = await fetchDefinitions({ apiUrl, clientKey, organizationId });
47
+ const archivedFlags = definitions.filter((d) => d.isArchived && d.type === "BOOLEAN");
48
+ if (archivedFlags.length === 0) {
49
+ console.log("No archived boolean flags to clean up.");
50
+ return;
51
+ }
52
+ console.log(`Found ${archivedFlags.length} archived boolean flags to scan for.`);
53
+ console.log(`Scanning ${dir}...`);
54
+ const files = await collectFiles(dir);
55
+ let totalReplacements = 0;
56
+ for (const filePath of files) {
57
+ let content = await (0, promises_1.readFile)(filePath, "utf-8");
58
+ let modified = false;
59
+ for (const flag of archivedFlags) {
60
+ const result = replaceArchivedFlagChecks({ content, dryRun, filePath, flag });
61
+ content = result.content;
62
+ modified ||= result.replacements > 0;
63
+ totalReplacements += result.replacements;
64
+ }
65
+ if (modified && !dryRun) {
66
+ await (0, promises_1.writeFile)(filePath, content, "utf-8");
67
+ console.log(` Updated: ${filePath}`);
68
+ }
69
+ }
70
+ if (totalReplacements === 0) {
71
+ console.log("No archived flag references found in code.");
72
+ }
73
+ else if (dryRun) {
74
+ console.log(`\n${totalReplacements} replacement(s) would be made. Re-run without --dry-run to apply.`);
75
+ }
76
+ else {
77
+ console.log(`\n${totalReplacements} replacement(s) applied. Run 'merged-ff generate' to update the generated file.`);
78
+ }
79
+ }
80
+ function escapeRegex(str) {
81
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
82
+ }
83
+ function createIsEnabledCallPattern(value) {
84
+ const target = escapeRegex(value);
85
+ return new RegExp(String.raw `(?:\b(?:this|[A-Za-z_$][\w$]*)(?:(?:\?\.|\.)[A-Za-z_$][\w$]*)*(?:\?\.|\.))?isEnabled\(["']${target}["']\)`, "g");
86
+ }
87
+ function replaceArchivedFlagChecks(params) {
88
+ const patterns = [
89
+ createIsEnabledCallPattern(params.flag.id),
90
+ createIsEnabledCallPattern(params.flag.codeKey),
91
+ ];
92
+ let nextContent = params.content;
93
+ let replacements = 0;
94
+ for (const pattern of patterns) {
95
+ nextContent = nextContent.replace(pattern, (match) => {
96
+ replacements++;
97
+ if (params.dryRun) {
98
+ console.log(` [DRY RUN] ${params.filePath}: ${match} → false`);
99
+ return match;
100
+ }
101
+ return "false";
102
+ });
103
+ }
104
+ return { content: nextContent, replacements };
105
+ }
@@ -0,0 +1,102 @@
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.FeatureFlagCliConfigLoader = void 0;
37
+ const node_fs_1 = require("node:fs");
38
+ const promises_1 = require("node:fs/promises");
39
+ const node_path_1 = require("node:path");
40
+ const node_url_1 = require("node:url");
41
+ const CONFIG_FILE_NAMES = [
42
+ "featureflags.config.json",
43
+ "featureflags.config.js",
44
+ "featureflags.config.mjs",
45
+ "featureflags.config.cjs",
46
+ ];
47
+ async function load(params) {
48
+ const cwd = params.cwd ?? process.cwd();
49
+ if (params.configPath) {
50
+ const explicitPath = (0, node_path_1.isAbsolute)(params.configPath) ? params.configPath : (0, node_path_1.resolve)(cwd, params.configPath);
51
+ return loadFile({ configPath: explicitPath });
52
+ }
53
+ const discoveredConfigPath = await findNearestConfigPath({ cwd });
54
+ if (!discoveredConfigPath) {
55
+ return { config: {}, configPath: null };
56
+ }
57
+ return loadFile({ configPath: discoveredConfigPath });
58
+ }
59
+ function resolveConfigRelativePath(params) {
60
+ if (!params.value) {
61
+ return undefined;
62
+ }
63
+ if ((0, node_path_1.isAbsolute)(params.value) || !params.configPath) {
64
+ return params.value;
65
+ }
66
+ return (0, node_path_1.resolve)((0, node_path_1.dirname)(params.configPath), params.value);
67
+ }
68
+ async function findNearestConfigPath(params) {
69
+ let currentDirectory = (0, node_path_1.resolve)(params.cwd);
70
+ while (true) {
71
+ for (const fileName of CONFIG_FILE_NAMES) {
72
+ const candidatePath = (0, node_path_1.resolve)(currentDirectory, fileName);
73
+ if ((0, node_fs_1.existsSync)(candidatePath)) {
74
+ return candidatePath;
75
+ }
76
+ }
77
+ const parentDirectory = (0, node_path_1.dirname)(currentDirectory);
78
+ if (parentDirectory === currentDirectory) {
79
+ return null;
80
+ }
81
+ currentDirectory = parentDirectory;
82
+ }
83
+ }
84
+ async function loadFile(params) {
85
+ if (params.configPath.endsWith(".json")) {
86
+ const content = await (0, promises_1.readFile)(params.configPath, "utf-8");
87
+ return {
88
+ config: JSON.parse(content),
89
+ configPath: params.configPath,
90
+ };
91
+ }
92
+ const imported = await Promise.resolve(`${(0, node_url_1.pathToFileURL)(params.configPath).href}`).then(s => __importStar(require(s)));
93
+ const config = (imported.default ?? imported);
94
+ if (!config || typeof config !== "object") {
95
+ return { config: {}, configPath: params.configPath };
96
+ }
97
+ return { config, configPath: params.configPath };
98
+ }
99
+ exports.FeatureFlagCliConfigLoader = {
100
+ load,
101
+ resolveConfigRelativePath,
102
+ };
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generate = generate;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_path_1 = require("node:path");
6
+ const config_loader_js_1 = require("./config-loader.js");
7
+ function assertValidCodeKey(codeKey) {
8
+ if (!/^[a-z][A-Za-z0-9]*$/.test(codeKey)) {
9
+ throw new Error(`Invalid feature flag codeKey "${codeKey}". Code keys must be lowerCamelCase alphanumeric identifiers.`);
10
+ }
11
+ }
12
+ function flagTypeToTsType(flagType) {
13
+ switch (flagType) {
14
+ case "BOOLEAN":
15
+ return "boolean";
16
+ case "STRING":
17
+ return "string";
18
+ case "NUMBER":
19
+ return "number";
20
+ case "JSON":
21
+ return "Record<string, unknown>";
22
+ case "ARRAY":
23
+ return "unknown[]";
24
+ default:
25
+ return "unknown";
26
+ }
27
+ }
28
+ function escapeJsDoc(str) {
29
+ return str.replace(/\*\//g, "*\\/").replace(/\n/g, " ");
30
+ }
31
+ async function fetchDefinitions(params) {
32
+ const url = new URL("/api/feature-flags/definitions", params.apiUrl);
33
+ url.searchParams.set("organizationId", params.organizationId);
34
+ if (params.teamId) {
35
+ url.searchParams.set("teamId", params.teamId);
36
+ }
37
+ const response = await fetch(url.toString(), {
38
+ headers: { Authorization: `Bearer ${params.clientKey}` },
39
+ });
40
+ if (!response.ok) {
41
+ throw new Error(`Failed to fetch definitions: ${response.status} ${response.statusText}`);
42
+ }
43
+ const body = (await response.json());
44
+ return body.definitions;
45
+ }
46
+ function generateCode(definitions) {
47
+ const activeFlags = definitions.filter((d) => !d.isArchived);
48
+ if (activeFlags.length === 0) {
49
+ return `// Auto-generated by @mergedapp/feature-flags. Do not edit.
50
+ // Generated at: ${new Date().toISOString()}
51
+ // No active feature flags found.
52
+
53
+ import { MergedFeatureFlags } from "@mergedapp/feature-flags"
54
+ import { createTypedHooks } from "@mergedapp/feature-flags/react"
55
+ import type { FeatureFlagClientConfig } from "@mergedapp/feature-flags"
56
+
57
+ export const FLAGS = {} as const
58
+
59
+ export interface FlagValues {}
60
+
61
+ export type FlagName = keyof typeof FLAGS
62
+
63
+ export function createClient(config: FeatureFlagClientConfig) {
64
+ return new MergedFeatureFlags<FlagValues>({ ...config, flagIds: FLAGS })
65
+ }
66
+
67
+ export const { FeatureFlagProvider, FeatureFlag, useFeatureFlag, useFeatureFlags, useFeatureFlagClient, useFeatureFlagStatus } =
68
+ createTypedHooks<FlagValues>()
69
+ `;
70
+ }
71
+ const flagEntries = activeFlags.map((flag) => {
72
+ assertValidCodeKey(flag.codeKey);
73
+ const doc = flag.description ? ` /** ${escapeJsDoc(flag.description)} */\n` : "";
74
+ return { key: flag.codeKey, id: flag.id, type: flag.type, doc };
75
+ });
76
+ const flagsConst = flagEntries.map((e) => `${e.doc} ${e.key}: "${e.id}",`).join("\n");
77
+ const flagValuesEntries = flagEntries.map((e) => ` ${e.key}: ${flagTypeToTsType(e.type)}`).join("\n");
78
+ return `// Auto-generated by @mergedapp/feature-flags. Do not edit.
79
+ // Generated at: ${new Date().toISOString()}
80
+
81
+ import { MergedFeatureFlags } from "@mergedapp/feature-flags"
82
+ import { createTypedHooks } from "@mergedapp/feature-flags/react"
83
+ import type { FeatureFlagClientConfig } from "@mergedapp/feature-flags"
84
+
85
+ /** Stable flag ID mapping. Names are for readability; IDs are used at runtime. */
86
+ export const FLAGS = {
87
+ ${flagsConst}
88
+ } as const
89
+
90
+ /** Type-safe flag value types inferred from flag definitions. */
91
+ export interface FlagValues {
92
+ ${flagValuesEntries}
93
+ }
94
+
95
+ export type FlagName = keyof typeof FLAGS
96
+
97
+ /** Create a type-safe client bound to this flag registry. */
98
+ export function createClient(config: FeatureFlagClientConfig) {
99
+ return new MergedFeatureFlags<FlagValues>({ ...config, flagIds: FLAGS })
100
+ }
101
+
102
+ /** Pre-typed React hooks, provider, and JSX wrapper bound to this flag registry. */
103
+ export const { FeatureFlagProvider, FeatureFlag, useFeatureFlag, useFeatureFlags, useFeatureFlagClient, useFeatureFlagStatus } =
104
+ createTypedHooks<FlagValues>()
105
+ `;
106
+ }
107
+ function generateNestCode(params) {
108
+ return `// Auto-generated by @mergedapp/feature-flags. Do not edit.
109
+ // Generated at: ${new Date().toISOString()}
110
+
111
+ import {
112
+ createTypedNestjsBindings,
113
+ FeatureFlagContext,
114
+ FeatureFlagsRequestContextStore,
115
+ FeatureFlagsService,
116
+ type FeatureFlagContextFactory,
117
+ type FeatureFlagEvaluationDetails,
118
+ } from "@mergedapp/feature-flags/nestjs"
119
+ import { FLAGS } from "${params.generatedModulePath}"
120
+ import type { FlagValues } from "${params.generatedModulePath}"
121
+
122
+ export {
123
+ FeatureFlagContext,
124
+ FeatureFlagsRequestContextStore,
125
+ FeatureFlagsService,
126
+ }
127
+ export type {
128
+ FeatureFlagContextFactory,
129
+ FeatureFlagEvaluationDetails,
130
+ }
131
+
132
+ export const { FeatureFlagsModule, RequireFeatureFlag, FeatureFlagGate } =
133
+ createTypedNestjsBindings<FlagValues>({ flagIds: FLAGS })
134
+ `;
135
+ }
136
+ function resolveNestOutputPath(outputPath) {
137
+ const extension = (0, node_path_1.extname)(outputPath);
138
+ if (extension === ".ts") {
139
+ return outputPath.slice(0, -extension.length) + ".nest.ts";
140
+ }
141
+ return `${outputPath}.nest.ts`;
142
+ }
143
+ function resolveRelativeGeneratedModulePath(params) {
144
+ const mainBaseName = (0, node_path_1.basename)(params.mainOutputPath, (0, node_path_1.extname)(params.mainOutputPath));
145
+ const nestDirectory = (0, node_path_1.dirname)(params.nestOutputPath);
146
+ const mainDirectory = (0, node_path_1.dirname)(params.mainOutputPath);
147
+ if (nestDirectory === mainDirectory) {
148
+ return `./${mainBaseName}`;
149
+ }
150
+ const relativeDirectory = (0, node_path_1.relative)(nestDirectory, mainDirectory);
151
+ const normalizedDirectory = relativeDirectory.split("\\").join("/");
152
+ return `${normalizedDirectory.startsWith(".") ? normalizedDirectory : `./${normalizedDirectory}`}/${mainBaseName}`;
153
+ }
154
+ async function generate(args) {
155
+ const loadedConfig = await config_loader_js_1.FeatureFlagCliConfigLoader.load({ configPath: args.config });
156
+ const config = loadedConfig.config;
157
+ const apiUrl = args["api-url"] ?? config.apiUrl ?? process.env.FEATURE_FLAG_API_URL;
158
+ const clientKey = args["client-key"] ?? config.clientKey ?? process.env.FEATURE_FLAG_CLIENT_KEY;
159
+ const outputPath = args.output ??
160
+ config_loader_js_1.FeatureFlagCliConfigLoader.resolveConfigRelativePath({
161
+ value: config.outputPath,
162
+ configPath: loadedConfig.configPath,
163
+ }) ??
164
+ "./src/generated/feature-flags.ts";
165
+ const teamId = args["team-id"] ?? config.teamId;
166
+ const organizationId = args["organization-id"] ?? config.organizationId ?? process.env.FEATURE_FLAG_ORGANIZATION_ID;
167
+ if (!apiUrl) {
168
+ throw new Error("Missing --api-url or FEATURE_FLAG_API_URL env var.");
169
+ }
170
+ if (!clientKey) {
171
+ throw new Error("Missing --client-key or FEATURE_FLAG_CLIENT_KEY env var.");
172
+ }
173
+ if (!organizationId) {
174
+ throw new Error("Missing --organization-id, FEATURE_FLAG_ORGANIZATION_ID, or config.organizationId.");
175
+ }
176
+ console.log(`Fetching flag definitions from ${apiUrl}...`);
177
+ const definitions = await fetchDefinitions({ apiUrl, clientKey, organizationId, teamId });
178
+ console.log(`Found ${definitions.length} flag definitions.`);
179
+ const code = generateCode(definitions);
180
+ const resolvedPath = (0, node_path_1.resolve)(outputPath);
181
+ const resolvedNestPath = resolveNestOutputPath(resolvedPath);
182
+ const nestCode = generateNestCode({
183
+ generatedModulePath: resolveRelativeGeneratedModulePath({
184
+ mainOutputPath: resolvedPath,
185
+ nestOutputPath: resolvedNestPath,
186
+ }),
187
+ });
188
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(resolvedPath), { recursive: true });
189
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(resolvedNestPath), { recursive: true });
190
+ await (0, promises_1.writeFile)(resolvedPath, code, "utf-8");
191
+ await (0, promises_1.writeFile)(resolvedNestPath, nestCode, "utf-8");
192
+ console.log(`Generated typed flags at ${resolvedPath}`);
193
+ console.log(`Generated typed NestJS bindings at ${resolvedNestPath}`);
194
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseArgs = parseArgs;
4
+ function parseArgs(args) {
5
+ const result = {};
6
+ for (const arg of args) {
7
+ const keyValueMatch = arg.match(/^--([^=]+)=(.+)$/);
8
+ if (keyValueMatch) {
9
+ result[keyValueMatch[1]] = keyValueMatch[2];
10
+ continue;
11
+ }
12
+ const flagMatch = arg.match(/^--(.+)$/);
13
+ if (flagMatch) {
14
+ result[flagMatch[1]] = "true";
15
+ }
16
+ }
17
+ return result;
18
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const generate_js_1 = require("./cli/generate.js");
4
+ const audit_js_1 = require("./cli/audit.js");
5
+ const cleanup_js_1 = require("./cli/cleanup.js");
6
+ const parse_args_js_1 = require("./cli/parse-args.js");
7
+ const [command, ...args] = process.argv.slice(2);
8
+ async function main() {
9
+ const parsed = (0, parse_args_js_1.parseArgs)(args);
10
+ switch (command) {
11
+ case "generate":
12
+ await (0, generate_js_1.generate)(parsed);
13
+ break;
14
+ case "audit":
15
+ await (0, audit_js_1.audit)(parsed);
16
+ break;
17
+ case "cleanup":
18
+ await (0, cleanup_js_1.cleanup)(parsed);
19
+ break;
20
+ default:
21
+ console.log(`@mergedapp/feature-flags CLI
22
+
23
+ Usage:
24
+ merged-ff generate Generate typed SDK from flag definitions
25
+ merged-ff audit Scan codebase for unused/stale flags
26
+ merged-ff cleanup Remove stale flag references (interactive)
27
+
28
+ Options:
29
+ --api-url=<url> API base URL
30
+ --client-key=<key> API key (or set FEATURE_FLAG_CLIENT_KEY env var)
31
+ --output=<path> Output path for generated file (default: ./src/generated/feature-flags.ts)
32
+ --dir=<path> Directory to scan (audit/cleanup, default: ./src)
33
+ --config=<path> Config file path for generate/audit (default: auto-discover featureflags.config.{json,js,mjs,cjs} from cwd upward)
34
+ --dry-run Preview changes without applying (cleanup)
35
+ --organization-id=<id> Organization ID (required)
36
+ --team-id=<id> Team ID scope`);
37
+ if (command) {
38
+ console.error(`\nUnknown command: ${command}`);
39
+ process.exit(1);
40
+ }
41
+ }
42
+ }
43
+ main().catch((error) => {
44
+ console.error(error instanceof Error ? error.message : error);
45
+ process.exit(1);
46
+ });