@probat/react 0.1.3 → 0.1.4

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,35 @@ 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
+ // Only attempt in browser and not during Next.js server-side compilation
354
+ const isBrowser = typeof window !== "undefined";
355
+ const isNextJSServer = typeof window === "undefined" ||
356
+ (typeof (globalThis as any).process !== "undefined" && (globalThis as any).process.env?.NEXT_RUNTIME === "nodejs");
357
+
358
+ if (isBrowser && !isNextJSServer) {
359
+ try {
360
+ const variantUrl = `/probat/${filePath}`;
361
+ // Use dynamic import - Vite can resolve relative imports automatically
362
+ const mod = await import(/* @vite-ignore */ variantUrl);
363
+ const VariantComponent = (mod as any)?.default || mod;
364
+ if (VariantComponent && typeof VariantComponent === "function") {
365
+ console.log(`[PROBAT] ✅ Loaded variant via dynamic import (CSR): ${variantUrl}`);
366
+ return VariantComponent as React.ComponentType<any>;
367
+ }
368
+ } catch (dynamicImportError) {
369
+ // Dynamic import failed (might be Next.js or other issue), fall through to fetch+babel
370
+ console.debug(`[PROBAT] Dynamic import failed, using fetch+babel:`, dynamicImportError);
371
+ }
372
+ }
373
+
374
+ // Fallback: fetch + babel compilation (works for Next.js SSR and when dynamic import fails)
230
375
  const localUrl = `/probat/${filePath}`;
231
376
  try {
232
377
  const localRes = await fetch(localUrl, {
@@ -316,6 +461,23 @@ export async function loadVariantComponent(
316
461
  rawCode = rawCode.replace(/^import\s+.*\/@vite\/client.*$/gm, "");
317
462
  rawCode = rawCode.replace(/import\.meta\.hot(?:\.[\w$]+)*/g, "undefined");
318
463
 
464
+ // Preprocess relative imports: convert to absolute paths that can be fetched
465
+ // This allows the require shim to fetch them later
466
+ const relativeImportMap = new Map<string, string>();
467
+ rawCode = rawCode.replace(
468
+ /^import\s+(\w+)\s+from\s+['"](\.\.?\/[^'"]+)['"];?\s*$/gm,
469
+ (match, importName, relativePath) => {
470
+ // Resolve relative path to absolute
471
+ const baseDir = filePath.substring(0, filePath.lastIndexOf('/'));
472
+ const resolvedPath = resolveRelativePath(relativePath, baseDir);
473
+ // Try to find the file with extension
474
+ const absolutePath = resolvedPath.startsWith('/') ? resolvedPath : '/' + resolvedPath;
475
+ relativeImportMap.set(importName, absolutePath);
476
+ // Replace with absolute path import - Babel will compile this to require()
477
+ return `import ${importName} from "${absolutePath}";`;
478
+ }
479
+ );
480
+
319
481
  const compiled = Babel.transform(rawCode, {
320
482
  presets: [
321
483
  ["react", { runtime: "classic" }],
@@ -326,8 +488,25 @@ export async function loadVariantComponent(
326
488
  filename: filePath,
327
489
  }).code;
328
490
 
491
+ // Pre-load relative imports if any
492
+ const relativeModules: Record<string, any> = {};
493
+ if (relativeImportMap.size > 0) {
494
+ for (const [importName, absolutePath] of relativeImportMap.entries()) {
495
+ try {
496
+ const moduleExports = await loadRelativeModule(absolutePath, filePath, repoFullName, baseRef);
497
+ relativeModules[absolutePath] = moduleExports.default || moduleExports;
498
+ } catch (err) {
499
+ console.warn(`[PROBAT] Failed to load relative import ${absolutePath}:`, err);
500
+ relativeModules[absolutePath] = null;
501
+ }
502
+ }
503
+ }
504
+
505
+ // Build require shim that can resolve relative imports
506
+ const relativeModulesJson = JSON.stringify(relativeModules);
329
507
  code = `
330
508
  var __probatVariant = (function() {
509
+ var relativeModules = ${relativeModulesJson};
331
510
  var require = function(name) {
332
511
  if (name === "react" || name === "react/jsx-runtime") {
333
512
  return window.React || globalThis.React;
@@ -338,6 +517,14 @@ export async function loadVariantComponent(
338
517
  if (name === "react/jsx-runtime.js") {
339
518
  return window.React || globalThis.React;
340
519
  }
520
+ // Handle relative imports (now converted to absolute paths)
521
+ if (name.startsWith("/") && relativeModules.hasOwnProperty(name)) {
522
+ var mod = relativeModules[name];
523
+ if (mod === null) {
524
+ throw new Error("Failed to load module: " + name);
525
+ }
526
+ return mod;
527
+ }
341
528
  throw new Error("Unsupported module: " + name);
342
529
  };
343
530
  var module = { exports: {} };
@@ -353,12 +540,12 @@ export async function loadVariantComponent(
353
540
  }
354
541
 
355
542
  if (!rawCodeFetched || code === "") {
356
- const variantUrl = `${baseUrl.replace(/\/$/, "")}/variants/${filePath}`;
543
+ const variantUrl = `${baseUrl.replace(/\/$/, "")}/variants/${filePath}`;
357
544
  const serverRes = await fetch(variantUrl, {
358
- method: "GET",
359
- headers: { Accept: "text/javascript" },
360
- credentials: "include",
361
- });
545
+ method: "GET",
546
+ headers: { Accept: "text/javascript" },
547
+ credentials: "include",
548
+ });
362
549
  if (!serverRes.ok) {
363
550
  throw new Error(`HTTP ${serverRes.status}`);
364
551
  }