@probat/react 0.1.2 → 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.
- package/dist/index.d.mts +1 -30
- package/dist/index.d.ts +1 -30
- package/dist/index.js +191 -193
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +193 -191
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -1
- package/src/context/ProbatContext.tsx +1 -12
- package/src/hoc/itrt-frontend.code-workspace +3 -0
- package/src/hoc/withExperiment.tsx +4 -12
- package/src/index.ts +0 -6
- package/src/utils/api.ts +158 -40
package/src/utils/api.ts
CHANGED
|
@@ -15,6 +15,8 @@ export type ComponentVariantInfo = {
|
|
|
15
15
|
|
|
16
16
|
export type ComponentExperimentConfig = {
|
|
17
17
|
proposal_id: string;
|
|
18
|
+
repo_full_name: string;
|
|
19
|
+
base_ref?: string; // Git branch/ref (default: "main")
|
|
18
20
|
variants: Record<string, ComponentVariantInfo>;
|
|
19
21
|
};
|
|
20
22
|
|
|
@@ -82,7 +84,7 @@ export async function sendMetric(
|
|
|
82
84
|
captured_at: new Date().toISOString(),
|
|
83
85
|
};
|
|
84
86
|
try {
|
|
85
|
-
|
|
87
|
+
await fetch(url, {
|
|
86
88
|
method: "POST",
|
|
87
89
|
headers: {
|
|
88
90
|
Accept: "application/json",
|
|
@@ -91,29 +93,8 @@ export async function sendMetric(
|
|
|
91
93
|
credentials: "include", // CRITICAL: Include cookies to distinguish different users
|
|
92
94
|
body: JSON.stringify(body),
|
|
93
95
|
});
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
if (!response.ok) {
|
|
97
|
-
console.warn('[PROBAT] Metric send failed:', {
|
|
98
|
-
status: response.status,
|
|
99
|
-
statusText: response.statusText,
|
|
100
|
-
url,
|
|
101
|
-
body,
|
|
102
|
-
});
|
|
103
|
-
} else {
|
|
104
|
-
console.log('[PROBAT] Metric sent successfully:', {
|
|
105
|
-
metricName,
|
|
106
|
-
proposalId,
|
|
107
|
-
variantLabel,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
} catch (error) {
|
|
111
|
-
// Log error in development, but don't break the app
|
|
112
|
-
console.error('[PROBAT] Error sending metric:', {
|
|
113
|
-
error: error instanceof Error ? error.message : String(error),
|
|
114
|
-
url,
|
|
115
|
-
body,
|
|
116
|
-
});
|
|
96
|
+
} catch {
|
|
97
|
+
// Silently fail - metrics should not break the app
|
|
117
98
|
}
|
|
118
99
|
}
|
|
119
100
|
|
|
@@ -208,7 +189,9 @@ export async function loadVariantComponent(
|
|
|
208
189
|
baseUrl: string,
|
|
209
190
|
proposalId: string,
|
|
210
191
|
experimentId: string,
|
|
211
|
-
filePath: string | null
|
|
192
|
+
filePath: string | null,
|
|
193
|
+
repoFullName?: string,
|
|
194
|
+
baseRef?: string
|
|
212
195
|
): Promise<React.ComponentType<any> | null> {
|
|
213
196
|
if (!filePath) {
|
|
214
197
|
return null;
|
|
@@ -225,29 +208,167 @@ export async function loadVariantComponent(
|
|
|
225
208
|
// Create new load promise
|
|
226
209
|
const loadPromise = (async () => {
|
|
227
210
|
try {
|
|
228
|
-
//
|
|
229
|
-
|
|
211
|
+
// ============================================
|
|
212
|
+
// Preferred path: dynamic ESM import (Vite/Next can resolve deps)
|
|
213
|
+
// ============================================
|
|
214
|
+
try {
|
|
215
|
+
const variantUrl = `/probat/${filePath}`;
|
|
216
|
+
const mod = await import(/* @vite-ignore */ variantUrl);
|
|
217
|
+
const VariantComponent = (mod as any)?.default || mod;
|
|
218
|
+
if (VariantComponent && typeof VariantComponent === "function") {
|
|
219
|
+
return VariantComponent as React.ComponentType<any>;
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
// Fall through to legacy path
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Legacy path (fetch + babel + eval) retained for non-Vite envs
|
|
226
|
+
let code: string = "";
|
|
227
|
+
let rawCode: string = "";
|
|
228
|
+
let rawCodeFetched = false;
|
|
229
|
+
|
|
230
|
+
const localUrl = `/probat/${filePath}`;
|
|
231
|
+
try {
|
|
232
|
+
const localRes = await fetch(localUrl, {
|
|
233
|
+
method: "GET",
|
|
234
|
+
headers: { Accept: "text/plain" },
|
|
235
|
+
});
|
|
236
|
+
if (localRes.ok) {
|
|
237
|
+
rawCode = await localRes.text();
|
|
238
|
+
rawCodeFetched = true;
|
|
239
|
+
console.log(`[PROBAT] ✅ Loaded variant from local (user's repo): ${localUrl}`);
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
console.debug(`[PROBAT] Local file not available (${localUrl}), trying GitHub...`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!rawCodeFetched && repoFullName) {
|
|
246
|
+
const githubPath = `probat/${filePath}`;
|
|
247
|
+
const gitRef = baseRef || "main";
|
|
248
|
+
const githubUrl = `https://raw.githubusercontent.com/${repoFullName}/${gitRef}/${githubPath}`;
|
|
249
|
+
const res = await fetch(githubUrl, { method: "GET", headers: { Accept: "text/plain" } });
|
|
250
|
+
if (res.ok) {
|
|
251
|
+
rawCode = await res.text();
|
|
252
|
+
rawCodeFetched = true;
|
|
253
|
+
console.log(`[PROBAT] ⚠️ Loaded variant from GitHub (fallback): ${githubUrl}`);
|
|
254
|
+
} else {
|
|
255
|
+
console.warn(`[PROBAT] ⚠️ GitHub fetch failed (${res.status}), falling back to server compilation`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
230
258
|
|
|
231
|
-
|
|
259
|
+
if (rawCodeFetched && rawCode) {
|
|
260
|
+
let Babel: any;
|
|
261
|
+
if (typeof window !== "undefined" && (window as any).Babel) {
|
|
262
|
+
Babel = (window as any).Babel;
|
|
263
|
+
} else {
|
|
264
|
+
try {
|
|
265
|
+
// @ts-ignore
|
|
266
|
+
const babelModule = await import("@babel/standalone");
|
|
267
|
+
Babel = babelModule.default || babelModule;
|
|
268
|
+
} catch (importError) {
|
|
269
|
+
try {
|
|
270
|
+
await new Promise<void>((resolve, reject) => {
|
|
271
|
+
if (typeof document === "undefined") {
|
|
272
|
+
reject(new Error("Document not available"));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if ((window as any).Babel) {
|
|
276
|
+
Babel = (window as any).Babel;
|
|
277
|
+
resolve();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const script = document.createElement("script");
|
|
281
|
+
script.src = "https://unpkg.com/@babel/standalone/babel.min.js";
|
|
282
|
+
script.async = true;
|
|
283
|
+
script.onload = () => {
|
|
284
|
+
Babel = (window as any).Babel;
|
|
285
|
+
if (!Babel) reject(new Error("Babel not found after script load"));
|
|
286
|
+
else resolve();
|
|
287
|
+
};
|
|
288
|
+
script.onerror = () => reject(new Error("Failed to load Babel from CDN"));
|
|
289
|
+
document.head.appendChild(script);
|
|
290
|
+
});
|
|
291
|
+
} catch (babelError) {
|
|
292
|
+
console.error("[PROBAT] Failed to load Babel, falling back to server compilation", babelError);
|
|
293
|
+
rawCodeFetched = false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (rawCodeFetched && rawCode && Babel) {
|
|
299
|
+
const isTSX = filePath.endsWith(".tsx");
|
|
300
|
+
rawCode = rawCode.replace(/^import\s+['"].*\.css['"];?\s*$/gm, "");
|
|
301
|
+
rawCode = rawCode.replace(/^import\s+.*from\s+['"].*\.css['"];?\s*$/gm, "");
|
|
302
|
+
rawCode = rawCode.replace(
|
|
303
|
+
/^import\s+React(?:\s*,\s*\{[^}]*\})?\s+from\s+['"]react['"];?\s*$/m,
|
|
304
|
+
"const React = window.React || globalThis.React;"
|
|
305
|
+
);
|
|
306
|
+
rawCode = rawCode.replace(
|
|
307
|
+
/^import\s+\*\s+as\s+React\s+from\s+['"]react['"];?\s*$/m,
|
|
308
|
+
"const React = window.React || globalThis.React;"
|
|
309
|
+
);
|
|
310
|
+
rawCode = rawCode.replace(
|
|
311
|
+
/^import\s+\{([^}]+)\}\s+from\s+['"]react['"];?\s*$/m,
|
|
312
|
+
(match, imports) => `const {${imports}} = window.React || globalThis.React;`
|
|
313
|
+
);
|
|
314
|
+
rawCode = rawCode.replace(/import\.meta\.env\.[\w$]+/g, "undefined");
|
|
315
|
+
rawCode = rawCode.replace(/\bimport\.meta\b/g, "({})");
|
|
316
|
+
rawCode = rawCode.replace(/^import\s+.*\/@vite\/client.*$/gm, "");
|
|
317
|
+
rawCode = rawCode.replace(/import\.meta\.hot(?:\.[\w$]+)*/g, "undefined");
|
|
318
|
+
|
|
319
|
+
const compiled = Babel.transform(rawCode, {
|
|
320
|
+
presets: [
|
|
321
|
+
["react", { runtime: "classic" }],
|
|
322
|
+
["typescript", { allExtensions: true, isTSX }],
|
|
323
|
+
],
|
|
324
|
+
plugins: [["transform-modules-commonjs", { allowTopLevelThis: true }]],
|
|
325
|
+
sourceType: "module",
|
|
326
|
+
filename: filePath,
|
|
327
|
+
}).code;
|
|
328
|
+
|
|
329
|
+
code = `
|
|
330
|
+
var __probatVariant = (function() {
|
|
331
|
+
var require = function(name) {
|
|
332
|
+
if (name === "react" || name === "react/jsx-runtime") {
|
|
333
|
+
return window.React || globalThis.React;
|
|
334
|
+
}
|
|
335
|
+
if (name.startsWith("/@vite") || name.includes("@vite/client") || name.includes(".vite/deps")) {
|
|
336
|
+
return {};
|
|
337
|
+
}
|
|
338
|
+
if (name === "react/jsx-runtime.js") {
|
|
339
|
+
return window.React || globalThis.React;
|
|
340
|
+
}
|
|
341
|
+
throw new Error("Unsupported module: " + name);
|
|
342
|
+
};
|
|
343
|
+
var module = { exports: {} };
|
|
344
|
+
var exports = module.exports;
|
|
345
|
+
${compiled}
|
|
346
|
+
return module.exports.default || module.exports;
|
|
347
|
+
})();
|
|
348
|
+
`;
|
|
349
|
+
} else {
|
|
350
|
+
rawCodeFetched = false;
|
|
351
|
+
code = "";
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!rawCodeFetched || code === "") {
|
|
356
|
+
const variantUrl = `${baseUrl.replace(/\/$/, "")}/variants/${filePath}`;
|
|
357
|
+
const serverRes = await fetch(variantUrl, {
|
|
232
358
|
method: "GET",
|
|
233
359
|
headers: { Accept: "text/javascript" },
|
|
234
360
|
credentials: "include",
|
|
235
361
|
});
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
362
|
+
if (!serverRes.ok) {
|
|
363
|
+
throw new Error(`HTTP ${serverRes.status}`);
|
|
364
|
+
}
|
|
365
|
+
code = await serverRes.text();
|
|
239
366
|
}
|
|
240
367
|
|
|
241
|
-
const code = await res.text();
|
|
242
|
-
|
|
243
|
-
// The server returns an IIFE that assigns to __probatVariant
|
|
244
|
-
// Execute the code to get the component
|
|
245
|
-
// First, ensure React is available globally
|
|
246
368
|
if (typeof window !== "undefined") {
|
|
247
369
|
(window as any).React = (window as any).React || React;
|
|
248
370
|
}
|
|
249
371
|
|
|
250
|
-
// Execute the IIFE code
|
|
251
372
|
const evalFunc = new Function(`
|
|
252
373
|
var __probatVariant;
|
|
253
374
|
${code}
|
|
@@ -255,10 +376,7 @@ export async function loadVariantComponent(
|
|
|
255
376
|
`);
|
|
256
377
|
|
|
257
378
|
const result = evalFunc();
|
|
258
|
-
|
|
259
|
-
// The result is { default: Component } from esbuild's IIFE output
|
|
260
379
|
const VariantComponent = result?.default || result;
|
|
261
|
-
|
|
262
380
|
if (typeof VariantComponent === "function") {
|
|
263
381
|
return VariantComponent as React.ComponentType<any>;
|
|
264
382
|
}
|