@geejay/use-feature-flags 1.0.12 → 1.0.17
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/dist/index.cjs +131 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -2
- package/dist/index.js +126 -2
- package/dist/index.js.map +1 -0
- package/package.json +21 -15
- package/dist/store.d.ts +0 -17
- package/dist/store.js +0 -13
- package/dist/useFeatureFlags.d.ts +0 -8
- package/dist/useFeatureFlags.js +0 -96
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jotai = require('jotai');
|
|
5
|
+
var supabaseJs = require('@supabase/supabase-js');
|
|
6
|
+
|
|
7
|
+
// src/useFeatureFlags.ts
|
|
8
|
+
var featureFlagsAtom = jotai.atom({
|
|
9
|
+
flags: [],
|
|
10
|
+
loading: true
|
|
11
|
+
});
|
|
12
|
+
var apiKey = null;
|
|
13
|
+
function setApiKey(key) {
|
|
14
|
+
apiKey = key;
|
|
15
|
+
}
|
|
16
|
+
function getApiKey() {
|
|
17
|
+
return apiKey;
|
|
18
|
+
}
|
|
19
|
+
var DEFAULT_SUPABASE_URL = "https://khppgsehvvlukzfdqbuo.supabase.co";
|
|
20
|
+
var DEFAULT_SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtocHBnc2VodnZsdWt6ZmRxYnVvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTI2ODU1MzQsImV4cCI6MjA2ODI2MTUzNH0.8Z4VY4HFMm95UgO21c-DnDkbLPN_0mbDBZJPExaghDk";
|
|
21
|
+
var _supabase = null;
|
|
22
|
+
function getSupabase() {
|
|
23
|
+
if (_supabase) return _supabase;
|
|
24
|
+
_supabase = supabaseJs.createClient(DEFAULT_SUPABASE_URL, DEFAULT_SUPABASE_KEY, {
|
|
25
|
+
auth: {
|
|
26
|
+
persistSession: false,
|
|
27
|
+
autoRefreshToken: false,
|
|
28
|
+
storageKey: "feature-flags"
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return _supabase;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/useFeatureFlags.ts
|
|
35
|
+
var EDGE_FN_URL = `${DEFAULT_SUPABASE_URL}/functions/v1/get-feature-flags`;
|
|
36
|
+
var initialized = false;
|
|
37
|
+
function useFeatureFlags(passedKey, environment = window.location.hostname || "localhost") {
|
|
38
|
+
const sanitizedEnvironment = react.useMemo(
|
|
39
|
+
() => environment.replace(/:\d+$/, ""),
|
|
40
|
+
[environment]
|
|
41
|
+
);
|
|
42
|
+
const [state, setState] = jotai.useAtom(featureFlagsAtom);
|
|
43
|
+
const [envId, setEnvId] = react.useState(null);
|
|
44
|
+
const debounceTimeout = react.useRef(null);
|
|
45
|
+
const cleanupRef = react.useRef(null);
|
|
46
|
+
const apiKey2 = passedKey;
|
|
47
|
+
react.useEffect(() => {
|
|
48
|
+
console.log(
|
|
49
|
+
"[use-feature-flags] initializing",
|
|
50
|
+
`environment: ${sanitizedEnvironment}`,
|
|
51
|
+
`apiKey provided: ${Boolean(apiKey2)}`
|
|
52
|
+
);
|
|
53
|
+
}, [sanitizedEnvironment, apiKey2]);
|
|
54
|
+
const supabase = getSupabase();
|
|
55
|
+
const fetchFlags = async () => {
|
|
56
|
+
setState((prev) => ({ ...prev, loading: true }));
|
|
57
|
+
console.log("[use-feature-flags] fetching flags for", sanitizedEnvironment);
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(EDGE_FN_URL, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: {
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
...apiKey2 ? { "api-key": apiKey2 } : {}
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify({ environment: sanitizedEnvironment })
|
|
66
|
+
});
|
|
67
|
+
const json = await res.json();
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
console.warn("Edge function error:", (json == null ? void 0 : json.error) || res.statusText);
|
|
70
|
+
setState({ flags: [], loading: false });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const flags = json.flags || [];
|
|
74
|
+
setState({ flags, loading: false });
|
|
75
|
+
console.log("[use-feature-flags] fetched flags", flags);
|
|
76
|
+
if (flags.length > 0 && flags[0].environment_id) {
|
|
77
|
+
setEnvId(flags[0].environment_id);
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error("Error fetching flags:", err.message);
|
|
81
|
+
setState({ flags: [], loading: false });
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
react.useEffect(() => {
|
|
85
|
+
if (!apiKey2 || initialized) return;
|
|
86
|
+
initialized = true;
|
|
87
|
+
fetchFlags();
|
|
88
|
+
return () => {
|
|
89
|
+
if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
|
|
90
|
+
if (cleanupRef.current) cleanupRef.current();
|
|
91
|
+
};
|
|
92
|
+
}, [sanitizedEnvironment, apiKey2]);
|
|
93
|
+
react.useEffect(() => {
|
|
94
|
+
if (!envId) return;
|
|
95
|
+
const channel = supabase.channel(`flags-${sanitizedEnvironment}`).on(
|
|
96
|
+
"postgres_changes",
|
|
97
|
+
{
|
|
98
|
+
event: "*",
|
|
99
|
+
schema: "public",
|
|
100
|
+
table: "feature_flags",
|
|
101
|
+
filter: `environment_id=eq.${envId}`
|
|
102
|
+
},
|
|
103
|
+
() => {
|
|
104
|
+
if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
|
|
105
|
+
debounceTimeout.current = setTimeout(fetchFlags, 300);
|
|
106
|
+
}
|
|
107
|
+
).subscribe();
|
|
108
|
+
if (cleanupRef.current) cleanupRef.current();
|
|
109
|
+
cleanupRef.current = () => supabase.removeChannel(channel);
|
|
110
|
+
return () => {
|
|
111
|
+
if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
|
|
112
|
+
supabase.removeChannel(channel);
|
|
113
|
+
};
|
|
114
|
+
}, [envId]);
|
|
115
|
+
return {
|
|
116
|
+
isActive: (key) => {
|
|
117
|
+
const active = state.flags.some((f) => f.key === key && f.enabled === true);
|
|
118
|
+
console.log("[use-feature-flags] isActive", key, active);
|
|
119
|
+
return active;
|
|
120
|
+
},
|
|
121
|
+
flags: state.flags,
|
|
122
|
+
loading: state.loading
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
exports.featureFlagsAtom = featureFlagsAtom;
|
|
127
|
+
exports.getApiKey = getApiKey;
|
|
128
|
+
exports.setApiKey = setApiKey;
|
|
129
|
+
exports.useFeatureFlags = useFeatureFlags;
|
|
130
|
+
//# sourceMappingURL=index.cjs.map
|
|
131
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store.ts","../src/supabaseClient.ts","../src/useFeatureFlags.ts"],"names":["atom","createClient","useMemo","useAtom","useState","useRef","apiKey","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["import { atom } from 'jotai';\n\nexport type FeatureFlag = {\n id: string;\n key: string;\n enabled: boolean;\n environment: string;\n};\n\nexport const featureFlagsAtom = atom<{ flags: FeatureFlag[]; loading: boolean }>({\n flags: [],\n loading: true,\n});\n\n// store.ts\nlet apiKey: string | null = null;\n\nexport function setApiKey(key: string) {\n apiKey = key;\n}\n\nexport function getApiKey() {\n return apiKey;\n}\n","import { createClient } from '@supabase/supabase-js';\n\nexport const DEFAULT_SUPABASE_URL = 'https://khppgsehvvlukzfdqbuo.supabase.co';\nexport const DEFAULT_SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtocHBnc2VodnZsdWt6ZmRxYnVvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTI2ODU1MzQsImV4cCI6MjA2ODI2MTUzNH0.8Z4VY4HFMm95UgO21c-DnDkbLPN_0mbDBZJPExaghDk';\n\nlet _supabase: ReturnType<typeof createClient> | null = null;\n\nexport function getSupabase() {\n if (_supabase) return _supabase;\n _supabase = createClient(DEFAULT_SUPABASE_URL, DEFAULT_SUPABASE_KEY, {\n auth: {\n persistSession: false,\n autoRefreshToken: false,\n storageKey: 'feature-flags',\n },\n });\n return _supabase;\n}\n","\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { FeatureFlag, featureFlagsAtom } from './store';\nimport { useAtom } from 'jotai';\nimport { getSupabase, DEFAULT_SUPABASE_URL } from './supabaseClient';\n\nconst EDGE_FN_URL = `${DEFAULT_SUPABASE_URL}/functions/v1/get-feature-flags`;\n\n\n\n\ntype FlagState = {\n flags: FeatureFlag[];\n loading: boolean;\n};\n\nlet initialized = false;\n\nexport function useFeatureFlags(\n passedKey?: string,\n environment = window.location.hostname || 'localhost'\n) {\n const sanitizedEnvironment = useMemo(\n () => environment.replace(/:\\d+$/, ''),\n [environment]\n );\n const [state, setState] = useAtom(featureFlagsAtom);\n const [envId, setEnvId] = useState<number | null>(null);\n const debounceTimeout = useRef<NodeJS.Timeout | null>(null);\n const cleanupRef = useRef<(() => void) | null>(null);\n\n const apiKey = passedKey;\n\n useEffect(() => {\n console.log(\n '[use-feature-flags] initializing',\n `environment: ${sanitizedEnvironment}`,\n `apiKey provided: ${Boolean(apiKey)}`\n );\n }, [sanitizedEnvironment, apiKey]);\n const supabase = getSupabase();\n\n const fetchFlags = async () => {\n setState((prev) => ({ ...prev, loading: true }));\n\n console.log('[use-feature-flags] fetching flags for', sanitizedEnvironment);\n\n try {\n const res = await fetch(EDGE_FN_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(apiKey ? { 'api-key': apiKey } : {}),\n },\n body: JSON.stringify({ environment: sanitizedEnvironment }),\n });\n\n const json = await res.json();\n if (!res.ok) {\n console.warn('Edge function error:', json?.error || res.statusText);\n setState({ flags: [], loading: false });\n return;\n }\n\n const flags = json.flags || [];\n setState({ flags, loading: false });\n console.log('[use-feature-flags] fetched flags', flags);\n\n // Store environment_id from first flag (assumes all have same env)\n if (flags.length > 0 && flags[0].environment_id) {\n setEnvId(flags[0].environment_id);\n }\n } catch (err: any) {\n console.error('Error fetching flags:', err.message);\n setState({ flags: [], loading: false });\n }\n };\n\n useEffect(() => {\n if (!apiKey || initialized) return;\n initialized = true;\n\n fetchFlags();\n\n return () => {\n if (debounceTimeout.current) clearTimeout(debounceTimeout.current);\n if (cleanupRef.current) cleanupRef.current();\n };\n }, [sanitizedEnvironment, apiKey]);\n\n // Subscribe to flag changes for the correct environment\n useEffect(() => {\n if (!envId) return;\n\n const channel = supabase\n .channel(`flags-${sanitizedEnvironment}`)\n .on(\n 'postgres_changes',\n {\n event: '*',\n schema: 'public',\n table: 'feature_flags',\n filter: `environment_id=eq.${envId}`,\n },\n () => {\n if (debounceTimeout.current) clearTimeout(debounceTimeout.current);\n debounceTimeout.current = setTimeout(fetchFlags, 300);\n }\n )\n .subscribe();\n\n if (cleanupRef.current) cleanupRef.current();\n cleanupRef.current = () => supabase.removeChannel(channel);\n\n return () => {\n if (debounceTimeout.current) clearTimeout(debounceTimeout.current);\n supabase.removeChannel(channel);\n };\n }, [envId]);\n\n return {\n isActive: (key: string) => {\n const active = state.flags.some((f) => f.key === key && f.enabled === true);\n console.log('[use-feature-flags] isActive', key, active);\n return active;\n },\n flags: state.flags,\n loading: state.loading,\n };\n}\n\n\n\n\n\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as jotai from 'jotai';
|
|
2
|
+
|
|
3
|
+
type FeatureFlag = {
|
|
4
|
+
id: string;
|
|
5
|
+
key: string;
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
environment: string;
|
|
8
|
+
};
|
|
9
|
+
declare const featureFlagsAtom: jotai.PrimitiveAtom<{
|
|
10
|
+
flags: FeatureFlag[];
|
|
11
|
+
loading: boolean;
|
|
12
|
+
}> & {
|
|
13
|
+
init: {
|
|
14
|
+
flags: FeatureFlag[];
|
|
15
|
+
loading: boolean;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
declare function setApiKey(key: string): void;
|
|
19
|
+
declare function getApiKey(): string | null;
|
|
20
|
+
|
|
21
|
+
declare function useFeatureFlags(passedKey?: string, environment?: string): {
|
|
22
|
+
isActive: (key: string) => boolean;
|
|
23
|
+
flags: FeatureFlag[];
|
|
24
|
+
loading: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export { type FeatureFlag, featureFlagsAtom, getApiKey, setApiKey, useFeatureFlags };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import * as jotai from 'jotai';
|
|
2
|
+
|
|
3
|
+
type FeatureFlag = {
|
|
4
|
+
id: string;
|
|
5
|
+
key: string;
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
environment: string;
|
|
8
|
+
};
|
|
9
|
+
declare const featureFlagsAtom: jotai.PrimitiveAtom<{
|
|
10
|
+
flags: FeatureFlag[];
|
|
11
|
+
loading: boolean;
|
|
12
|
+
}> & {
|
|
13
|
+
init: {
|
|
14
|
+
flags: FeatureFlag[];
|
|
15
|
+
loading: boolean;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
declare function setApiKey(key: string): void;
|
|
19
|
+
declare function getApiKey(): string | null;
|
|
20
|
+
|
|
21
|
+
declare function useFeatureFlags(passedKey?: string, environment?: string): {
|
|
22
|
+
isActive: (key: string) => boolean;
|
|
23
|
+
flags: FeatureFlag[];
|
|
24
|
+
loading: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export { type FeatureFlag, featureFlagsAtom, getApiKey, setApiKey, useFeatureFlags };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,126 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { useMemo, useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { atom, useAtom } from 'jotai';
|
|
3
|
+
import { createClient } from '@supabase/supabase-js';
|
|
4
|
+
|
|
5
|
+
// src/useFeatureFlags.ts
|
|
6
|
+
var featureFlagsAtom = atom({
|
|
7
|
+
flags: [],
|
|
8
|
+
loading: true
|
|
9
|
+
});
|
|
10
|
+
var apiKey = null;
|
|
11
|
+
function setApiKey(key) {
|
|
12
|
+
apiKey = key;
|
|
13
|
+
}
|
|
14
|
+
function getApiKey() {
|
|
15
|
+
return apiKey;
|
|
16
|
+
}
|
|
17
|
+
var DEFAULT_SUPABASE_URL = "https://khppgsehvvlukzfdqbuo.supabase.co";
|
|
18
|
+
var DEFAULT_SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtocHBnc2VodnZsdWt6ZmRxYnVvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTI2ODU1MzQsImV4cCI6MjA2ODI2MTUzNH0.8Z4VY4HFMm95UgO21c-DnDkbLPN_0mbDBZJPExaghDk";
|
|
19
|
+
var _supabase = null;
|
|
20
|
+
function getSupabase() {
|
|
21
|
+
if (_supabase) return _supabase;
|
|
22
|
+
_supabase = createClient(DEFAULT_SUPABASE_URL, DEFAULT_SUPABASE_KEY, {
|
|
23
|
+
auth: {
|
|
24
|
+
persistSession: false,
|
|
25
|
+
autoRefreshToken: false,
|
|
26
|
+
storageKey: "feature-flags"
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return _supabase;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/useFeatureFlags.ts
|
|
33
|
+
var EDGE_FN_URL = `${DEFAULT_SUPABASE_URL}/functions/v1/get-feature-flags`;
|
|
34
|
+
var initialized = false;
|
|
35
|
+
function useFeatureFlags(passedKey, environment = window.location.hostname || "localhost") {
|
|
36
|
+
const sanitizedEnvironment = useMemo(
|
|
37
|
+
() => environment.replace(/:\d+$/, ""),
|
|
38
|
+
[environment]
|
|
39
|
+
);
|
|
40
|
+
const [state, setState] = useAtom(featureFlagsAtom);
|
|
41
|
+
const [envId, setEnvId] = useState(null);
|
|
42
|
+
const debounceTimeout = useRef(null);
|
|
43
|
+
const cleanupRef = useRef(null);
|
|
44
|
+
const apiKey2 = passedKey;
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
console.log(
|
|
47
|
+
"[use-feature-flags] initializing",
|
|
48
|
+
`environment: ${sanitizedEnvironment}`,
|
|
49
|
+
`apiKey provided: ${Boolean(apiKey2)}`
|
|
50
|
+
);
|
|
51
|
+
}, [sanitizedEnvironment, apiKey2]);
|
|
52
|
+
const supabase = getSupabase();
|
|
53
|
+
const fetchFlags = async () => {
|
|
54
|
+
setState((prev) => ({ ...prev, loading: true }));
|
|
55
|
+
console.log("[use-feature-flags] fetching flags for", sanitizedEnvironment);
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(EDGE_FN_URL, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
...apiKey2 ? { "api-key": apiKey2 } : {}
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify({ environment: sanitizedEnvironment })
|
|
64
|
+
});
|
|
65
|
+
const json = await res.json();
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
console.warn("Edge function error:", (json == null ? void 0 : json.error) || res.statusText);
|
|
68
|
+
setState({ flags: [], loading: false });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const flags = json.flags || [];
|
|
72
|
+
setState({ flags, loading: false });
|
|
73
|
+
console.log("[use-feature-flags] fetched flags", flags);
|
|
74
|
+
if (flags.length > 0 && flags[0].environment_id) {
|
|
75
|
+
setEnvId(flags[0].environment_id);
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error("Error fetching flags:", err.message);
|
|
79
|
+
setState({ flags: [], loading: false });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!apiKey2 || initialized) return;
|
|
84
|
+
initialized = true;
|
|
85
|
+
fetchFlags();
|
|
86
|
+
return () => {
|
|
87
|
+
if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
|
|
88
|
+
if (cleanupRef.current) cleanupRef.current();
|
|
89
|
+
};
|
|
90
|
+
}, [sanitizedEnvironment, apiKey2]);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!envId) return;
|
|
93
|
+
const channel = supabase.channel(`flags-${sanitizedEnvironment}`).on(
|
|
94
|
+
"postgres_changes",
|
|
95
|
+
{
|
|
96
|
+
event: "*",
|
|
97
|
+
schema: "public",
|
|
98
|
+
table: "feature_flags",
|
|
99
|
+
filter: `environment_id=eq.${envId}`
|
|
100
|
+
},
|
|
101
|
+
() => {
|
|
102
|
+
if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
|
|
103
|
+
debounceTimeout.current = setTimeout(fetchFlags, 300);
|
|
104
|
+
}
|
|
105
|
+
).subscribe();
|
|
106
|
+
if (cleanupRef.current) cleanupRef.current();
|
|
107
|
+
cleanupRef.current = () => supabase.removeChannel(channel);
|
|
108
|
+
return () => {
|
|
109
|
+
if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
|
|
110
|
+
supabase.removeChannel(channel);
|
|
111
|
+
};
|
|
112
|
+
}, [envId]);
|
|
113
|
+
return {
|
|
114
|
+
isActive: (key) => {
|
|
115
|
+
const active = state.flags.some((f) => f.key === key && f.enabled === true);
|
|
116
|
+
console.log("[use-feature-flags] isActive", key, active);
|
|
117
|
+
return active;
|
|
118
|
+
},
|
|
119
|
+
flags: state.flags,
|
|
120
|
+
loading: state.loading
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { featureFlagsAtom, getApiKey, setApiKey, useFeatureFlags };
|
|
125
|
+
//# sourceMappingURL=index.js.map
|
|
126
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store.ts","../src/supabaseClient.ts","../src/useFeatureFlags.ts"],"names":["apiKey"],"mappings":"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","file":"index.js","sourcesContent":["import { atom } from 'jotai';\n\nexport type FeatureFlag = {\n id: string;\n key: string;\n enabled: boolean;\n environment: string;\n};\n\nexport const featureFlagsAtom = atom<{ flags: FeatureFlag[]; loading: boolean }>({\n flags: [],\n loading: true,\n});\n\n// store.ts\nlet apiKey: string | null = null;\n\nexport function setApiKey(key: string) {\n apiKey = key;\n}\n\nexport function getApiKey() {\n return apiKey;\n}\n","import { createClient } from '@supabase/supabase-js';\n\nexport const DEFAULT_SUPABASE_URL = 'https://khppgsehvvlukzfdqbuo.supabase.co';\nexport const DEFAULT_SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtocHBnc2VodnZsdWt6ZmRxYnVvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTI2ODU1MzQsImV4cCI6MjA2ODI2MTUzNH0.8Z4VY4HFMm95UgO21c-DnDkbLPN_0mbDBZJPExaghDk';\n\nlet _supabase: ReturnType<typeof createClient> | null = null;\n\nexport function getSupabase() {\n if (_supabase) return _supabase;\n _supabase = createClient(DEFAULT_SUPABASE_URL, DEFAULT_SUPABASE_KEY, {\n auth: {\n persistSession: false,\n autoRefreshToken: false,\n storageKey: 'feature-flags',\n },\n });\n return _supabase;\n}\n","\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { FeatureFlag, featureFlagsAtom } from './store';\nimport { useAtom } from 'jotai';\nimport { getSupabase, DEFAULT_SUPABASE_URL } from './supabaseClient';\n\nconst EDGE_FN_URL = `${DEFAULT_SUPABASE_URL}/functions/v1/get-feature-flags`;\n\n\n\n\ntype FlagState = {\n flags: FeatureFlag[];\n loading: boolean;\n};\n\nlet initialized = false;\n\nexport function useFeatureFlags(\n passedKey?: string,\n environment = window.location.hostname || 'localhost'\n) {\n const sanitizedEnvironment = useMemo(\n () => environment.replace(/:\\d+$/, ''),\n [environment]\n );\n const [state, setState] = useAtom(featureFlagsAtom);\n const [envId, setEnvId] = useState<number | null>(null);\n const debounceTimeout = useRef<NodeJS.Timeout | null>(null);\n const cleanupRef = useRef<(() => void) | null>(null);\n\n const apiKey = passedKey;\n\n useEffect(() => {\n console.log(\n '[use-feature-flags] initializing',\n `environment: ${sanitizedEnvironment}`,\n `apiKey provided: ${Boolean(apiKey)}`\n );\n }, [sanitizedEnvironment, apiKey]);\n const supabase = getSupabase();\n\n const fetchFlags = async () => {\n setState((prev) => ({ ...prev, loading: true }));\n\n console.log('[use-feature-flags] fetching flags for', sanitizedEnvironment);\n\n try {\n const res = await fetch(EDGE_FN_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(apiKey ? { 'api-key': apiKey } : {}),\n },\n body: JSON.stringify({ environment: sanitizedEnvironment }),\n });\n\n const json = await res.json();\n if (!res.ok) {\n console.warn('Edge function error:', json?.error || res.statusText);\n setState({ flags: [], loading: false });\n return;\n }\n\n const flags = json.flags || [];\n setState({ flags, loading: false });\n console.log('[use-feature-flags] fetched flags', flags);\n\n // Store environment_id from first flag (assumes all have same env)\n if (flags.length > 0 && flags[0].environment_id) {\n setEnvId(flags[0].environment_id);\n }\n } catch (err: any) {\n console.error('Error fetching flags:', err.message);\n setState({ flags: [], loading: false });\n }\n };\n\n useEffect(() => {\n if (!apiKey || initialized) return;\n initialized = true;\n\n fetchFlags();\n\n return () => {\n if (debounceTimeout.current) clearTimeout(debounceTimeout.current);\n if (cleanupRef.current) cleanupRef.current();\n };\n }, [sanitizedEnvironment, apiKey]);\n\n // Subscribe to flag changes for the correct environment\n useEffect(() => {\n if (!envId) return;\n\n const channel = supabase\n .channel(`flags-${sanitizedEnvironment}`)\n .on(\n 'postgres_changes',\n {\n event: '*',\n schema: 'public',\n table: 'feature_flags',\n filter: `environment_id=eq.${envId}`,\n },\n () => {\n if (debounceTimeout.current) clearTimeout(debounceTimeout.current);\n debounceTimeout.current = setTimeout(fetchFlags, 300);\n }\n )\n .subscribe();\n\n if (cleanupRef.current) cleanupRef.current();\n cleanupRef.current = () => supabase.removeChannel(channel);\n\n return () => {\n if (debounceTimeout.current) clearTimeout(debounceTimeout.current);\n supabase.removeChannel(channel);\n };\n }, [envId]);\n\n return {\n isActive: (key: string) => {\n const active = state.flags.some((f) => f.key === key && f.enabled === true);\n console.log('[use-feature-flags] isActive', key, active);\n return active;\n },\n flags: state.flags,\n loading: state.loading,\n };\n}\n\n\n\n\n\n"]}
|
package/package.json
CHANGED
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geejay/use-feature-flags",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.17",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"import": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"require": "./dist/index.cjs"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"module": "dist/index.js",
|
|
6
13
|
"types": "dist/index.d.ts",
|
|
7
14
|
"files": [
|
|
8
15
|
"dist",
|
|
9
16
|
"README.md"
|
|
10
17
|
],
|
|
11
18
|
"scripts": {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"keywords": ["feature flags", "supabase", "react", "hooks"],
|
|
16
|
-
"repository": {
|
|
17
|
-
"type": "git",
|
|
18
|
-
"url": "https://github.com/your-org/use-feature-flags.git"
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"prepack": "npm run build"
|
|
19
21
|
},
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"dependencies": {
|
|
23
|
-
"@supabase/supabase-js": "^2.52.0",
|
|
24
|
-
"jotai": "^2.12.5"
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
25
24
|
},
|
|
26
25
|
"peerDependencies": {
|
|
27
26
|
"react": ">=16.8"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@supabase/supabase-js": "^2.54.0",
|
|
30
|
+
"@types/react": "^18.3.23",
|
|
31
|
+
"jotai": "^2.13.0",
|
|
32
|
+
"tsup": "^8.5.0",
|
|
33
|
+
"typescript": "^5.9.2"
|
|
28
34
|
}
|
|
29
35
|
}
|
package/dist/store.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export type FeatureFlag = {
|
|
2
|
-
id: string;
|
|
3
|
-
key: string;
|
|
4
|
-
enabled: boolean;
|
|
5
|
-
environment: string;
|
|
6
|
-
};
|
|
7
|
-
export declare const featureFlagsAtom: import("jotai").PrimitiveAtom<{
|
|
8
|
-
flags: FeatureFlag[];
|
|
9
|
-
loading: boolean;
|
|
10
|
-
}> & {
|
|
11
|
-
init: {
|
|
12
|
-
flags: FeatureFlag[];
|
|
13
|
-
loading: boolean;
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
export declare function setApiKey(key: string): void;
|
|
17
|
-
export declare function getApiKey(): string;
|
package/dist/store.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { FeatureFlag } from './store';
|
|
2
|
-
export declare const DEFAULT_SUPABASE_URL = "https://khppgsehvvlukzfdqbuo.supabase.co";
|
|
3
|
-
export declare const DEFAULT_SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtocHBnc2VodnZsdWt6ZmRxYnVvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTI2ODU1MzQsImV4cCI6MjA2ODI2MTUzNH0.8Z4VY4HFMm95UgO21c-DnDkbLPN_0mbDBZJPExaghDk";
|
|
4
|
-
export declare function useFeatureFlags(passedKey?: string, environment?: string): {
|
|
5
|
-
isActive: (key: string) => boolean;
|
|
6
|
-
flags: FeatureFlag[];
|
|
7
|
-
loading: boolean;
|
|
8
|
-
};
|
package/dist/useFeatureFlags.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import { createClient } from '@supabase/supabase-js';
|
|
3
|
-
import { featureFlagsAtom } from './store';
|
|
4
|
-
import { useAtom } from 'jotai';
|
|
5
|
-
export const DEFAULT_SUPABASE_URL = 'https://khppgsehvvlukzfdqbuo.supabase.co';
|
|
6
|
-
export const DEFAULT_SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtocHBnc2VodnZsdWt6ZmRxYnVvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTI2ODU1MzQsImV4cCI6MjA2ODI2MTUzNH0.8Z4VY4HFMm95UgO21c-DnDkbLPN_0mbDBZJPExaghDk';
|
|
7
|
-
const EDGE_FN_URL = `${DEFAULT_SUPABASE_URL}/functions/v1/get-feature-flags`;
|
|
8
|
-
let initialized = false;
|
|
9
|
-
export function useFeatureFlags(passedKey, environment = window.location.hostname || 'localhost') {
|
|
10
|
-
const sanitizedEnvironment = useMemo(() => environment.replace(/:\d+$/, ''), [environment]);
|
|
11
|
-
const [state, setState] = useAtom(featureFlagsAtom);
|
|
12
|
-
const [envId, setEnvId] = useState(null);
|
|
13
|
-
const debounceTimeout = useRef(null);
|
|
14
|
-
const cleanupRef = useRef();
|
|
15
|
-
const apiKey = passedKey;
|
|
16
|
-
const supabase = createClient(DEFAULT_SUPABASE_URL, DEFAULT_SUPABASE_KEY, {
|
|
17
|
-
global: {
|
|
18
|
-
headers: {
|
|
19
|
-
Authorization: `Bearer ${DEFAULT_SUPABASE_KEY}`, // 👈 dynamic access token
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
const fetchFlags = async () => {
|
|
24
|
-
setState((prev) => ({ ...prev, loading: true }));
|
|
25
|
-
try {
|
|
26
|
-
const res = await fetch(EDGE_FN_URL, {
|
|
27
|
-
method: 'POST',
|
|
28
|
-
headers: {
|
|
29
|
-
'Content-Type': 'application/json',
|
|
30
|
-
'api-key': apiKey,
|
|
31
|
-
},
|
|
32
|
-
body: JSON.stringify({ environment: sanitizedEnvironment }),
|
|
33
|
-
});
|
|
34
|
-
const json = await res.json();
|
|
35
|
-
if (!res.ok) {
|
|
36
|
-
console.warn('Edge function error:', (json === null || json === void 0 ? void 0 : json.error) || res.statusText);
|
|
37
|
-
setState({ flags: [], loading: false });
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const flags = json.flags || [];
|
|
41
|
-
setState({ flags, loading: false });
|
|
42
|
-
console.log(`Flags are cool man ${JSON.stringify(flags)}`);
|
|
43
|
-
// Store environment_id from first flag (assumes all have same env)
|
|
44
|
-
if (flags.length > 0 && flags[0].environment_id) {
|
|
45
|
-
setEnvId(flags[0].environment_id);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
catch (err) {
|
|
49
|
-
console.error('Error fetching flags:', err.message);
|
|
50
|
-
setState({ flags: [], loading: false });
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
if (!apiKey || initialized)
|
|
55
|
-
return;
|
|
56
|
-
initialized = true;
|
|
57
|
-
fetchFlags();
|
|
58
|
-
return () => {
|
|
59
|
-
if (debounceTimeout.current)
|
|
60
|
-
clearTimeout(debounceTimeout.current);
|
|
61
|
-
if (cleanupRef.current)
|
|
62
|
-
cleanupRef.current();
|
|
63
|
-
};
|
|
64
|
-
}, [sanitizedEnvironment, apiKey]);
|
|
65
|
-
// Subscribe to flag changes for the correct environment
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
if (!envId)
|
|
68
|
-
return;
|
|
69
|
-
const channel = supabase
|
|
70
|
-
.channel(`flags-${sanitizedEnvironment}`)
|
|
71
|
-
.on('postgres_changes', {
|
|
72
|
-
event: '*',
|
|
73
|
-
schema: 'public',
|
|
74
|
-
table: 'feature_flags',
|
|
75
|
-
filter: `environment_id=eq.${envId}`,
|
|
76
|
-
}, () => {
|
|
77
|
-
if (debounceTimeout.current)
|
|
78
|
-
clearTimeout(debounceTimeout.current);
|
|
79
|
-
debounceTimeout.current = setTimeout(fetchFlags, 300);
|
|
80
|
-
})
|
|
81
|
-
.subscribe();
|
|
82
|
-
if (cleanupRef.current)
|
|
83
|
-
cleanupRef.current();
|
|
84
|
-
cleanupRef.current = () => supabase.removeChannel(channel);
|
|
85
|
-
return () => {
|
|
86
|
-
if (debounceTimeout.current)
|
|
87
|
-
clearTimeout(debounceTimeout.current);
|
|
88
|
-
supabase.removeChannel(channel);
|
|
89
|
-
};
|
|
90
|
-
}, [envId]);
|
|
91
|
-
return {
|
|
92
|
-
isActive: (key) => state.flags.some((f) => f.key === key && f.enabled === true),
|
|
93
|
-
flags: state.flags,
|
|
94
|
-
loading: state.loading,
|
|
95
|
-
};
|
|
96
|
-
}
|