BREAKING CHANGE(tswatch): refactor tswatch to a config-driven design (load config from npmextra.json) and add interactive init wizard; change TsWatch public API and enhance Watcher behavior

This commit is contained in:
2026-01-24 11:29:55 +00:00
parent 5a702055f4
commit 9fb3f6a872
20 changed files with 3686 additions and 4431 deletions

View File

@@ -3,221 +3,185 @@ 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';
// Create smartfs instance for directory operations
const smartfs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
/**
* Lists all folders in a directory
* 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.
*/
const listFolders = async (dirPath: string): Promise<string[]> => {
const entries = await smartfs.directory(dirPath).list();
return entries
.filter((entry) => entry.isDirectory)
.map((entry) => entry.name);
};
export class TsWatch {
public watchmode: interfaces.TWatchModes;
public config: interfaces.ITswatchConfig;
public watcherMap = new plugins.lik.ObjectMap<Watcher>();
public typedserver: plugins.typedserver.TypedServer;
public typedserver: plugins.typedserver.TypedServer | null = null;
constructor(watchmodeArg: interfaces.TWatchModes) {
this.watchmode = watchmodeArg;
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() {
const tsbundle = new plugins.tsbundle.TsBundle();
const assetsHandler = new plugins.tsbundle.AssetsHandler();
const htmlHandler = new plugins.tsbundle.HtmlHandler();
switch (this.watchmode) {
case 'test':
/**
* this strategy runs test whenever there is a change in the ts directory
*/
this.watcherMap.add(
new Watcher({
filePathToWatch: paths.cwd,
commandToExecute: 'npm run test2',
timeout: null,
}),
);
break;
case 'node':
this.watcherMap.add(
new Watcher({
filePathToWatch: paths.cwd,
commandToExecute: 'npm run test',
timeout: null,
}),
);
break;
case 'element':
await (async () => {
/**
* this strategy runs a standard server and bundles the ts files to a dist_watch directory
*/
// lets create a standard server
logger.log(
'info',
'bundling TypeScript files to "dist_watch" Note: This is for development only!',
);
this.typedserver = new plugins.typedserver.TypedServer({
cors: true,
injectReload: true,
serveDir: plugins.path.join(paths.cwd, './dist_watch/'),
port: 3002,
compression: true,
spaFallback: true,
securityHeaders: {
crossOriginOpenerPolicy: 'same-origin',
crossOriginEmbedderPolicy: 'require-corp',
},
});
logger.log('info', 'Starting tswatch with config-driven mode');
const bundleAndReloadElement = async () => {
await tsbundle.build(paths.cwd, './html/index.ts', './dist_watch/bundle.js', {
bundler: 'esbuild',
});
await this.typedserver.reload();
};
this.watcherMap.add(
new Watcher({
filePathToWatch: plugins.path.join(paths.cwd, './ts_web/'),
functionToCall: async () => {
await bundleAndReloadElement();
},
timeout: null,
}),
);
// lets get the other ts folders
let tsfolders = await listFolders(paths.cwd);
tsfolders = tsfolders.filter(
(itemArg) => itemArg.startsWith('ts') && itemArg !== 'ts_web',
);
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash',
});
for (const tsfolder of tsfolders) {
logger.log('info', `creating watcher for folder ${tsfolder}`);
this.watcherMap.add(
new Watcher({
filePathToWatch: plugins.path.join(paths.cwd, `./${tsfolder}/`),
functionToCall: async () => {
logger.log('info', `building ${tsfolder}`);
await smartshellInstance.exec(`(cd ${paths.cwd} && npm run build)`);
await bundleAndReloadElement();
},
timeout: null,
}),
);
}
this.watcherMap.add(
new Watcher({
filePathToWatch: plugins.path.join(paths.cwd, './html/'),
functionToCall: async () => {
await htmlHandler.processHtml({
from: plugins.path.join(paths.cwd, './html/index.html'),
to: plugins.path.join(paths.cwd, './dist_watch/index.html'),
minify: false,
});
await bundleAndReloadElement();
},
timeout: null,
}),
);
})();
break;
case 'website':
await (async () => {
const websiteExecution = new plugins.smartshell.SmartExecution('npm run startTs');
const bundleAndReloadWebsite = async () => {
await tsbundle.build(paths.cwd, './ts_web/index.ts', './dist_serve/bundle.js', {
bundler: 'esbuild',
});
};
let tsfolders = await listFolders(paths.cwd);
tsfolders = tsfolders.filter(
(itemArg) => itemArg.startsWith('ts') && itemArg !== 'ts_web',
);
for (const tsfolder of tsfolders) {
this.watcherMap.add(
new Watcher({
filePathToWatch: plugins.path.join(paths.cwd, `./${tsfolder}/`),
functionToCall: async () => {
await websiteExecution.restart();
await bundleAndReloadWebsite();
},
timeout: null,
}),
);
}
this.watcherMap.add(
new Watcher({
filePathToWatch: plugins.path.join(paths.cwd, './ts_web/'),
functionToCall: async () => {
await bundleAndReloadWebsite();
},
timeout: null,
}),
);
this.watcherMap.add(
new Watcher({
filePathToWatch: plugins.path.join(paths.cwd, './html/'),
functionToCall: async () => {
await htmlHandler.processHtml({
from: plugins.path.join(paths.cwd, './html/index.html'),
to: plugins.path.join(paths.cwd, './dist_serve/index.html'),
minify: false,
});
await bundleAndReloadWebsite();
},
timeout: null,
}),
);
this.watcherMap.add(
new Watcher({
filePathToWatch: plugins.path.join(paths.cwd, './assets/'),
functionToCall: async () => {
await assetsHandler.processAssets();
await bundleAndReloadWebsite();
},
timeout: null,
}),
);
})();
break;
case 'service':
this.watcherMap.add(
new Watcher({
filePathToWatch: plugins.path.join(paths.cwd, './ts/'),
commandToExecute: 'npm run startTs',
timeout: null,
}),
);
break;
case 'echo':
const tsWatchInstanceEchoSomething = new Watcher({
filePathToWatch: plugins.path.join(paths.cwd, './ts'),
commandToExecute: 'npm -v',
timeout: null,
});
this.watcherMap.add(tsWatchInstanceEchoSomething);
break;
default:
break;
// Start server if configured
if (this.config.server?.enabled) {
await this.startServer();
}
this.watcherMap.forEach(async (watcher) => {
// 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,
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,
}),
);
}
}
@@ -228,7 +192,7 @@ export class TsWatch {
if (this.typedserver) {
await this.typedserver.stop();
}
this.watcherMap.forEach(async (watcher) => {
await this.watcherMap.forEach(async (watcher) => {
await watcher.stop();
});
}