@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/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
- const response = await fetch(url, {
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
- // Log in development for debugging
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
- // Fetch the variant code (server compiles TSX/JSX to JS)
229
- const variantUrl = `${baseUrl.replace(/\/$/, "")}/variants/${filePath}`;
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
- const res = await fetch(variantUrl, {
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
- if (!res.ok) {
238
- throw new Error(`HTTP ${res.status}`);
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
  }