UNPKG

@jakobcooldown/react-csr-sdk

Version:

Mockery SDK for dynamic bundle loading in web applications

386 lines (293 loc) 10.4 kB
# Mockery React CSR SDK Dynamic bundle loading for full React app replacement based on user/tenant context. ## Overview The Mockery React CSR SDK enables you to **completely replace** your React application bundle based on who is logged in. ## How It Works 1. **HTML loads** → **Bundle loader checks localStorage** → **Loads custom OR default bundle** → **Single React app starts** 2. No React component conflicts or DOM manipulation issues 3. True full-app replacement, not overlay customizations ## Security Architecture The SDK follows a secure server-to-server architecture: ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Browser │ │ Vendor │ │ Mockery │ │ (SDK) │ │ Backend │ │ API │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ GET /api/bundle │ │ ├─────────────────►│ │ │ │ GET /vendor-api │ │ ├─────────────────►│ │ │ (with API key) │ │ │ │ │ │ bundle URL │ │ │◄─────────────────┤ │ bundle URL │ │ │◄─────────────────┤ │ │ │ │ SDK loads bundle │ │ ``` - **Browser**: Never sees API keys, calls vendor backend - **Vendor Backend**: Authenticates users, calls Mockery API server-to-server - **Mockery API**: Returns bundle URLs based on tenant configuration ## Installation ```bash npm install @mockery/react-csr-sdk ``` ## Quick Start ### 1. Include the Bundle Loader Add the bundle loader script to your HTML **before any React bundles**: ```html <!DOCTYPE html> <html> <head> <!-- Include Mockery bundle loader FIRST --> <script src="node_modules/@mockery/react-csr-sdk/dist/bundle-loader.js"></script> <!-- Mark your default bundle --> <script data-mockery-default-bundle type="module" src="./assets/index-abc123.js"></script> </head> <body> <div id="root"></div> </body> </html> ``` ### 2. Create Vendor Backend Endpoint Your backend should authenticate users and call the Mockery API: ```javascript // GET /api/user/bundle app.get('/api/user/bundle', authenticateUser, async (req, res) => { const user = req.user; // Your authenticated user if (!user.tenantId) { return res.json({ bundleUrl: undefined }); } try { // Call Mockery API server-to-server with your API key const response = await fetch( `${MOCKERY_API_URL}/vendor-api/products/${PRODUCT_SLUG}/tenants/${user.tenantId}/bundle`, { headers: { 'X-API-Key': process.env.MOCKERY_API_KEY, // Keep API keys on server 'Content-Type': 'application/json', }, } ); if (response.ok) { const data = await response.json(); res.json({ bundleUrl: data.bundle_url }); } else { res.json({ bundleUrl: undefined }); } } catch (error) { console.error('Failed to fetch bundle from Mockery:', error); res.json({ bundleUrl: undefined }); } }); ``` ### 3. Use SDK in Frontend ```javascript import { setCustomBundle, clearCustomBundle } from '@mockery/react-csr-sdk'; // Handle user login - call your own backend async function handleUserLogin(credentials) { try { // 1. Authenticate with your backend (your existing auth flow) const authResponse = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials), credentials: 'include' }); if (authResponse.ok) { // 2. Get bundle URL from your backend const bundleResponse = await fetch('/api/user/bundle', { credentials: 'include' }); if (bundleResponse.ok) { const { bundleUrl } = await bundleResponse.json(); // setCustomBundle will exit early if bundleUrl is unchanged setCustomBundle(bundleUrl); // Page reloads only if bundle changed } } } catch (error) { console.error('Login failed:', error); } } // Clear custom bundle on logout async function handleUserLogout() { try { await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); clearCustomBundle(); // Reloads with default bundle } catch (error) { console.error('Logout failed:', error); } } ``` ## API Reference ### Bundle Management #### `setCustomBundle(bundleUrl, reload?)` Sets a custom bundle URL and optionally reloads the page. Exits early (no reload) if the bundle URL is unchanged. ```javascript setCustomBundle('https://cdn.example.com/custom-bundle.js', true); setCustomBundle(undefined); // Clear bundle ``` **Parameters:** - `bundleUrl: string | undefined` - Bundle URL or undefined to clear - `reload: boolean` - Whether to reload page (default: true) #### `getCustomBundle()` Gets the currently set custom bundle URL. ```javascript const currentBundle = getCustomBundle(); // Returns: string | undefined ``` #### `clearCustomBundle(reload?)` Clears the custom bundle and optionally reloads to use the default. ```javascript clearCustomBundle(true); ``` #### `reloadWithBundle(bundleUrl)` Sets a bundle URL and immediately reloads the page. ```javascript reloadWithBundle('https://cdn.example.com/new-bundle.js'); reloadWithBundle(undefined); // Clear and reload ``` ### Utility Functions #### `setDebugMode(enabled)` Enable/disable debug logging. ```javascript setDebugMode(true); // See bundle loading logs ``` ## Integration Patterns ### User Login Flow ```javascript async function handleUserLogin(credentials) { // 1. Authenticate with your backend const user = await authenticateUser(credentials); // 2. Get bundle URL from your backend if (user) { try { const response = await fetch('/api/user/bundle', { credentials: 'include' }); if (response.ok) { const { bundleUrl } = await response.json(); // Only reloads if bundle URL changed setCustomBundle(bundleUrl); } } catch (error) { console.error('Failed to load custom bundle:', error); // App continues with default bundle } } } ``` ### User Logout Flow ```javascript async function handleUserLogout() { // 1. Clear session with your backend await logoutUser(); // 2. Clear custom bundle and reload with default clearCustomBundle(); } ``` ### Tenant Switching ```javascript async function switchTenant(newTenantId) { // 1. Update tenant in your backend await updateUserTenant(newTenantId); // 2. Get new tenant's bundle URL const response = await fetch('/api/user/bundle', { credentials: 'include' }); if (response.ok) { const { bundleUrl } = await response.json(); setCustomBundle(bundleUrl); // Page reloads only if bundle changed } } ``` ### Error Handling ```javascript async function loadUserBundle() { try { const response = await fetch('/api/user/bundle', { credentials: 'include' }); if (response.ok) { const { bundleUrl } = await response.json(); setCustomBundle(bundleUrl); } } catch (error) { console.error('Bundle loading failed:', error); // Optional: Show user notification showNotification('Custom features unavailable, using default app'); // App continues with default bundle - no action needed } } ``` ## Global API (No Module Import Required) If you can't use ES modules, the bundle loader exposes global functions: ```javascript // Available on window.mockery after bundle-loader.js loads window.mockery.setCustomBundle(url); window.mockery.getCustomBundle(); window.mockery.clearCustomBundle(); window.mockery.reloadWithBundle(url); window.mockery.setDebug(enabled); ``` ## How Bundle Replacement Works 1. **Bundle Loader Script**: Runs before any React code 2. **localStorage Check**: Looks for `mockery_bundle_url` 3. **Bundle Decision**: - If custom URL found → Prevents default bundle, loads custom bundle - If no custom URL → Allows default bundle to load normally 4. **Single App**: Only one React application ever starts ## Bundle Requirements Your custom bundles should: - Be complete, standalone React applications - Include all dependencies (React, ReactDOM, etc.) - Mount to the same DOM element (`#root`) - Handle their own routing and state management ## Backend Integration Requirements Your backend endpoint (`/api/user/bundle` or similar) should: 1. **Authenticate the user** (session, JWT, etc.) 2. **Determine user's tenant ID** from your user database 3. **Call Mockery API server-to-server** with your API key 4. **Return bundle URL** to frontend (or undefined if no custom bundle) Example response format: ```json { "bundleUrl": "https://cdn.example.com/tenant-123/bundle.js" } ``` Or if no custom bundle: ```json { "bundleUrl": undefined } ``` ## Security Benefits - **API Keys Protected**: Never exposed to browser - **User Authentication**: Your backend validates identity - **Authorization**: Your backend ensures user can access their tenant - **Audit Trail**: Your backend can log bundle requests - **Rate Limiting**: Your backend can implement limits ## Debugging Enable debug mode to see bundle loading logs: ```javascript import { setDebugMode } from '@mockery/react-csr-sdk'; setDebugMode(true); // Or globally: window.mockery.setDebug(true); ``` This will log: - Bundle URL resolution - Bundle loading attempts - Fallback scenarios - Error details ## TypeScript Support The SDK includes full TypeScript definitions: ```typescript import type { MockeryConfig } from '@mockery/react-csr-sdk'; ``` ## Examples See the `examples/` directory for complete implementation examples. ## Support For issues or questions, please see the Mockery documentation or contact support.