UNPKG

npm-mfe-live-reload

Version:

A simple script to reload a microfrontend application

322 lines (278 loc) 10.9 kB
#!/usr/bin/env node const WebSocket = require('ws'); const fs = require('fs'); const path = require('path'); const { promisify } = require('util'); const fsReadFile = promisify(fs.readFile); const fsAccess = promisify(fs.access); const watchedPaths = process.argv.slice(2); if (watchedPaths.length === 0) { console.error('[LiveReloadServer] No paths provided. Example usage:'); console.error('mfe-live-reload -- ../remote-app-1 ../remote-app-2'); process.exit(1); } // Set up WebSocket server const wss = new WebSocket.Server({ port: 42099 }); wss.on('connection', (ws) => { console.log('[LiveReloadClient] Connected to server'); }); // Map of paths with pending builds const pendingBuilds = new Map(); // Debounce function const debounce = (func, wait) => { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; }; // Function to wait for build completion async function waitForBuildCompletion(basePath) { // Wait for a maximum of 30 seconds - as a safety measure const maxWaitTime = 30000; const startTime = Date.now(); const buildStartTime = pendingBuilds.get(basePath); // Directory to check for build artifacts const distDir = path.join(basePath, 'dist'); // Check if the dist directory exists try { await fsAccess(distDir, fs.constants.F_OK); } catch (error) { console.log(`[LiveReloadServer] Dist directory does not exist yet: ${distDir}`); } // Function to recursively search for remoteEntry files async function findRemoteEntryFiles(directory) { try { const files = await promisify(fs.readdir)(directory, { withFileTypes: true }); const remoteEntries = []; for (const file of files) { const filePath = path.join(directory, file.name); if (file.isDirectory()) { // Recursively search subdirectories const subDirEntries = await findRemoteEntryFiles(filePath); remoteEntries.push(...subDirEntries); } else if (file.isFile() && (file.name === 'remoteEntry.json' || file.name === 'remoteEntry.js')) { // Found a remote entry file remoteEntries.push(filePath); } } return remoteEntries; } catch (error) { console.error(`[LiveReloadServer] Error searching directory ${directory}:`, error); return []; } } // Check every 500ms if any remote entry files have been created or modified while (Date.now() - startTime < maxWaitTime) { try { // First, check if the dist directory exists await fsAccess(distDir, fs.constants.F_OK); // Find all remoteEntry files in the dist directory (at any depth) const remoteEntryFiles = await findRemoteEntryFiles(distDir); if (remoteEntryFiles.length > 0) { // Check modification times of all found remote entry files for (const remoteEntryFile of remoteEntryFiles) { const stats = await promisify(fs.stat)(remoteEntryFile); const modifiedTime = stats.mtimeMs; // If the file was modified after the build started, we can assume build is complete if (modifiedTime > buildStartTime) { console.log(`[LiveReloadServer] Build completed`); return true; } } } } catch (error) { // Dist directory may not exist yet, continue waiting } // Wait before checking again await new Promise(resolve => setTimeout(resolve, 500)); } console.log('[LiveReloadServer] Build timed out after 30 seconds'); return false; // Timed out, but we'll still reload } // Create a debounced reload function that waits for build completion const triggerReload = debounce(async (changedPath) => { const basePath = watchedPaths.map(p => path.resolve(p)) .find(resolvedPath => changedPath.startsWith(resolvedPath)); if (!basePath) { console.error(`[LiveReloadServer] Could not determine base path for ${changedPath}`); return; } // Record the time when the build started pendingBuilds.set(basePath, Date.now()); // Wait for the build to complete console.log(`[LiveReloadServer] File changed: ${changedPath}`); console.log(`[LiveReloadServer] Waiting for build to complete...`); try { await waitForBuildCompletion(basePath); // Build is complete, now trigger the reload console.log('[LiveReloadServer] Triggering reload'); wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify({ type: 'reload', path: changedPath, timestamp: Date.now() })); } }); } catch (error) { console.error('[LiveReloadServer] Error while waiting for build:', error); } finally { pendingBuilds.delete(basePath); } }, 300); // Function to recursively watch directories function watchDirectory(dir, callback) { try { if (!fs.existsSync(dir)) { console.error(`[LiveReloadServer] Directory does not exist: ${dir}`); return; } // Watch for changes in this directory const watcher = fs.watch(dir, { recursive: false }, (eventType, filename) => { if (!filename) return; const fullPath = path.join(dir, filename); // Only watch source files that would trigger a build if (!/\.(ts|html|scss|css|js)$/.test(fullPath) || /\.spec\.(ts|js)$/.test(fullPath) || filename.includes('node_modules') || filename.includes('dist') || filename.includes('.angular')) { return; } callback(fullPath); }); // Recursively watch subdirectories fs.readdir(dir, { withFileTypes: true }, (err, files) => { if (err) { console.error(`[LiveReloadServer] Error reading directory ${dir}:`, err); return; } files.forEach(file => { if (file.isDirectory() && !file.name.includes('node_modules') && !file.name.includes('.git') && !file.name.includes('dist') && !file.name.includes('.angular') && !file.name.includes('coverage')) { watchDirectory(path.join(dir, file.name), callback); } }); }); return watcher; } catch (error) { console.error(`[LiveReloadServer] Error watching directory ${dir}:`, error); } } // Watch each provided path const watchers = []; watchedPaths.forEach(watchPath => { const resolved = path.resolve(watchPath); // Check for different directory structures let dirsToWatch = []; // Standard structure: direct src folder const standardSrcPath = path.join(resolved, 'src'); // Monorepo structures const appsDir = path.join(resolved, 'apps'); const libsDir = path.join(resolved, 'libs'); const packagesDir = path.join(resolved, 'packages'); const projectsDir = path.join(resolved, 'projects'); if (fs.existsSync(standardSrcPath)) { // Standard structure dirsToWatch.push(standardSrcPath); console.log(`[LiveReloadServer] Found standard src directory: ${standardSrcPath}`); } else if (fs.existsSync(appsDir) || fs.existsSync(libsDir) || fs.existsSync(packagesDir) || fs.existsSync(projectsDir)) { // Monorepo structure detected console.log(`[LiveReloadServer] Detected monorepo structure in: ${resolved}`); // Process apps directory if (fs.existsSync(appsDir)) { try { const apps = fs.readdirSync(appsDir, { withFileTypes: true }); apps.forEach(app => { if (app.isDirectory()) { const appSrcPath = path.join(appsDir, app.name, 'src'); if (fs.existsSync(appSrcPath)) { dirsToWatch.push(appSrcPath); console.log(`[LiveReloadServer] Found app src: ${appSrcPath}`); } } }); } catch (error) { console.error(`[LiveReloadServer] Error reading apps directory: ${error.message}`); } } // Process libs directory if (fs.existsSync(libsDir)) { try { const libs = fs.readdirSync(libsDir, { withFileTypes: true }); libs.forEach(lib => { if (lib.isDirectory()) { const libSrcPath = path.join(libsDir, lib.name, 'src'); if (fs.existsSync(libSrcPath)) { dirsToWatch.push(libSrcPath); console.log(`[LiveReloadServer] Found lib src: ${libSrcPath}`); } } }); } catch (error) { console.error(`[LiveReloadServer] Error reading libs directory: ${error.message}`); } } // Process packages directory (common in Lerna/Yarn workspaces) if (fs.existsSync(packagesDir)) { try { const packages = fs.readdirSync(packagesDir, { withFileTypes: true }); packages.forEach(pkg => { if (pkg.isDirectory()) { const pkgSrcPath = path.join(packagesDir, pkg.name, 'src'); if (fs.existsSync(pkgSrcPath)) { dirsToWatch.push(pkgSrcPath); console.log(`[LiveReloadServer] Found package src: ${pkgSrcPath}`); } } }); } catch (error) { console.error(`[LiveReloadServer] Error reading packages directory: ${error.message}`); } } // Process projects directory (Angular workspace) if (fs.existsSync(projectsDir)) { try { const projects = fs.readdirSync(projectsDir, { withFileTypes: true }); projects.forEach(project => { if (project.isDirectory()) { const projectSrcPath = path.join(projectsDir, project.name, 'src'); if (fs.existsSync(projectSrcPath)) { dirsToWatch.push(projectSrcPath); console.log(`[LiveReloadServer] Found project src: ${projectSrcPath}`); } } }); } catch (error) { console.error(`[LiveReloadServer] Error reading projects directory: ${error.message}`); } } } // If we couldn't find any src directories, watch the root as a fallback if (dirsToWatch.length === 0) { console.log(`[LiveReloadServer] No src directories found. Watching root: ${resolved}`); dirsToWatch.push(resolved); } // Set up watchers for all identified directories dirsToWatch.forEach(dirToWatch => { console.log(`[LiveReloadServer] Watching: ${dirToWatch}`); const watcher = watchDirectory(dirToWatch, triggerReload); if (watcher) watchers.push(watcher); }); }); // Handle process termination process.on('SIGINT', () => { console.log('[LiveReloadServer] Stopping server...'); watchers.forEach(watcher => watcher.close()); wss.close(); process.exit(0); }); console.log('[LiveReloadServer] Server started on port 42099');