create-bluecopa-react-app
Version:
CLI tool to create bluecopa React applications
361 lines (322 loc) • 13.5 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bluecopa Preview</title>
<meta name="description" content="Preview wrapper for federation module" />
<style>
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
font-size: 16px;
color: #666;
}
.error {
padding: 20px;
background: #fee;
border: 1px solid #fcc;
border-radius: 8px;
margin: 20px;
}
.error h2 {
color: #c33;
margin-top: 0;
}
.error code {
background: #f5f5f5;
padding: 2px 4px;
border-radius: 3px;
font-family: monospace;
}
#bluecopa-preview {
width: 100%;
min-height: 500px;
}
</style>
</head>
<body>
<div id="loading-indicator" class="loading">
Loading Bluecopa module...
</div>
<div id="error-container" style="display: none;"></div>
<div id="bluecopa-preview"></div>
<!-- SystemJS import map for shared dependencies (UMD for compatibility) -->
<script type="systemjs-importmap">
{
"imports": {
"react": "https://unpkg.com/react@18/umd/react.development.js",
"react-dom": "https://unpkg.com/react-dom@18/umd/react-dom.development.js",
"react-router-dom": "https://unpkg.com/react-router-dom@6/dist/umd/react-router-dom.development.js",
"single-spa": "https://unpkg.com/single-spa@5/dist/system/single-spa.min.js",
"single-spa-react": "https://unpkg.com/single-spa-react@4/dist/umd/single-spa-react.min.js"
}
}
</script>
<!-- SystemJS for loading System format bluecopa modules -->
<script src="https://unpkg.com/systemjs@6/dist/system.min.js"></script>
<script type="module">
// Configure SystemJS with import map after load
document.addEventListener('DOMContentLoaded', async () => {
if (window.System) {
const importMap = document.querySelector('script[type="systemjs-importmap"]');
if (importMap) {
window.System.addImportMap(JSON.parse(importMap.textContent));
}
// Auto-load on page open
await loadFederationModule();
}
});
let currentModule = null;
let currentMountProps = null;
// Helper to load remoteEntry.js script (no wait needed, as System.import will handle)
async function loadRemoteEntryScript(moduleUrl) {
return new Promise((resolve, reject) => {
// Remove any previous script
const existingScript = document.querySelector(`script[src='${moduleUrl}']`);
if (existingScript) existingScript.remove();
// Create new script
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = moduleUrl;
script.async = true;
script.onload = () => resolve();
script.onerror = () => reject(new Error('Failed to load remoteEntry.js'));
document.head.appendChild(script);
});
}
// Global function to load federation module
window.loadFederationModule = async function() {
// Hardcoded defaults
const moduleUrl = "http://localhost:3001/assets/remoteEntry.js";
const moduleName = "__copa_ext_app__react_route";
const exposeName = "./App";
const basePath = "/preview";
const loadingIndicator = document.getElementById('loading-indicator');
const errorContainer = document.getElementById('error-container');
const previewContainer = document.getElementById('bluecopa-preview');
// Show loading state
loadingIndicator.style.display = 'flex';
loadingIndicator.textContent = 'Loading bluecopa module...';
errorContainer.style.display = 'none';
previewContainer.innerHTML = '';
try {
// Unload any existing module first
await unloadModule();
console.log('Loading federation module from:', moduleUrl);
// Load remoteEntry script
await loadRemoteEntryScript(moduleUrl);
// Clear System cache for fresh load
if (window.System && typeof window.System.delete === 'function') {
window.System.delete(moduleUrl);
}
// Use SystemJS to import the container with timeout
if (typeof System !== 'undefined') {
const container = await Promise.race([
System.import(moduleUrl),
new Promise((_, reject) => setTimeout(() => reject(new Error('Module load timeout')), 15000))
]);
console.log('Loaded container:', container);
// Try to get the exposed module with multiple entry points
let appModule;
if (typeof container.get === 'function') {
const entryPoints = ['./App', './app', './index', './main', './src/App', './src/index'];
for (const entry of entryPoints) {
try {
const factory = await container.get(entry);
appModule = factory ? factory() : factory;
console.log(`Found module at ${entry}:`, appModule);
break;
} catch (e) {
console.log(`No module at ${entry}`);
}
}
}
if (!appModule) {
appModule = container;
}
await mountModule(appModule, basePath, loadingIndicator, previewContainer, container);
} else {
throw new Error('SystemJS not available');
}
} catch (error) {
// Show error visibly on the page
loadingIndicator.style.display = 'none';
errorContainer.style.display = 'block';
errorContainer.innerHTML = `
<div class="error">
<h2>Failed to Load Application</h2>
<p>${error.message}</p>
<ul>
<li>Make sure the federation build is running: <code>npm run preview</code></li>
<li>Verify the module URL points to the correct remoteEntry.js</li>
<li>Check that module name matches your vite.config.ts federation name</li>
<li>Open browser DevTools Console for detailed error information</li>
<li>Ensure CORS is enabled on your federation server</li>
</ul>
<p>Refresh the page to retry.</p>
</div>
`;
// Also log to console
console.error('bluecopa preview error:', error);
}
};
// Helper function to mount the module
async function mountModule(appModule, basePath, loadingIndicator, previewContainer, container) {
// Create a container for the module
const moduleContainer = document.createElement('div');
moduleContainer.id = 'single-spa-application:bluecopa-preview';
moduleContainer.style.width = '100%';
moduleContainer.style.minHeight = '500px';
previewContainer.appendChild(moduleContainer);
console.log('App module:', appModule);
console.log('App module type:', typeof appModule);
let mounted = false;
// Initialize container if needed
if (typeof container.init === 'function') {
try {
await container.init({});
} catch (e) {
console.warn('Container init failed:', e);
}
}
// Try multiple mount patterns
const mountFunc = appModule?.mount ||
appModule?.default?.mount ||
appModule?.render ||
appModule?.default?.render ||
appModule?.init ||
appModule?.default?.init ||
appModule?.bootstrap ||
appModule?.default?.bootstrap ||
appModule?.start ||
appModule?.default?.start ||
(typeof appModule === 'function' ? appModule : null);
if (typeof mountFunc === 'function') {
console.log('Found mount function, mounting app...');
// Clear container
moduleContainer.innerHTML = '';
try {
// Pattern 1: Single-spa config
const props = {
domElement: moduleContainer,
basename: basePath,
singleSpa: {
name: 'bluecopa-preview',
mountParcel: () => {},
getProps: () => ({})
}
};
await mountFunc(props);
mounted = true;
} catch (e1) {
console.log('Single-spa pattern failed:', e1);
try {
// Pattern 2: Direct element
await mountFunc(moduleContainer);
mounted = true;
} catch (e2) {
console.log('Direct element pattern failed:', e2);
try {
// Pattern 3: No args
await mountFunc();
mounted = true;
} catch (e3) {
console.error('All mount patterns failed:', { e1, e2, e3 });
throw new Error('Failed to mount application with any pattern');
}
}
}
if (mounted) {
currentModule = { unmount: async () => await unmountModule(appModule, moduleContainer), container };
currentMountProps = { domElement: moduleContainer, basename: basePath };
loadingIndicator.style.display = 'none';
console.log('Bluecopa module mounted successfully');
return;
}
}
// Fallback: Treat as plain React component
if (typeof appModule === 'function') {
console.log('Mounting as plain React component');
const { default: React } = await import('react');
const { createRoot } = await import('react-dom/client');
const root = createRoot(moduleContainer);
root.render(React.createElement(appModule, { basename: basePath }));
currentModule = { unmount: () => root.unmount(), container };
loadingIndicator.style.display = 'none';
console.log('React component mounted successfully');
return;
}
throw new Error('No valid mount function or React component found');
}
// Helper to unmount
async function unmountModule(appModule, moduleContainer) {
// Try multiple unmount patterns
const unmountFunc = appModule?.unmount ||
appModule?.default?.unmount ||
appModule?.destroy ||
appModule?.default?.destroy ||
appModule?.cleanup ||
appModule?.default?.cleanup ||
appModule?.dispose ||
appModule?.default?.dispose;
if (typeof unmountFunc === 'function') {
try {
// Try with props if available
if (currentMountProps) {
await unmountFunc(currentMountProps);
} else {
await unmountFunc();
}
console.log('Module unmounted via function');
return;
} catch (e) {
console.warn('Unmount function failed:', e);
}
}
// Fallback manual cleanup
if (moduleContainer) {
moduleContainer.innerHTML = '';
}
console.log('Module cleaned up manually');
}
// Global function to unload module
window.unloadModule = async function() {
if (currentModule) {
try {
if (currentModule.container && typeof currentModule.container.delete === 'function') {
currentModule.container.delete(currentModule.container.url || ''); // Clear cache
}
await unmountModule(currentModule.appModule || currentModule, document.getElementById('bluecopa-preview'));
} catch (error) {
console.error('Error unmounting module:', error);
}
}
currentModule = null;
currentMountProps = null;
const previewContainer = document.getElementById('bluecopa-preview');
previewContainer.innerHTML = '';
const loadingIndicator = document.getElementById('loading-indicator');
loadingIndicator.style.display = 'flex';
loadingIndicator.textContent = 'Ready to load bluecopa module...';
const errorContainer = document.getElementById('error-container');
errorContainer.style.display = 'none';
};
// Global function to reload module
window.reloadModule = async function() {
await unloadModule();
// Clear any remaining cache
if (window.System && typeof window.System.clear === 'function') {
window.System.clear();
}
// Small delay to ensure cleanup
setTimeout(() => {
loadFederationModule();
}, 100);
};
</script>
</body>
</html>