import { spawn } from 'child_process'; import { resolve } from 'path'; import { normalizePath } from 'vite'; import picomatch from 'picomatch'; // Build queue to prevent overlapping builds class BuildQueue { constructor() { this.queue = []; this.pending = false; } enqueue(buildFn) { return new Promise((resolve, reject) => { this.queue.push({ buildFn, resolve, reject }); this.process(); }); } async process() { if (this.pending || this.queue.length === 0) return; this.pending = true; const { buildFn, resolve, reject } = this.queue.shift(); try { await buildFn(); resolve(); } catch (error) { reject(error); } finally { this.pending = false; this.process(); } } } const buildQueue = new BuildQueue(); export default function hotReloadPlugin(options = {}) { const config = { // Files to watch for changes watchFiles: [ "./site/**/*.blade.php", "./site/**/*.md", "./site/**/*.html", "./config/**/*.php", "./app/**/*.php", "./zap.yml" ], // Files to ignore ignoreFiles: [ 'node_modules/**', 'build_local/**', '.git/**', 'vendor/**' ], // Build command buildCommand: 'php zap build', // Delay before refresh (ms) refreshDelay: 100, // Always do full page reload always: true, // Override with user options ...options }; function runSiteBuild() { return new Promise((resolve, reject) => { const [command, ...args] = config.buildCommand.split(' '); const build = spawn(command, args, { stdio: 'inherit', shell: true }); build.on('exit', (code) => { if (code === 0) resolve(); else reject(new Error(`Build failed with code ${code}`)); }); }); } return { name: 'static-site-hot-reload', config() { return { server: { watch: { disableGlobbing: false, ignored: config.ignoreFiles.map(pattern => resolve(process.cwd(), pattern) ).map(normalizePath) } } }; }, configureServer(server) { // Normalize file paths const watchFiles = config.watchFiles.map(pattern => resolve(process.cwd(), pattern) ).map(normalizePath); const shouldReload = picomatch(watchFiles); // Add files to Vite's watcher server.watcher.add(watchFiles); const handleFileChange = async (filePath) => { if (!shouldReload(filePath)) return; const start = performance.now(); try { // Queue the build to prevent overlaps await buildQueue.enqueue(() => runSiteBuild()); const duration = Math.round(performance.now() - start); // Delay the refresh setTimeout(() => { server.config.logger.info( `full reload for ${filePath} - build: ${duration}ms`, { timestamp: true, clear: true } ); // Send full reload command server.ws.send({ type: 'full-reload', path: config.always ? '*' : filePath }); }, config.refreshDelay); } catch (error) { server.config.logger.error(`Build failed: ${error.message}`); } }; // Listen for file changes server.watcher.on('add', handleFileChange); server.watcher.on('change', handleFileChange); // Initial build runSiteBuild().then(() => { server.config.logger.info('Initial build completed'); }).catch((error) => { server.config.logger.error(`Initial build failed: ${error.message}`); }); }, // Handle hot updates handleHotUpdate({ file }) { const watchFiles = config.watchFiles.map(pattern => resolve(process.cwd(), pattern) ).map(normalizePath); const shouldReload = picomatch(watchFiles); if (shouldReload(file)) { // Return empty array to prevent HMR, we'll do full reload instead return []; } // Let Vite handle other files normally return undefined; } }; }