@xyd-js/host 0.0.0-build
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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/app/debug-null.tsx +3 -0
- package/app/docPaths.ts +69 -0
- package/app/entry.client.tsx +58 -0
- package/app/entry.server.tsx +68 -0
- package/app/pathRoutes.ts +86 -0
- package/app/public.ts +43 -0
- package/app/raw.ts +31 -0
- package/app/robots.ts +18 -0
- package/app/root.tsx +579 -0
- package/app/routes.ts +35 -0
- package/app/scripts/abtesting.ts +279 -0
- package/app/scripts/bannerHeight.ts +14 -0
- package/app/scripts/colorSchemeScript.ts +21 -0
- package/app/scripts/growthbook.js +3574 -0
- package/app/scripts/launchdarkly.js +2 -0
- package/app/scripts/openfeature.growthbook.js +692 -0
- package/app/scripts/openfeature.js +1715 -0
- package/app/scripts/openfeature.launchdarkly.js +877 -0
- package/app/scripts/testFeatureFlag.ts +39 -0
- package/app/sitemap.ts +40 -0
- package/app/types/raw.d.ts +4 -0
- package/auto-imports.d.ts +10 -0
- package/package.json +41 -0
- package/plugins/README.md +1 -0
- package/postcss.config.cjs +5 -0
- package/react-router.config.ts +94 -0
- package/src/auto-imports.d.ts +29 -0
- package/tsconfig.json +28 -0
- package/types.d.ts +8 -0
- package/vite.config.ts +8 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// TODO: ts + use bunled
|
|
2
|
+
// TODO: custom user logic about context
|
|
3
|
+
(function () {
|
|
4
|
+
const abTestingSettings = window.__xydAbTestingSettings;
|
|
5
|
+
const abTestingProviders = abTestingSettings?.providers || {}
|
|
6
|
+
|
|
7
|
+
// Context storage configuration
|
|
8
|
+
const contextMaxAge = abTestingSettings?.contextMaxAge || 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
|
|
9
|
+
const contextStorageKey = abTestingSettings?.contextStorageKey || "__xydABContextKey";
|
|
10
|
+
|
|
11
|
+
// Utility functions for base64 encoding/decoding
|
|
12
|
+
function isBase64(str) {
|
|
13
|
+
try {
|
|
14
|
+
return btoa(atob(str)) === str;
|
|
15
|
+
} catch (err) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function encodeContext(context) {
|
|
21
|
+
return btoa(JSON.stringify(context));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function decodeContext(encodedContext) {
|
|
25
|
+
try {
|
|
26
|
+
if (!isBase64(encodedContext)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return JSON.parse(atob(encodedContext));
|
|
30
|
+
} catch (err) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Context storage functions
|
|
36
|
+
function saveContext(context) {
|
|
37
|
+
try {
|
|
38
|
+
const encodedContext = encodeContext(context);
|
|
39
|
+
localStorage.setItem(contextStorageKey, encodedContext);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.debug('Error saving context to localStorage:', err);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function loadContext() {
|
|
46
|
+
try {
|
|
47
|
+
const encodedContext = localStorage.getItem(contextStorageKey);
|
|
48
|
+
if (!encodedContext) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const context = decodeContext(encodedContext);
|
|
53
|
+
if (!context) {
|
|
54
|
+
// Invalid base64, remove the corrupted data
|
|
55
|
+
localStorage.removeItem(contextStorageKey);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check if context is expired
|
|
60
|
+
if (context.timestamp && Date.now() - context.timestamp > contextMaxAge) {
|
|
61
|
+
localStorage.removeItem(contextStorageKey);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return context;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.debug('Error loading context from localStorage:', err);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const xhr = new XMLHttpRequest();
|
|
73
|
+
let featuresUrl = ""
|
|
74
|
+
|
|
75
|
+
let ctx;
|
|
76
|
+
|
|
77
|
+
function randomId() {
|
|
78
|
+
return crypto.randomUUID();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Try to load existing context, create new one if not found or expired
|
|
82
|
+
let existingContext = loadContext();
|
|
83
|
+
let userId = existingContext?.userId || randomId();
|
|
84
|
+
if (!existingContext?.userId) {
|
|
85
|
+
// Save context with timestamp
|
|
86
|
+
const contextToSave = {
|
|
87
|
+
userId: userId,
|
|
88
|
+
timestamp: Date.now(),
|
|
89
|
+
};
|
|
90
|
+
saveContext(contextToSave);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (abTestingProviders?.growthbook) {
|
|
94
|
+
if (!abTestingProviders.growthbook.clientKey) {
|
|
95
|
+
console.error('GrowthBook client key is not set in settings.');
|
|
96
|
+
delete window.__xydAbTestingSettings;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
featuresUrl = `${'https://cdn.growthbook.io'}/api/features/${abTestingProviders?.growthbook.clientKey}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (abTestingProviders?.launchdarkly) {
|
|
104
|
+
if (!abTestingProviders.launchdarkly.env) {
|
|
105
|
+
console.error('LaunchDarkly env is not set in settings.');
|
|
106
|
+
delete window.__xydAbTestingSettings;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
ctx = {
|
|
111
|
+
kind: "user",
|
|
112
|
+
key: userId
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const ctxBase64 = btoa(JSON.stringify(ctx));
|
|
116
|
+
|
|
117
|
+
// TODO: use sdk in the future
|
|
118
|
+
featuresUrl = `https://app.launchdarkly.com/sdk/evalx/${abTestingProviders.launchdarkly.env}/contexts/${ctxBase64}`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
xhr.open('GET', featuresUrl, false);
|
|
122
|
+
xhr.send(null);
|
|
123
|
+
if (xhr.status != 200) {
|
|
124
|
+
console.error(`Error fetching feature flags: ${xhr.status} ${xhr.statusText}`);
|
|
125
|
+
delete window.__xydAbTestingSettings;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const resp = JSON.parse(xhr.responseText);
|
|
131
|
+
let features = {};
|
|
132
|
+
let client;
|
|
133
|
+
|
|
134
|
+
if (abTestingProviders?.growthbook) {
|
|
135
|
+
// Normalize GrowthBook features: extract defaultValue from each feature
|
|
136
|
+
const rawFeatures = resp.features || {};
|
|
137
|
+
Object.entries(rawFeatures).forEach(([flagKey, flagData]) => {
|
|
138
|
+
if (flagData && typeof flagData === 'object' && 'defaultValue' in flagData) {
|
|
139
|
+
features[flagKey] = flagData.defaultValue;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// TODO: finish
|
|
144
|
+
const gbContext = {
|
|
145
|
+
apiHost: abTestingProviders.growthbook.apiHost || 'https://cdn.growthbook.io',
|
|
146
|
+
clientKey: abTestingProviders.growthbook.clientKey,
|
|
147
|
+
attributes: {
|
|
148
|
+
id: userId,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
/*
|
|
152
|
+
* optional init options
|
|
153
|
+
* @see https://docs.growthbook.io/lib/js#switching-to-init
|
|
154
|
+
*/
|
|
155
|
+
const initOptions = {
|
|
156
|
+
timeout: 2000,
|
|
157
|
+
payload: resp,
|
|
158
|
+
// streaming: false,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// TODO: Add proper type declarations for OpenFeature and GrowthbookProvider
|
|
162
|
+
const OpenFeature = window.OpenFeature;
|
|
163
|
+
const GrowthbookProvider = window.GrowthbookProvider;
|
|
164
|
+
|
|
165
|
+
const ghOpenFeature = new GrowthbookProvider.GrowthbookClientProvider(gbContext, initOptions)
|
|
166
|
+
if (OpenFeature && GrowthbookProvider) {
|
|
167
|
+
OpenFeature.OpenFeature.setProvider(ghOpenFeature);
|
|
168
|
+
client = OpenFeature.OpenFeature.getClient();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (abTestingProviders?.launchdarkly) {
|
|
173
|
+
// Normalize LaunchDarkly features: extract value from each flag
|
|
174
|
+
Object.entries(resp).forEach(([flagKey, flagData]) => {
|
|
175
|
+
if (flagData && typeof flagData === 'object' && 'value' in flagData) {
|
|
176
|
+
features[flagKey] = flagData.value;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Reconstruct bootstrap state from API response
|
|
181
|
+
const bootstrapFlags = {};
|
|
182
|
+
const flagsState = {};
|
|
183
|
+
|
|
184
|
+
// TODO: !!! USE LAUNCHDARKLY SDK !!! BUT WE NEED TO DO THIS SYNC
|
|
185
|
+
Object.entries(resp).forEach(([flagKey, flagData]) => {
|
|
186
|
+
if (flagData && typeof flagData === 'object' && 'value' in flagData) {
|
|
187
|
+
const flag = flagData;
|
|
188
|
+
// Set the flag value in bootstrap
|
|
189
|
+
bootstrapFlags[flagKey] = flag.value;
|
|
190
|
+
|
|
191
|
+
// Set the full flag state
|
|
192
|
+
flagsState[flagKey] = flag
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// TODO: use OpenFeature.OpenFeature.setProvider
|
|
197
|
+
const OpenFeature = window.OpenFeature;
|
|
198
|
+
const LaunchDarklyProvider = window.LaunchDarklyProvider;
|
|
199
|
+
|
|
200
|
+
if (OpenFeature && LaunchDarklyProvider) {
|
|
201
|
+
OpenFeature.OpenFeature.setProvider(
|
|
202
|
+
new LaunchDarklyProvider.LaunchDarklyClientProvider(
|
|
203
|
+
abTestingProviders.launchdarkly.env,
|
|
204
|
+
{
|
|
205
|
+
bootstrap: {
|
|
206
|
+
...bootstrapFlags,
|
|
207
|
+
$flagsState: flagsState
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
),
|
|
211
|
+
ctx
|
|
212
|
+
);
|
|
213
|
+
client = OpenFeature.OpenFeature.getClient();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (client) {
|
|
218
|
+
window.openfeature = client
|
|
219
|
+
}
|
|
220
|
+
console.log
|
|
221
|
+
const events = client.emitterAccessor()
|
|
222
|
+
events.emit("PROVIDER_READY")
|
|
223
|
+
events.emit("PROVIDER_CONFIGURATION_CHANGED")
|
|
224
|
+
|
|
225
|
+
// Create dynamic CSS for feature flags
|
|
226
|
+
let cssRules = [];
|
|
227
|
+
|
|
228
|
+
// Default rule: hide all feature elements
|
|
229
|
+
cssRules.push('[data-feature-flag] { display: none !important; }');
|
|
230
|
+
|
|
231
|
+
// Generate rules for each feature flag
|
|
232
|
+
// TODO: !!! FOR SOME REASON WE NEED THIS HACK EVEN IF WE EMIT THE EVENTS (maybe it's wrong used?) - BUT CHANGE THAT IN THE FUTURE !!!
|
|
233
|
+
setTimeout(() => {
|
|
234
|
+
Object.entries(features || {}).forEach(([featureKey, flagValue]) => {
|
|
235
|
+
// Determine the type of feature flag and get its current value
|
|
236
|
+
let currentValue;
|
|
237
|
+
// Now features is normalized: {[flag]: value}
|
|
238
|
+
|
|
239
|
+
// Check if it's a boolean feature
|
|
240
|
+
if (typeof flagValue === 'boolean') {
|
|
241
|
+
// TODO: impl default values from markdown + navigation
|
|
242
|
+
currentValue = client.getBooleanValue(featureKey, false);
|
|
243
|
+
}
|
|
244
|
+
// Check if it's a string feature
|
|
245
|
+
else if (typeof flagValue === 'string') {
|
|
246
|
+
currentValue = client.getStringValue(featureKey, "");
|
|
247
|
+
}
|
|
248
|
+
// Check if it's a number feature
|
|
249
|
+
else if (typeof flagValue === 'number') {
|
|
250
|
+
currentValue = client.getNumberValue(featureKey, 0);
|
|
251
|
+
}
|
|
252
|
+
// For object features, use getObjectValue
|
|
253
|
+
else if (typeof flagValue === 'object' && flagValue !== null) {
|
|
254
|
+
currentValue = client.getObjectValue(featureKey);
|
|
255
|
+
}
|
|
256
|
+
// Fallback to string evaluation for unknown types
|
|
257
|
+
else {
|
|
258
|
+
currentValue = client.getStringValue(featureKey, "");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (currentValue !== undefined) {
|
|
262
|
+
// Create rule to show elements when feature flag matches the expected value
|
|
263
|
+
cssRules.push(`[data-feature-flag="${featureKey}"][data-feature-match="${currentValue}"] { display: block !important; }`);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Apply the CSS rules
|
|
268
|
+
if (cssRules.length > 0) {
|
|
269
|
+
const style = document.createElement('style');
|
|
270
|
+
style.textContent = cssRules.join('\n');
|
|
271
|
+
document.head.appendChild(style);
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
} catch (e) {
|
|
275
|
+
console.error('Error processing feature flags:', e);
|
|
276
|
+
} finally {
|
|
277
|
+
delete window.__xydAbTestingSettings;
|
|
278
|
+
}
|
|
279
|
+
})();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
(window.requestAnimationFrame ?? window.setTimeout)(() => {
|
|
3
|
+
if (typeof window === "undefined") {
|
|
4
|
+
return
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const bannerHeight = document.querySelector("xyd-banner")?.clientHeight ?? 0;
|
|
8
|
+
if (!bannerHeight) {
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
document.documentElement.style.setProperty("--xyd-banner-height-dynamic", `${String(bannerHeight)}px`)
|
|
13
|
+
})
|
|
14
|
+
})()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
try {
|
|
3
|
+
var theme = localStorage.getItem('xyd-color-scheme') || 'auto';
|
|
4
|
+
var isDark = false;
|
|
5
|
+
|
|
6
|
+
if (theme === 'dark') {
|
|
7
|
+
isDark = true;
|
|
8
|
+
} else if (theme === 'auto') {
|
|
9
|
+
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (isDark) {
|
|
13
|
+
document.documentElement.setAttribute('data-color-scheme', 'dark');
|
|
14
|
+
}
|
|
15
|
+
} catch (e) {
|
|
16
|
+
// Fallback to system preference if localStorage fails
|
|
17
|
+
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
18
|
+
document.documentElement.setAttribute('data-color-scheme', 'dark');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
})();
|