@jakobcooldown/react-csr-sdk
Version:
Mockery SDK for dynamic bundle loading in web applications
386 lines (293 loc) • 10.4 kB
Markdown
# 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.