npm-mfe-live-reload
Version:
A simple script to reload a microfrontend application
322 lines (278 loc) • 10.9 kB
JavaScript
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');