UNPKG

clovie

Version:

Vintage web dev tooling with modern quality of life

284 lines (236 loc) 8.95 kB
import chokidar from 'chokidar'; import path from 'path'; import fs from 'fs'; import { BuildCache } from './cache.js'; export class SmartWatcher { constructor(clovieInstance) { this.clovie = clovieInstance; this.watchers = []; this.cache = new BuildCache(clovieInstance.config.outputDir); this.debounceTimer = null; this.isWatching = false; } start() { if (this.isWatching) return; console.log('👀 Starting smart file watcher...'); this.isWatching = true; // Watch views directory if (this.clovie.config.views) { const viewsWatcher = chokidar.watch(this.clovie.config.views, { ignored: /(^|[\/\\])\../, // Ignore hidden files persistent: true }); viewsWatcher.on('change', (filePath) => this.handleViewChange(filePath)); viewsWatcher.on('add', (filePath) => this.handleViewChange(filePath)); viewsWatcher.on('unlink', (filePath) => this.handleViewChange(filePath)); this.watchers.push(viewsWatcher); } // Watch partials directory if (this.clovie.config.partials) { const partialsWatcher = chokidar.watch(this.clovie.config.partials, { ignored: /(^|[\/\\])\../, // Ignore hidden files persistent: true }); partialsWatcher.on('change', (filePath) => this.handlePartialChange(filePath)); partialsWatcher.on('add', (filePath) => this.handlePartialChange(filePath)); partialsWatcher.on('unlink', (filePath) => this.handlePartialChange(filePath)); this.watchers.push(partialsWatcher); } // Watch scripts directory if (this.clovie.config.scriptsDir) { const scriptsWatcher = chokidar.watch(this.clovie.config.scriptsDir, { ignored: /(^|[\/\\])\../, persistent: true }); scriptsWatcher.on('change', (filePath) => this.handleScriptChange(filePath)); scriptsWatcher.on('add', (filePath) => this.handleScriptChange(filePath)); scriptsWatcher.on('unlink', (filePath) => this.handleScriptChange(filePath)); this.watchers.push(scriptsWatcher); } // Watch styles directory if (this.clovie.config.stylesDir) { const stylesWatcher = chokidar.watch(this.clovie.config.stylesDir, { ignored: /(^|[\/\\])\../, persistent: true }); stylesWatcher.on('change', (filePath) => this.handleStyleChange(filePath)); stylesWatcher.on('add', (filePath) => this.handleStyleChange(filePath)); stylesWatcher.on('unlink', (filePath) => this.handleStyleChange(filePath)); this.watchers.push(stylesWatcher); } // Watch assets directory if (this.clovie.config.assets) { const assetsWatcher = chokidar.watch(this.clovie.config.assets, { ignored: /(^|[\/\\])\../, persistent: true }); assetsWatcher.on('change', (filePath) => this.handleAssetChange(filePath)); assetsWatcher.on('add', (filePath) => this.handleAssetChange(filePath)); assetsWatcher.on('unlink', (filePath) => this.handleAssetChange(filePath)); this.watchers.push(assetsWatcher); } console.log('✅ Smart watcher started'); } stop() { this.watchers.forEach(watcher => { watcher.close(); }); this.watchers = []; this.isWatching = false; console.log('🛑 Smart watcher stopped'); } // Debounced rebuild to avoid multiple rapid rebuilds scheduleRebuild(type, filePath) { clearTimeout(this.debounceTimer); this.debounceTimer = setTimeout(() => { this.rebuild(type, filePath); }, 100); // 100ms debounce } async rebuild(type, filePath) { const startTime = Date.now(); console.log(`🔄 Rebuilding due to ${type} change: ${path.basename(filePath)}`); try { // Only rebuild what's necessary based on what changed switch (type) { case 'partial': case 'view': await this.rebuildViews(); break; case 'script': await this.rebuildScripts(); break; case 'style': await this.rebuildStyles(); break; case 'asset': await this.rebuildAssets(); break; } this.cache.markBuilt(); const buildTime = Date.now() - startTime; console.log(`✅ Incremental rebuild completed in ${buildTime}ms`); // Trigger live reload if callback is set if (this.onRebuild) { this.onRebuild(); } } catch (err) { console.error(`❌ Incremental rebuild failed:`, err); } } async rebuildViews() { // For now, just trigger a full rebuild of views since incremental is complex console.log(' Triggering full view rebuild...'); try { // Re-process all views const getViews = await import('./getViews.js'); const viewsResult = getViews.default(this.clovie.config.views, this.clovie.config.models, this.clovie.data, this.clovie.config.partials); this.clovie.views = viewsResult.pages; this.clovie.partials = viewsResult.partials; // Convert to the format expected by render (same as main build process) this.clovie.processedViews = {}; for (const [key, value] of Object.entries(this.clovie.views)) { if (value && value.template) { // Use filename from model processing, or generate default const fileName = value.filename || key.replace(/\.[^/.]+$/, '.html'); this.clovie.processedViews[fileName] = value; } } // Re-render all views using the render function const render = await import('./render.js'); this.clovie.rendered = await render.default( this.clovie.processedViews, this.clovie.config.compiler, Object.keys(this.clovie.processedViews), this.clovie.isDevMode, this.clovie.partials, this.clovie.config.register ); // Write the updated views const write = await import('./write.js'); write.default(this.clovie.rendered, this.clovie.config.outputDir); console.log(' Views rebuilt successfully'); } catch (err) { console.error(' View rebuild failed:', err); throw err; } } async rebuildScripts() { if (!this.clovie.config.scripts) return; console.log(' Rebuilding scripts...'); try { const bundler = await import('./bundler.js'); this.clovie.scripts = await bundler.default(this.clovie.config.scripts); const write = await import('./write.js'); write.default(this.clovie.scripts, this.clovie.config.outputDir); console.log(' Scripts rebuilt successfully'); } catch (err) { console.error(' Script rebuild failed:', err); throw err; } } async rebuildStyles() { if (!this.clovie.config.styles) return; console.log(' Rebuilding styles...'); try { const getStyles = await import('./getStyles.js'); this.clovie.styles = getStyles.default(this.clovie.config.styles); const write = await import('./write.js'); write.default(this.clovie.styles, this.clovie.config.outputDir); console.log(' Styles rebuilt successfully'); } catch (err) { console.error(' Style rebuild failed:', err); throw err; } } async rebuildAssets() { if (!this.clovie.config.assets) return; console.log(' Rebuilding assets...'); try { const getAssets = await import('./getAssets.js'); this.clovie.assets = getAssets.default(this.clovie.config.assets); const write = await import('./write.js'); write.default(this.clovie.assets, this.clovie.config.outputDir); console.log(' Assets rebuilt successfully'); } catch (err) { console.error(' Asset rebuild failed:', err); throw err; } } getViewFiles(viewsDir) { // Get all view files recursively const files = []; const scanDir = (dir) => { try { const items = fs.readdirSync(dir); for (const item of items) { const fullPath = path.join(dir, item); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { scanDir(fullPath); } else if (path.extname(item) === '.html') { files.push(fullPath); } } } catch (err) { // Ignore errors } }; scanDir(viewsDir); return files; } handleViewChange(filePath) { this.scheduleRebuild('view', filePath); } handlePartialChange(filePath) { this.scheduleRebuild('partial', filePath); } handleScriptChange(filePath) { this.scheduleRebuild('script', filePath); } handleStyleChange(filePath) { this.scheduleRebuild('style', filePath); } handleAssetChange(filePath) { this.scheduleRebuild('asset', filePath); } }