zap/dev-server.js

175 lines
4.9 KiB
JavaScript

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;
}
};
}