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:
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user