const fs = require('fs') const path = require('path') const { spawn } = require('child_process') const rootDir = path.resolve(__dirname, '..') const ignoredDirectories = new Set([ '.git', '.cursor', '.vscode', '.docker', 'docs', 'local-ghost', 'node_modules', 'seed', 'theme-export' ]) const ignoredFiles = new Set([ '.DS_Store' ]) const syncExtensions = new Set([ '.css', '.hbs', '.js', '.json', '.svg', '.yaml', '.yml' ]) const watchedRoots = [rootDir] let syncTimer = null let syncProcess = null let scanTimer = null let previousSnapshot = new Map() /** * @param {string} relativePath */ function shouldIgnorePath(relativePath) { if (!relativePath || relativePath === '.') { return false } const segments = relativePath.split(path.sep) return segments.some((segment) => ignoredDirectories.has(segment) || ignoredFiles.has(segment)) } /** * @param {string} relativePath */ function shouldSyncFile(relativePath) { if (shouldIgnorePath(relativePath)) { return false } if (relativePath === path.join('assets', 'styles', 'tailwind.css')) { return false } const extension = path.extname(relativePath) return syncExtensions.has(extension) } /** * @param {string} command * @param {string[]} args * @param {(code: number | null) => void} [onExit] */ function runCommand(command, args, onExit) { const child = spawn(command, args, { cwd: rootDir, stdio: 'inherit', shell: false }) child.on('exit', (code) => { if (onExit) { onExit(code) } }) return child } function queueSync() { if (syncTimer) { clearTimeout(syncTimer) } syncTimer = setTimeout(() => { if (syncProcess) { queueSync() return } console.log('[dev:watch] theme sync') syncProcess = runCommand('npm', ['run', 'dev:sync'], (code) => { syncProcess = null if (code !== 0) { console.error(`[dev:watch] sync failed with code ${code}`) } }) }, 180) } /** * @param {string} directoryPath * @param {Map} snapshot */ function collectSnapshot(directoryPath, snapshot) { if (!fs.existsSync(directoryPath)) { return } for (const entry of fs.readdirSync(directoryPath, { withFileTypes: true })) { const entryPath = path.join(directoryPath, entry.name) const relativePath = path.relative(rootDir, entryPath) if (shouldIgnorePath(relativePath)) { continue } if (entry.isDirectory()) { collectSnapshot(entryPath, snapshot) continue } if (shouldSyncFile(relativePath)) { snapshot.set(relativePath, fs.statSync(entryPath).mtimeMs) } } } function buildSnapshot() { const snapshot = new Map() for (const watchedRoot of watchedRoots) { collectSnapshot(watchedRoot, snapshot) } return snapshot } function scanForChanges() { const nextSnapshot = buildSnapshot() for (const [relativePath, modifiedTime] of nextSnapshot.entries()) { if (previousSnapshot.get(relativePath) !== modifiedTime) { console.log(`[dev:watch] change detected: ${relativePath}`) queueSync() previousSnapshot = nextSnapshot return } } for (const relativePath of previousSnapshot.keys()) { if (!nextSnapshot.has(relativePath)) { console.log(`[dev:watch] change detected: ${relativePath}`) queueSync() previousSnapshot = nextSnapshot return } } previousSnapshot = nextSnapshot } function startPolling() { previousSnapshot = buildSnapshot() scanTimer = setInterval(scanForChanges, 800) } function shutdown() { if (syncTimer) { clearTimeout(syncTimer) } if (tailwindProcess && !tailwindProcess.killed) { tailwindProcess.kill('SIGINT') } if (syncProcess && !syncProcess.killed) { syncProcess.kill('SIGINT') } if (scanTimer) { clearInterval(scanTimer) } process.exit(0) } console.log('[dev:watch] initial prepare') runCommand('npm', ['run', 'dev:prepare'], (prepareCode) => { if (prepareCode !== 0) { process.exit(prepareCode || 1) } console.log('[dev:watch] tailwind watch start') tailwindProcess = runCommand('npm', ['run', 'build:tailwind', '--', '--watch']) console.log('[dev:watch] filesystem watch start') startPolling() }) let tailwindProcess = null process.on('SIGINT', shutdown) process.on('SIGTERM', shutdown)