@probat/react 0.1.3 → 0.1.5

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
@@ -178,6 +178,7 @@ export async function fetchComponentExperimentConfig(
178
178
 
179
179
  // Cache for variant component loads
180
180
  const variantComponentCache = new Map<string, Promise<React.ComponentType<any> | null>>();
181
+ const moduleCache = new Map<string, any>(); // Cache for loaded modules (relative imports)
181
182
 
182
183
  // Make React available globally for variant components
183
184
  if (typeof window !== "undefined") {
@@ -185,6 +186,139 @@ if (typeof window !== "undefined") {
185
186
  (window as any).React = (window as any).React || React;
186
187
  }
187
188
 
189
+ /**
190
+ * Resolve relative import path to absolute path
191
+ */
192
+ function resolveRelativePath(relativePath: string, basePath: string): string {
193
+ // Remove file extension if present
194
+ const baseDir = basePath.substring(0, basePath.lastIndexOf('/'));
195
+ const parts = baseDir.split('/').filter(Boolean);
196
+ const relativeParts = relativePath.split('/').filter(Boolean);
197
+
198
+ for (const part of relativeParts) {
199
+ if (part === '..') {
200
+ parts.pop();
201
+ } else if (part !== '.') {
202
+ parts.push(part);
203
+ }
204
+ }
205
+
206
+ return '/' + parts.join('/');
207
+ }
208
+
209
+ /**
210
+ * Fetch and compile a module from absolute path
211
+ */
212
+ async function loadRelativeModule(
213
+ absolutePath: string,
214
+ baseFilePath: string,
215
+ repoFullName?: string,
216
+ baseRef?: string
217
+ ): Promise<any> {
218
+ const cacheKey = `${baseFilePath}:${absolutePath}`;
219
+ if (moduleCache.has(cacheKey)) {
220
+ return moduleCache.get(cacheKey);
221
+ }
222
+
223
+ // Try to determine file extension
224
+ const extensions = ['.jsx', '.tsx', '.js', '.ts'];
225
+ let moduleCode: string | null = null;
226
+ let modulePath: string | null = null;
227
+
228
+ // Try each extension
229
+ for (const ext of extensions) {
230
+ const testPath = absolutePath + (absolutePath.includes('.') ? '' : ext);
231
+
232
+ // Try local first
233
+ try {
234
+ const localRes = await fetch(testPath, {
235
+ method: "GET",
236
+ headers: { Accept: "text/plain" },
237
+ });
238
+ if (localRes.ok) {
239
+ moduleCode = await localRes.text();
240
+ modulePath = testPath;
241
+ break;
242
+ }
243
+ } catch {
244
+ // Continue to next extension or GitHub
245
+ }
246
+
247
+ // Try GitHub if local failed
248
+ if (!moduleCode && repoFullName) {
249
+ try {
250
+ const githubPath = testPath.startsWith('/') ? testPath.substring(1) : testPath;
251
+ const githubUrl = `https://raw.githubusercontent.com/${repoFullName}/${baseRef || 'main'}/${githubPath}`;
252
+ const res = await fetch(githubUrl, { method: "GET", headers: { Accept: "text/plain" } });
253
+ if (res.ok) {
254
+ moduleCode = await res.text();
255
+ modulePath = testPath;
256
+ break;
257
+ }
258
+ } catch {
259
+ // Continue
260
+ }
261
+ }
262
+ }
263
+
264
+ if (!moduleCode || !modulePath) {
265
+ throw new Error(`Could not resolve module: ${absolutePath} from ${baseFilePath}`);
266
+ }
267
+
268
+ // Compile the module
269
+ let Babel: any;
270
+ if (typeof window !== "undefined" && (window as any).Babel) {
271
+ Babel = (window as any).Babel;
272
+ } else {
273
+ try {
274
+ // @ts-ignore
275
+ const babelModule = await import("@babel/standalone");
276
+ Babel = babelModule.default || babelModule;
277
+ } catch {
278
+ throw new Error("Babel not available for compiling relative import");
279
+ }
280
+ }
281
+
282
+ // Preprocess module code
283
+ let processedCode = moduleCode;
284
+ processedCode = processedCode.replace(/^import\s+['"].*\.css['"];?\s*$/gm, "");
285
+ processedCode = processedCode.replace(/^import\s+React\s+from\s+['"]react['"];?\s*$/m, "const React = window.React || globalThis.React;");
286
+ processedCode = processedCode.replace(/import\.meta\.env\.[\w$]+/g, "undefined");
287
+ processedCode = processedCode.replace(/\bimport\.meta\b/g, "({})");
288
+
289
+ const isTSX = modulePath.endsWith(".tsx");
290
+ const compiled = Babel.transform(processedCode, {
291
+ presets: [
292
+ ["react", { runtime: "classic" }],
293
+ ["typescript", { allExtensions: true, isTSX }],
294
+ ],
295
+ plugins: [["transform-modules-commonjs", { allowTopLevelThis: true }]],
296
+ sourceType: "module",
297
+ filename: modulePath,
298
+ }).code;
299
+
300
+ // Execute compiled module
301
+ const moduleCodeWrapper = `
302
+ var require = function(name) {
303
+ if (name === "react" || name === "react/jsx-runtime") {
304
+ return window.React || globalThis.React;
305
+ }
306
+ if (name.startsWith("/@vite") || name.includes("@vite/client")) {
307
+ return {};
308
+ }
309
+ throw new Error("Unsupported module in relative import: " + name);
310
+ };
311
+ var module = { exports: {} };
312
+ var exports = module.exports;
313
+ ${compiled}
314
+ return module.exports;
315
+ `;
316
+
317
+ const moduleExports = new Function(moduleCodeWrapper)();
318
+ moduleCache.set(cacheKey, moduleExports);
319
+ return moduleExports;
320
+ }
321
+
188
322
  export async function loadVariantComponent(
189
323
  baseUrl: string,
190
324
  proposalId: string,
@@ -209,24 +343,41 @@ export async function loadVariantComponent(
209
343
  const loadPromise = (async () => {
210
344
  try {
211
345
  // ============================================
212
- // Preferred path: dynamic ESM import (Vite/Next can resolve deps)
346
+ // Hybrid approach: Dynamic import for CSR (Vite), fetch+babel for SSR (Next.js)
213
347
  // ============================================
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
348
  let code: string = "";
227
349
  let rawCode: string = "";
228
350
  let rawCodeFetched = false;
229
351
 
352
+ // Try dynamic import first (works great for Vite/CSR, resolves relative imports automatically)
353
+ // Skip for Next.js - Turbopack statically analyzes import() calls and fails
354
+ const isBrowser = typeof window !== "undefined";
355
+ const isNextJS = isBrowser && (
356
+ (window as any).__NEXT_DATA__ !== undefined ||
357
+ (window as any).__NEXT_LOADED_PAGES__ !== undefined ||
358
+ typeof (globalThis as any).__NEXT_DATA__ !== "undefined"
359
+ );
360
+
361
+ // Only use dynamic import for non-Next.js environments (Vite, etc.)
362
+ if (isBrowser && !isNextJS) {
363
+ try {
364
+ const variantUrl = `/probat/${filePath}`;
365
+ // Use Function constructor to prevent Next.js/Turbopack from statically analyzing
366
+ // This makes the import completely dynamic and invisible to static analysis
367
+ const dynamicImportFunc = new Function('url', 'return import(url)');
368
+ const mod = await dynamicImportFunc(variantUrl);
369
+ const VariantComponent = (mod as any)?.default || mod;
370
+ if (VariantComponent && typeof VariantComponent === "function") {
371
+ console.log(`[PROBAT] ✅ Loaded variant via dynamic import (CSR): ${variantUrl}`);
372
+ return VariantComponent as React.ComponentType<any>;
373
+ }
374
+ } catch (dynamicImportError) {
375
+ // Dynamic import failed, fall through to fetch+babel
376
+ console.debug(`[PROBAT] Dynamic import failed, using fetch+babel:`, dynamicImportError);
377
+ }
378
+ }
379
+
380
+ // Fallback: fetch + babel compilation (works for Next.js SSR and when dynamic import fails)
230
381
  const localUrl = `/probat/${filePath}`;
231
382
  try {
232
383
  const localRes = await fetch(localUrl, {
@@ -316,6 +467,23 @@ export async function loadVariantComponent(
316
467
  rawCode = rawCode.replace(/^import\s+.*\/@vite\/client.*$/gm, "");
317
468
  rawCode = rawCode.replace(/import\.meta\.hot(?:\.[\w$]+)*/g, "undefined");
318
469
 
470
+ // Preprocess relative imports: convert to absolute paths that can be fetched
471
+ // This allows the require shim to fetch them later
472
+ const relativeImportMap = new Map<string, string>();
473
+ rawCode = rawCode.replace(
474
+ /^import\s+(\w+)\s+from\s+['"](\.\.?\/[^'"]+)['"];?\s*$/gm,
475
+ (match, importName, relativePath) => {
476
+ // Resolve relative path to absolute
477
+ const baseDir = filePath.substring(0, filePath.lastIndexOf('/'));
478
+ const resolvedPath = resolveRelativePath(relativePath, baseDir);
479
+ // Try to find the file with extension
480
+ const absolutePath = resolvedPath.startsWith('/') ? resolvedPath : '/' + resolvedPath;
481
+ relativeImportMap.set(importName, absolutePath);
482
+ // Replace with absolute path import - Babel will compile this to require()
483
+ return `import ${importName} from "${absolutePath}";`;
484
+ }
485
+ );
486
+
319
487
  const compiled = Babel.transform(rawCode, {
320
488
  presets: [
321
489
  ["react", { runtime: "classic" }],
@@ -326,8 +494,25 @@ export async function loadVariantComponent(
326
494
  filename: filePath,
327
495
  }).code;
328
496
 
497
+ // Pre-load relative imports if any
498
+ const relativeModules: Record<string, any> = {};
499
+ if (relativeImportMap.size > 0) {
500
+ for (const [importName, absolutePath] of relativeImportMap.entries()) {
501
+ try {
502
+ const moduleExports = await loadRelativeModule(absolutePath, filePath, repoFullName, baseRef);
503
+ relativeModules[absolutePath] = moduleExports.default || moduleExports;
504
+ } catch (err) {
505
+ console.warn(`[PROBAT] Failed to load relative import ${absolutePath}:`, err);
506
+ relativeModules[absolutePath] = null;
507
+ }
508
+ }
509
+ }
510
+
511
+ // Build require shim that can resolve relative imports
512
+ const relativeModulesJson = JSON.stringify(relativeModules);
329
513
  code = `
330
514
  var __probatVariant = (function() {
515
+ var relativeModules = ${relativeModulesJson};
331
516
  var require = function(name) {
332
517
  if (name === "react" || name === "react/jsx-runtime") {
333
518
  return window.React || globalThis.React;
@@ -338,6 +523,14 @@ export async function loadVariantComponent(
338
523
  if (name === "react/jsx-runtime.js") {
339
524
  return window.React || globalThis.React;
340
525
  }
526
+ // Handle relative imports (now converted to absolute paths)
527
+ if (name.startsWith("/") && relativeModules.hasOwnProperty(name)) {
528
+ var mod = relativeModules[name];
529
+ if (mod === null) {
530
+ throw new Error("Failed to load module: " + name);
531
+ }
532
+ return mod;
533
+ }
341
534
  throw new Error("Unsupported module: " + name);
342
535
  };
343
536
  var module = { exports: {} };
@@ -353,12 +546,12 @@ export async function loadVariantComponent(
353
546
  }
354
547
 
355
548
  if (!rawCodeFetched || code === "") {
356
- const variantUrl = `${baseUrl.replace(/\/$/, "")}/variants/${filePath}`;
549
+ const variantUrl = `${baseUrl.replace(/\/$/, "")}/variants/${filePath}`;
357
550
  const serverRes = await fetch(variantUrl, {
358
- method: "GET",
359
- headers: { Accept: "text/javascript" },
360
- credentials: "include",
361
- });
551
+ method: "GET",
552
+ headers: { Accept: "text/javascript" },
553
+ credentials: "include",
554
+ });
362
555
  if (!serverRes.ok) {
363
556
  throw new Error(`HTTP ${serverRes.status}`);
364
557
  }