pp-flags 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/isFeatureEnabled.test.js +232 -0
- package/index.js +295 -0
- package/jest.config.js +7 -0
- package/package.json +15 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
|
3
|
+
|
|
4
|
+
// Mock posthog-js with config structure for initialization check
|
|
5
|
+
const mockPostHog = {
|
|
6
|
+
config: {
|
|
7
|
+
api_key: 'test-api-key',
|
|
8
|
+
},
|
|
9
|
+
isFeatureEnabled: jest.fn((flagName) => {
|
|
10
|
+
const mockFlags = {
|
|
11
|
+
'test-feature': true,
|
|
12
|
+
'disabled-feature': false,
|
|
13
|
+
};
|
|
14
|
+
return mockFlags[flagName] || false;
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
jest.unstable_mockModule('posthog-js', () => ({
|
|
19
|
+
default: mockPostHog,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
// Mock fetch globally
|
|
23
|
+
global.fetch = jest.fn();
|
|
24
|
+
|
|
25
|
+
// Mock window for browser environment
|
|
26
|
+
global.window = {};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Helper: set up fetch mock to return a JSON version response,
|
|
30
|
+
* import a fresh module, configure it, and wait for version to load.
|
|
31
|
+
*/
|
|
32
|
+
async function importModuleWithVersion(version) {
|
|
33
|
+
global.fetch.mockResolvedValueOnce({
|
|
34
|
+
ok: true,
|
|
35
|
+
json: async () => ({ version }),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const mod = await import(`../index.js?t=${Date.now()}-${Math.random()}`);
|
|
39
|
+
|
|
40
|
+
// Configure with a fake site API URL — this triggers version loading
|
|
41
|
+
// Awaiting configure() ensures the version is fully loaded before returning
|
|
42
|
+
await mod.configure({ siteApiBaseUrl: 'http://localhost:3003/a' });
|
|
43
|
+
|
|
44
|
+
return mod;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function importModuleWithFailedFetch() {
|
|
48
|
+
global.fetch.mockResolvedValueOnce({ ok: false });
|
|
49
|
+
|
|
50
|
+
const mod = await import(`../index.js?t=${Date.now()}-${Math.random()}`);
|
|
51
|
+
|
|
52
|
+
await mod.configure({ siteApiBaseUrl: 'http://localhost:3003/a' });
|
|
53
|
+
|
|
54
|
+
return mod;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('isFeatureEnabled', () => {
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
jest.clearAllMocks();
|
|
60
|
+
global.fetch.mockClear();
|
|
61
|
+
|
|
62
|
+
// Reset PostHog mock
|
|
63
|
+
mockPostHog.config = { api_key: 'test-api-key' };
|
|
64
|
+
mockPostHog.isFeatureEnabled.mockImplementation((flagName) => {
|
|
65
|
+
const mockFlags = {
|
|
66
|
+
'test-feature': true,
|
|
67
|
+
'disabled-feature': false,
|
|
68
|
+
};
|
|
69
|
+
return mockFlags[flagName] || false;
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
jest.clearAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('PostHog feature flags (ph:)', () => {
|
|
78
|
+
let isFeatureEnabled;
|
|
79
|
+
|
|
80
|
+
beforeEach(async () => {
|
|
81
|
+
const mod = await import(`../index.js?t=${Date.now()}-${Math.random()}`);
|
|
82
|
+
isFeatureEnabled = mod.isFeatureEnabled;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should return true for enabled PostHog feature', () => {
|
|
86
|
+
mockPostHog.isFeatureEnabled.mockReturnValue(true);
|
|
87
|
+
|
|
88
|
+
const result = isFeatureEnabled('ph:test-feature');
|
|
89
|
+
expect(result).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return false for disabled PostHog feature', () => {
|
|
93
|
+
mockPostHog.isFeatureEnabled.mockReturnValue(false);
|
|
94
|
+
|
|
95
|
+
const result = isFeatureEnabled('ph:disabled-feature');
|
|
96
|
+
expect(result).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should return false if PostHog is not initialized', async () => {
|
|
100
|
+
const originalConfig = mockPostHog.config;
|
|
101
|
+
mockPostHog.config = null;
|
|
102
|
+
|
|
103
|
+
const freshModule = await import(`../index.js?t=${Date.now()}-uninit`);
|
|
104
|
+
const freshIsFeatureEnabled = freshModule.isFeatureEnabled;
|
|
105
|
+
|
|
106
|
+
const result = freshIsFeatureEnabled('ph:test-feature');
|
|
107
|
+
expect(result).toBe(false);
|
|
108
|
+
|
|
109
|
+
mockPostHog.config = originalConfig;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should return false for empty PostHog flag name', () => {
|
|
113
|
+
const result = isFeatureEnabled('ph:');
|
|
114
|
+
expect(result).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('nf-ext feature flags', () => {
|
|
119
|
+
it('should return true if feature exists and is enabled in current version', async () => {
|
|
120
|
+
// Version 1.0.2 has "paapi" enabled in nfExtensions
|
|
121
|
+
const mod = await importModuleWithVersion('1.0.2');
|
|
122
|
+
|
|
123
|
+
const result = mod.isFeatureEnabled('nf-ext:paapi');
|
|
124
|
+
expect(result).toBe(true);
|
|
125
|
+
expect(global.fetch).toHaveBeenCalledWith('http://localhost:3003/a/nf-ext/version');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should return false if feature does not exist in current version', async () => {
|
|
129
|
+
const mod = await importModuleWithVersion('1.0.2');
|
|
130
|
+
|
|
131
|
+
const result = mod.isFeatureEnabled('nf-ext:nonexistent-feature');
|
|
132
|
+
expect(result).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should return false if version does not exist in nfExtensions', async () => {
|
|
136
|
+
const mod = await importModuleWithVersion('1.0.99');
|
|
137
|
+
|
|
138
|
+
const result = mod.isFeatureEnabled('nf-ext:paapi');
|
|
139
|
+
expect(result).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should use cached version on subsequent calls', async () => {
|
|
143
|
+
const mod = await importModuleWithVersion('1.0.2');
|
|
144
|
+
|
|
145
|
+
const result1 = mod.isFeatureEnabled('nf-ext:paapi');
|
|
146
|
+
expect(result1).toBe(true);
|
|
147
|
+
const fetchCallCount = global.fetch.mock.calls.length;
|
|
148
|
+
|
|
149
|
+
// Second call should use cache (no new fetch)
|
|
150
|
+
const result2 = mod.isFeatureEnabled('nf-ext:paapi');
|
|
151
|
+
expect(result2).toBe(true);
|
|
152
|
+
expect(global.fetch.mock.calls.length).toBe(fetchCallCount);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should return false if fetch fails', async () => {
|
|
156
|
+
const mod = await importModuleWithFailedFetch();
|
|
157
|
+
|
|
158
|
+
const result = mod.isFeatureEnabled('nf-ext:paapi');
|
|
159
|
+
expect(result).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should return false for empty feature name', async () => {
|
|
163
|
+
const mod = await importModuleWithVersion('1.0.2');
|
|
164
|
+
|
|
165
|
+
const result = mod.isFeatureEnabled('nf-ext:');
|
|
166
|
+
expect(result).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should return false if configure() was not called', async () => {
|
|
170
|
+
// Import without calling configure — no siteApiBaseUrl set
|
|
171
|
+
const mod = await import(`../index.js?t=${Date.now()}-noconfig`);
|
|
172
|
+
|
|
173
|
+
const result = mod.isFeatureEnabled('nf-ext:paapi');
|
|
174
|
+
expect(result).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should return true if specified version >= current version (nf-ext:version:XXX)', async () => {
|
|
178
|
+
const mod = await importModuleWithVersion('1.0.3');
|
|
179
|
+
|
|
180
|
+
// Current version is 1.0.3
|
|
181
|
+
expect(mod.isFeatureEnabled('nf-ext:version:1.0.3')).toBe(true); // 1.0.3 >= 1.0.3
|
|
182
|
+
expect(mod.isFeatureEnabled('nf-ext:version:1.0.4')).toBe(true); // 1.0.4 >= 1.0.3
|
|
183
|
+
expect(mod.isFeatureEnabled('nf-ext:version:1.0.2')).toBe(false); // 1.0.2 < 1.0.3
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should handle multi-part version numbers correctly', async () => {
|
|
187
|
+
const mod = await importModuleWithVersion('1.2.5');
|
|
188
|
+
|
|
189
|
+
expect(mod.isFeatureEnabled('nf-ext:version:1.2.5')).toBe(true); // 1.2.5 >= 1.2.5
|
|
190
|
+
expect(mod.isFeatureEnabled('nf-ext:version:1.2.6')).toBe(true); // 1.2.6 >= 1.2.5
|
|
191
|
+
expect(mod.isFeatureEnabled('nf-ext:version:1.3.0')).toBe(true); // 1.3.0 >= 1.2.5
|
|
192
|
+
expect(mod.isFeatureEnabled('nf-ext:version:1.2.4')).toBe(false); // 1.2.4 < 1.2.5
|
|
193
|
+
expect(mod.isFeatureEnabled('nf-ext:version:1.1.9')).toBe(false); // 1.1.9 < 1.2.5
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should return false for empty version in nf-ext:version:', async () => {
|
|
197
|
+
const mod = await importModuleWithVersion('1.0.2');
|
|
198
|
+
|
|
199
|
+
const result = mod.isFeatureEnabled('nf-ext:version:');
|
|
200
|
+
expect(result).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('Invalid inputs', () => {
|
|
205
|
+
let isFeatureEnabled;
|
|
206
|
+
|
|
207
|
+
beforeEach(async () => {
|
|
208
|
+
const mod = await import(`../index.js?t=${Date.now()}-${Math.random()}`);
|
|
209
|
+
isFeatureEnabled = mod.isFeatureEnabled;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should return false for null input', () => {
|
|
213
|
+
expect(isFeatureEnabled(null)).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should return false for undefined input', () => {
|
|
217
|
+
expect(isFeatureEnabled(undefined)).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should return false for non-string input', () => {
|
|
221
|
+
expect(isFeatureEnabled(123)).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should return false for string without colon', () => {
|
|
225
|
+
expect(isFeatureEnabled('invalid-format')).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should return false for unknown prefix', () => {
|
|
229
|
+
expect(isFeatureEnabled('unknown:value')).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
package/index.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// Import posthog - it will be initialized by the app before use
|
|
2
|
+
import posthog from 'posthog-js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cached PostHog client (checked at module initialization)
|
|
6
|
+
*/
|
|
7
|
+
let cachedPostHogClient = null;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Initialize PostHog client check (called at module load)
|
|
11
|
+
*/
|
|
12
|
+
function initializePostHogClient() {
|
|
13
|
+
if (typeof window === 'undefined') {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Check if PostHog is initialized (has config with api_key)
|
|
18
|
+
if (posthog && posthog.config && posthog.config.api_key) {
|
|
19
|
+
cachedPostHogClient = posthog;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get PostHog client (synchronous, returns cached value)
|
|
25
|
+
* @returns {object|null} - PostHog client if available and initialized, null otherwise
|
|
26
|
+
*/
|
|
27
|
+
function getPostHogClient() {
|
|
28
|
+
return cachedPostHogClient;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check PostHog initialization when module loads
|
|
32
|
+
initializePostHogClient();
|
|
33
|
+
|
|
34
|
+
// Also check periodically in case PostHog initializes later
|
|
35
|
+
if (typeof window !== 'undefined') {
|
|
36
|
+
// Check again after a short delay in case PostHog initializes asynchronously
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
initializePostHogClient();
|
|
39
|
+
}, 100);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const nfExtensions = {
|
|
43
|
+
"1.0.2": {
|
|
44
|
+
"paapi": true,
|
|
45
|
+
"amazon-scrape": false,
|
|
46
|
+
"product-boxes": true
|
|
47
|
+
},
|
|
48
|
+
"1.0.1": {
|
|
49
|
+
"amazon-scrape": true
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Configured site API base URL (set via configure())
|
|
55
|
+
*/
|
|
56
|
+
let siteApiBaseUrl = null;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Cached current extension version (loaded at module initialization)
|
|
60
|
+
*/
|
|
61
|
+
let cachedExtensionVersion = null;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Promise that resolves when the extension version is loaded
|
|
65
|
+
*/
|
|
66
|
+
let versionLoadPromise = null;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the version load promise (for testing purposes)
|
|
70
|
+
* @returns {Promise<string|null>|null} - The version load promise or null
|
|
71
|
+
*/
|
|
72
|
+
export function getVersionLoadPromise() {
|
|
73
|
+
return versionLoadPromise;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Compare two semantic versions
|
|
78
|
+
* @param {string} version1 - First version to compare
|
|
79
|
+
* @param {string} version2 - Second version to compare
|
|
80
|
+
* @returns {number} - Returns 1 if version1 > version2, -1 if version1 < version2, 0 if equal
|
|
81
|
+
*/
|
|
82
|
+
function compareVersions(version1, version2) {
|
|
83
|
+
const v1Parts = version1.split('.').map(Number);
|
|
84
|
+
const v2Parts = version2.split('.').map(Number);
|
|
85
|
+
|
|
86
|
+
// Pad shorter version with zeros
|
|
87
|
+
const maxLength = Math.max(v1Parts.length, v2Parts.length);
|
|
88
|
+
while (v1Parts.length < maxLength) v1Parts.push(0);
|
|
89
|
+
while (v2Parts.length < maxLength) v2Parts.push(0);
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < maxLength; i++) {
|
|
92
|
+
if (v1Parts[i] > v2Parts[i]) return 1;
|
|
93
|
+
if (v1Parts[i] < v2Parts[i]) return -1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Load the current extension version from the site API (async)
|
|
101
|
+
* The site API proxies the request to Google's update server to avoid CORS issues.
|
|
102
|
+
* @returns {Promise<string|null>} - The version string or null if not found
|
|
103
|
+
*/
|
|
104
|
+
async function loadExtensionVersion() {
|
|
105
|
+
try {
|
|
106
|
+
if (!siteApiBaseUrl) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const response = await fetch(`${siteApiBaseUrl}/nf-ext/version`);
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const data = await response.json();
|
|
116
|
+
return data.version || null;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get the current extension version (synchronous, returns cached value)
|
|
124
|
+
* @returns {string|null} - The version string or null if not loaded yet
|
|
125
|
+
*/
|
|
126
|
+
function getCurrentExtensionVersion() {
|
|
127
|
+
return cachedExtensionVersion;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Initialize extension version loading (called after configure())
|
|
132
|
+
* Fetches the extension version from the site API.
|
|
133
|
+
*/
|
|
134
|
+
function initializeExtensionVersion() {
|
|
135
|
+
if (typeof window === 'undefined') {
|
|
136
|
+
// Server-side, skip loading
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Only fetch from site API if configured
|
|
141
|
+
if (!siteApiBaseUrl) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (versionLoadPromise) {
|
|
146
|
+
// Already loading or loaded
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
versionLoadPromise = loadExtensionVersion().then(version => {
|
|
151
|
+
cachedExtensionVersion = version;
|
|
152
|
+
return version;
|
|
153
|
+
}).catch(() => {
|
|
154
|
+
cachedExtensionVersion = null;
|
|
155
|
+
return null;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Configure the flags package with required settings.
|
|
161
|
+
* Must be called (and awaited) before nf-ext flags will work.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```js
|
|
165
|
+
* import { configure } from 'pp-flags';
|
|
166
|
+
* await configure({ siteApiBaseUrl: 'http://localhost:3003/a' });
|
|
167
|
+
* // Now isFeatureEnabled('nf-ext:...') will have the version loaded
|
|
168
|
+
* ```
|
|
169
|
+
*
|
|
170
|
+
* @param {Object} options - Configuration options
|
|
171
|
+
* @param {string} options.siteApiBaseUrl - The base URL for the site API (e.g., 'http://localhost:3003/a')
|
|
172
|
+
* @returns {Promise<string|null>} - Resolves with the loaded version, or null if loading failed
|
|
173
|
+
*/
|
|
174
|
+
export async function configure({ siteApiBaseUrl: url }) {
|
|
175
|
+
if (url) {
|
|
176
|
+
siteApiBaseUrl = url;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Trigger version loading now that we have the URL
|
|
180
|
+
initializeExtensionVersion();
|
|
181
|
+
|
|
182
|
+
// Wait for version to finish loading before returning
|
|
183
|
+
if (versionLoadPromise) {
|
|
184
|
+
return versionLoadPromise;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Check if a featured version is enabled by comparing with XML version or PostHog feature flag
|
|
193
|
+
*
|
|
194
|
+
* This function uses the global PostHog instance (initialized via posthog.init() in your app).
|
|
195
|
+
* The XML version and PostHog client are pre-loaded when the module is imported.
|
|
196
|
+
*
|
|
197
|
+
* @example Using in React components:
|
|
198
|
+
* ```jsx
|
|
199
|
+
* import { isFeatureEnabled } from 'pp-flags';
|
|
200
|
+
*
|
|
201
|
+
* function MyComponent() {
|
|
202
|
+
* // Synchronous check - no async/await needed
|
|
203
|
+
* const isEnabled = isFeatureEnabled('ph:my-feature');
|
|
204
|
+
* const isPaapiEnabled = isFeatureEnabled('nf-ext:paapi');
|
|
205
|
+
* const isVersionOk = isFeatureEnabled('nf-ext:version:1.0.3');
|
|
206
|
+
* // ...
|
|
207
|
+
* }
|
|
208
|
+
* ```
|
|
209
|
+
*
|
|
210
|
+
* @param {string} flagString - Flag string in format "nf-ext:feature-name" (e.g., "nf-ext:paapi"), "nf-ext:version:1.0.3", or "ph:feature-flag-name"
|
|
211
|
+
* @returns {boolean} - True if enabled, false otherwise. Returns false if version/PostHog not loaded yet.
|
|
212
|
+
*/
|
|
213
|
+
export function isFeatureEnabled(flagString) {
|
|
214
|
+
try {
|
|
215
|
+
// Extract version from parameter string
|
|
216
|
+
if (!flagString || typeof flagString !== 'string') {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check if this is a PostHog feature flag (starts with "ph:")
|
|
221
|
+
if (flagString.startsWith('ph:')) {
|
|
222
|
+
const featureFlagName = flagString.substring(3); // Get everything after "ph:"
|
|
223
|
+
if (!featureFlagName) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Get PostHog client (cached at module load)
|
|
228
|
+
const posthogClient = getPostHogClient();
|
|
229
|
+
|
|
230
|
+
// Check if PostHog is available and has the method
|
|
231
|
+
if (posthogClient && typeof posthogClient.isFeatureEnabled === 'function') {
|
|
232
|
+
try {
|
|
233
|
+
return posthogClient.isFeatureEnabled(featureFlagName) || false;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
// Return false if PostHog check fails
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Return false if PostHog is not available or not initialized
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Handle nf-ext feature check (e.g., "nf-ext:paapi" or "nf-ext:version:1.0.3")
|
|
244
|
+
if (flagString.startsWith('nf-ext:')) {
|
|
245
|
+
const afterPrefix = flagString.substring(7); // Get everything after "nf-ext:"
|
|
246
|
+
if (!afterPrefix) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Get current version from cache (loaded at module initialization)
|
|
251
|
+
const currentVersion = getCurrentExtensionVersion();
|
|
252
|
+
if (!currentVersion) {
|
|
253
|
+
// Version not loaded yet, return false
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Check if this is a version comparison (nf-ext:version:XXX)
|
|
258
|
+
if (afterPrefix.startsWith('version:')) {
|
|
259
|
+
const specifiedVersion = afterPrefix.substring(8); // Get everything after "version:"
|
|
260
|
+
if (!specifiedVersion) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Compare versions: return true if specifiedVersion >= currentVersion
|
|
265
|
+
// This means: return true if the specified version is at least the current version
|
|
266
|
+
const comparison = compareVersions(specifiedVersion, currentVersion);
|
|
267
|
+
return comparison >= 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Otherwise, treat it as a feature name (e.g., "nf-ext:paapi")
|
|
271
|
+
const featureName = afterPrefix;
|
|
272
|
+
|
|
273
|
+
// Look up features for this version in nfExtensions
|
|
274
|
+
const versionFeatures = nfExtensions[currentVersion];
|
|
275
|
+
if (!versionFeatures) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check if the feature exists and is enabled
|
|
280
|
+
return !!versionFeatures[featureName];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Handle other prefixes (legacy support or future extensions)
|
|
284
|
+
const parts = flagString.split(':');
|
|
285
|
+
if (parts.length < 2) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// For other formats, return false (or implement custom logic)
|
|
290
|
+
return false;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
// Return false on any error
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pp-flags",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Minimal feature flags package for frontend and backend",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
|
+
},
|
|
10
|
+
"author": "",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"posthog-js": "^1.227.0"
|
|
14
|
+
}
|
|
15
|
+
}
|