import * as plugins from './tswatch.plugins.js'; import * as paths from './tswatch.paths.js'; import * as interfaces from './interfaces/index.js'; import { Watcher } from './tswatch.classes.watcher.js'; import { ConfigHandler } from './tswatch.classes.confighandler.js'; import { logger } from './tswatch.logging.js'; /** * TsWatch - Config-driven file watcher * * Reads configuration from npmextra.json under the key '@git.zone/tswatch' * and sets up watchers, bundles, and dev server accordingly. */ export class TsWatch { public config: interfaces.ITswatchConfig; public watcherMap = new plugins.lik.ObjectMap(); public typedserver: plugins.typedserver.TypedServer | null = null; private tsbundle = new plugins.tsbundle.TsBundle(); private htmlHandler = new plugins.tsbundle.HtmlHandler(); private assetsHandler = new plugins.tsbundle.AssetsHandler(); constructor(configArg: interfaces.ITswatchConfig) { this.config = configArg; } /** * Create TsWatch from npmextra.json configuration */ public static fromConfig(cwdArg?: string): TsWatch | null { const configHandler = new ConfigHandler(cwdArg); const config = configHandler.loadConfig(); if (!config) { return null; } return new TsWatch(config); } /** * starts the TsWatch instance */ public async start() { logger.log('info', 'Starting tswatch with config-driven mode'); // Start server if configured if (this.config.server?.enabled) { await this.startServer(); } // Setup bundles and their watchers if (this.config.bundles && this.config.bundles.length > 0) { await this.setupBundles(); } // Setup watchers from config if (this.config.watchers && this.config.watchers.length > 0) { await this.setupWatchers(); } // Start all watchers await this.watcherMap.forEach(async (watcher) => { await watcher.start(); }); // Start server after watchers are ready if (this.typedserver) { await this.typedserver.start(); logger.log('ok', `Dev server started on port ${this.config.server?.port || 3002}`); } } /** * Start the development server */ private async startServer() { const serverConfig = this.config.server!; const port = serverConfig.port || 3002; const serveDir = serverConfig.serveDir || './dist_watch/'; logger.log('info', `Setting up dev server on port ${port}, serving ${serveDir}`); this.typedserver = new plugins.typedserver.TypedServer({ cors: true, injectReload: serverConfig.liveReload !== false, serveDir: plugins.path.join(paths.cwd, serveDir), port: port, compression: true, spaFallback: true, noCache: true, securityHeaders: { crossOriginOpenerPolicy: 'same-origin', crossOriginEmbedderPolicy: 'require-corp', }, }); } /** * Setup bundle watchers */ private async setupBundles() { for (const bundleConfig of this.config.bundles!) { const name = bundleConfig.name || `bundle-${bundleConfig.from}`; logger.log('info', `Setting up bundle: ${name}`); // Determine what patterns to watch const watchPatterns = bundleConfig.watchPatterns || [ plugins.path.dirname(bundleConfig.from) + '/**/*', ]; // Create the bundle function const bundleFunction = async () => { logger.log('info', `[${name}] bundling...`); // Determine bundle type based on file extension const fromPath = bundleConfig.from; const toPath = bundleConfig.to; if (fromPath.endsWith('.html')) { // HTML processing await this.htmlHandler.processHtml({ from: plugins.path.join(paths.cwd, fromPath), to: plugins.path.join(paths.cwd, toPath), minify: false, }); } else if (fromPath.endsWith('/') || !fromPath.includes('.')) { // Assets directory copy await this.assetsHandler.processAssets(); } else { // TypeScript bundling await this.tsbundle.build(paths.cwd, fromPath, toPath, { bundler: 'esbuild', }); } logger.log('ok', `[${name}] bundle complete`); // Trigger reload if configured and server is running if (bundleConfig.triggerReload !== false && this.typedserver) { await this.typedserver.reload(); } }; // Run initial bundle await bundleFunction(); // Create watcher for this bundle this.watcherMap.add( new Watcher({ name: name, filePathToWatch: watchPatterns.map((p) => plugins.path.join(paths.cwd, p)), functionToCall: bundleFunction, runOnStart: false, // Already ran above debounce: 300, }), ); } } /** * Setup watchers from config */ private async setupWatchers() { for (const watcherConfig of this.config.watchers!) { logger.log('info', `Setting up watcher: ${watcherConfig.name}`); // Convert watch paths to absolute const watchPaths = Array.isArray(watcherConfig.watch) ? watcherConfig.watch : [watcherConfig.watch]; const absolutePaths = watchPaths.map((p) => plugins.path.join(paths.cwd, p)); this.watcherMap.add( new Watcher({ name: watcherConfig.name, filePathToWatch: absolutePaths, commandToExecute: watcherConfig.command, restart: watcherConfig.restart ?? true, debounce: watcherConfig.debounce ?? 300, runOnStart: watcherConfig.runOnStart ?? true, }), ); } } /** * stops the execution of any active Watchers */ public async stop() { if (this.typedserver) { await this.typedserver.stop(); } await this.watcherMap.forEach(async (watcher) => { await watcher.stop(); }); } }