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,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tswatch',
version: '2.3.13',
version: '3.0.0',
description: 'A development tool for automatically watching and re-compiling TypeScript projects upon detecting file changes, enhancing developer workflows.'
}

View File

@@ -1,7 +1,11 @@
import * as early from '@push.rocks/early';
early.start('tswatch');
export * from './tswatch.classes.tswatch.js';
export * from './tswatch.cli.js';
early.stop();
export * from './tswatch.classes.watcher.js';
export * from './tswatch.classes.tswatch.js';
export * from './tswatch.classes.watcher.js';
export * from './tswatch.classes.confighandler.js';
export * from './tswatch.cli.js';
export * from './tswatch.init.js';
export * from './interfaces/index.js';
early.stop();

View File

@@ -1 +1 @@
export * from './interfaces.watchmodes.js';
export * from './interfaces.config.js';

View File

@@ -0,0 +1,61 @@
/**
* Configuration for a single watcher
*/
export interface IWatcherConfig {
/** Name for this watcher (used in logging) */
name: string;
/** Glob pattern(s) to watch */
watch: string | string[];
/** Shell command to execute on changes */
command?: string;
/** If true, kill previous process before restarting (default: true) */
restart?: boolean;
/** Debounce delay in ms (default: 300) */
debounce?: number;
/** If true, run the command immediately on start (default: true) */
runOnStart?: boolean;
}
/**
* Configuration for the development server
*/
export interface IServerConfig {
/** Whether the server is enabled */
enabled: boolean;
/** Server port (default: 3002) */
port?: number;
/** Directory to serve (default: ./dist_watch/) */
serveDir?: string;
/** Whether to inject live reload script (default: true) */
liveReload?: boolean;
}
/**
* Configuration for a bundle operation
*/
export interface IBundleConfig {
/** Name for this bundle (used in logging) */
name?: string;
/** Entry point file */
from: string;
/** Output file */
to: string;
/** Additional patterns to watch that trigger this bundle */
watchPatterns?: string[];
/** If true, trigger server reload after bundling (default: true) */
triggerReload?: boolean;
}
/**
* Main tswatch configuration
*/
export interface ITswatchConfig {
/** Array of watcher configurations */
watchers?: IWatcherConfig[];
/** Development server configuration */
server?: IServerConfig;
/** Bundle configurations */
bundles?: IBundleConfig[];
/** Use a preset configuration (overridden by explicit watchers/server/bundles) */
preset?: 'element' | 'website' | 'npm' | 'service' | 'test';
}

View File

@@ -1 +0,0 @@
export type TWatchModes = 'test' | 'node' | 'service' | 'element' | 'website' | 'echo';

View File

@@ -0,0 +1,185 @@
import * as plugins from './tswatch.plugins.js';
import * as paths from './tswatch.paths.js';
import * as interfaces from './interfaces/index.js';
const CONFIG_KEY = '@git.zone/tswatch';
/**
* Preset configurations matching legacy watch modes
*/
const presets: Record<string, interfaces.ITswatchConfig> = {
npm: {
watchers: [
{
name: 'npm-test',
watch: ['./ts/**/*', './test/**/*'],
command: 'npm run test',
restart: true,
debounce: 300,
runOnStart: true,
},
],
},
test: {
watchers: [
{
name: 'test2',
watch: ['./ts/**/*', './test/**/*'],
command: 'npm run test2',
restart: true,
debounce: 300,
runOnStart: true,
},
],
},
service: {
watchers: [
{
name: 'service',
watch: './ts/**/*',
command: 'npm run startTs',
restart: true,
debounce: 300,
runOnStart: true,
},
],
},
element: {
server: {
enabled: true,
port: 3002,
serveDir: './dist_watch/',
liveReload: true,
},
bundles: [
{
name: 'element-bundle',
from: './html/index.ts',
to: './dist_watch/bundle.js',
watchPatterns: ['./ts_web/**/*'],
triggerReload: true,
},
{
name: 'html',
from: './html/index.html',
to: './dist_watch/index.html',
watchPatterns: ['./html/**/*'],
triggerReload: true,
},
],
watchers: [
{
name: 'ts-build',
watch: './ts/**/*',
command: 'npm run build',
restart: false,
debounce: 300,
runOnStart: false,
},
],
},
website: {
bundles: [
{
name: 'website-bundle',
from: './ts_web/index.ts',
to: './dist_serve/bundle.js',
watchPatterns: ['./ts_web/**/*'],
triggerReload: false,
},
{
name: 'html',
from: './html/index.html',
to: './dist_serve/index.html',
watchPatterns: ['./html/**/*'],
triggerReload: false,
},
{
name: 'assets',
from: './assets/',
to: './dist_serve/assets/',
watchPatterns: ['./assets/**/*'],
triggerReload: false,
},
],
watchers: [
{
name: 'backend',
watch: './ts/**/*',
command: 'npm run startTs',
restart: true,
debounce: 300,
runOnStart: true,
},
],
},
};
/**
* Handles loading and managing tswatch configuration
*/
export class ConfigHandler {
private npmextra: plugins.npmextra.Npmextra;
private cwd: string;
constructor(cwdArg?: string) {
this.cwd = cwdArg || paths.cwd;
this.npmextra = new plugins.npmextra.Npmextra(this.cwd);
}
/**
* Check if a tswatch configuration exists
*/
public hasConfig(): boolean {
const config = this.npmextra.dataFor<interfaces.ITswatchConfig>(CONFIG_KEY, null);
return config !== null;
}
/**
* Load configuration from npmextra.json
* If a preset is specified, merge preset defaults with user overrides
*/
public loadConfig(): interfaces.ITswatchConfig | null {
const config = this.npmextra.dataFor<interfaces.ITswatchConfig>(CONFIG_KEY, null);
if (!config) {
return null;
}
// If a preset is specified, merge it with user config
if (config.preset && presets[config.preset]) {
const preset = presets[config.preset];
return {
...preset,
...config,
// Merge arrays instead of replacing
watchers: config.watchers || preset.watchers,
bundles: config.bundles || preset.bundles,
server: config.server !== undefined ? config.server : preset.server,
};
}
return config;
}
/**
* Get a preset configuration by name
*/
public getPreset(presetName: string): interfaces.ITswatchConfig | null {
return presets[presetName] || null;
}
/**
* Get all available preset names
*/
public getPresetNames(): string[] {
return Object.keys(presets);
}
/**
* Get the config key for npmextra.json
*/
public getConfigKey(): string {
return CONFIG_KEY;
}
}

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();
});
}

View File

@@ -1,11 +1,24 @@
import * as plugins from './tswatch.plugins.js';
import * as interfaces from './interfaces/index.js';
import { logger } from './tswatch.logging.js';
export interface IWatcherConstructorOptions {
filePathToWatch: string;
/** Name for this watcher (used in logging) */
name?: string;
/** Path(s) to watch - can be a single path or array */
filePathToWatch: string | string[];
/** Shell command to execute on changes */
commandToExecute?: string;
/** Function to call on changes */
functionToCall?: () => Promise<any>;
/** Timeout for the watcher */
timeout?: number;
/** If true, kill previous process before restarting (default: true) */
restart?: boolean;
/** Debounce delay in ms (default: 300) */
debounce?: number;
/** If true, run the command immediately on start (default: true) */
runOnStart?: boolean;
}
/**
@@ -22,53 +35,148 @@ export class Watcher {
private currentExecution: plugins.smartshell.IExecResultStreaming;
private smartwatchInstance = new plugins.smartwatch.Smartwatch([]);
private options: IWatcherConstructorOptions;
private debounceTimer: NodeJS.Timeout | null = null;
private isExecuting = false;
private pendingExecution = false;
constructor(optionsArg: IWatcherConstructorOptions) {
this.options = optionsArg;
this.options = {
restart: true,
debounce: 300,
runOnStart: true,
...optionsArg,
};
}
/**
* Create a Watcher from config
*/
public static fromConfig(config: interfaces.IWatcherConfig): Watcher {
const watchPaths = Array.isArray(config.watch) ? config.watch : [config.watch];
return new Watcher({
name: config.name,
filePathToWatch: watchPaths,
commandToExecute: config.command,
restart: config.restart ?? true,
debounce: config.debounce ?? 300,
runOnStart: config.runOnStart ?? true,
});
}
/**
* Get the watcher name for logging
*/
private getName(): string {
return this.options.name || 'unnamed';
}
/**
* start the file
*/
public async start() {
logger.log('info', `trying to start watcher for ${this.options.filePathToWatch}`);
const name = this.getName();
logger.log('info', `[${name}] starting watcher`);
await this.setupCleanup();
console.log(`Looking at ${this.options.filePathToWatch} for changes`);
// Convert directory path to glob pattern for smartwatch
const watchPath = this.options.filePathToWatch.endsWith('/')
? `${this.options.filePathToWatch}**/*`
: `${this.options.filePathToWatch}/**/*`;
this.smartwatchInstance.add([watchPath]);
// Convert paths to glob patterns
const paths = Array.isArray(this.options.filePathToWatch)
? this.options.filePathToWatch
: [this.options.filePathToWatch];
const watchPatterns = paths.map((p) => {
// Convert directory path to glob pattern for smartwatch
if (p.endsWith('/')) {
return `${p}**/*`;
}
// If it's already a glob pattern, use as-is
if (p.includes('*')) {
return p;
}
// Otherwise assume it's a directory
return `${p}/**/*`;
});
logger.log('info', `[${name}] watching patterns: ${watchPatterns.join(', ')}`);
this.smartwatchInstance.add(watchPatterns);
await this.smartwatchInstance.start();
const changeObservable = await this.smartwatchInstance.getObservableFor('change');
changeObservable.subscribe(() => {
this.updateCurrentExecution();
this.handleChange();
});
await this.updateCurrentExecution();
logger.log('info', `watcher started for ${this.options.filePathToWatch}`);
// Run on start if configured
if (this.options.runOnStart) {
await this.executeCommand();
}
logger.log('info', `[${name}] watcher started`);
}
/**
* updates the current execution
* Handle file change with debouncing
*/
private async updateCurrentExecution() {
if (this.options.commandToExecute) {
if (this.currentExecution) {
logger.log('ok', `reexecuting ${this.options.commandToExecute}`);
this.currentExecution.kill();
} else {
logger.log('ok', `executing ${this.options.commandToExecute} for the first time`);
private handleChange() {
const name = this.getName();
// Clear existing debounce timer
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
// Set new debounce timer
this.debounceTimer = setTimeout(async () => {
this.debounceTimer = null;
// If currently executing and not in restart mode, mark pending
if (this.isExecuting && !this.options.restart) {
logger.log('info', `[${name}] change detected, queuing execution`);
this.pendingExecution = true;
return;
}
await this.executeCommand();
// If there was a pending execution, run it
if (this.pendingExecution) {
this.pendingExecution = false;
await this.executeCommand();
}
}, this.options.debounce);
}
/**
* Execute the command or function
*/
private async executeCommand() {
const name = this.getName();
if (this.options.commandToExecute) {
if (this.currentExecution && this.options.restart) {
logger.log('ok', `[${name}] restarting: ${this.options.commandToExecute}`);
this.currentExecution.kill();
} else if (!this.currentExecution) {
logger.log('ok', `[${name}] executing: ${this.options.commandToExecute}`);
}
this.isExecuting = true;
this.currentExecution = await this.smartshellInstance.execStreaming(
this.options.commandToExecute,
);
} else {
console.log('no executionCommand set');
// Track when execution completes
this.currentExecution.childProcess.on('exit', () => {
this.isExecuting = false;
});
}
if (this.options.functionToCall) {
this.options.functionToCall();
} else {
console.log('no functionToCall set.');
this.isExecuting = true;
try {
await this.options.functionToCall();
} finally {
this.isExecuting = false;
}
}
}
@@ -103,6 +211,9 @@ export class Watcher {
* stops the watcher
*/
public async stop() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
await this.smartwatchInstance.stop();
if (this.currentExecution && !this.currentExecution.childProcess.killed) {
this.currentExecution.kill();

View File

@@ -3,42 +3,48 @@ import * as paths from './tswatch.paths.js';
import { logger } from './tswatch.logging.js';
import { TsWatch } from './tswatch.classes.tswatch.js';
import { ConfigHandler } from './tswatch.classes.confighandler.js';
import { runInit } from './tswatch.init.js';
const tswatchCli = new plugins.smartcli.Smartcli();
// standard behaviour will assume gitzone setup
tswatchCli.standardCommand().subscribe((argvArg) => {
tswatchCli.triggerCommand('npm', {});
/**
* Standard command (no args) - run with config or launch wizard
*/
tswatchCli.standardCommand().subscribe(async (argvArg) => {
const configHandler = new ConfigHandler();
if (configHandler.hasConfig()) {
// Config exists - run with it
const tsWatch = TsWatch.fromConfig();
if (tsWatch) {
logger.log('info', 'Starting tswatch with configuration from npmextra.json');
await tsWatch.start();
} else {
logger.log('error', 'Failed to load configuration');
process.exit(1);
}
} else {
// No config - launch wizard
logger.log('info', 'No tswatch configuration found in npmextra.json');
const config = await runInit();
if (config) {
// Run with the newly created config
const tsWatch = new TsWatch(config);
await tsWatch.start();
}
}
});
tswatchCli.addCommand('element').subscribe(async (argvArg) => {
logger.log('info', `running watch task for a gitzone element project`);
const tsWatch = new TsWatch('element');
await tsWatch.start();
});
tswatchCli.addCommand('npm').subscribe(async (argvArg) => {
logger.log('info', `running watch task for a gitzone element project`);
const tsWatch = new TsWatch('node');
await tsWatch.start();
});
tswatchCli.addCommand('service').subscribe(async (argvArg) => {
logger.log('info', `running test task`);
const tsWatch = new TsWatch('service');
await tsWatch.start();
});
tswatchCli.addCommand('test').subscribe(async (argvArg) => {
logger.log('info', `running test task`);
const tsWatch = new TsWatch('test');
await tsWatch.start();
});
tswatchCli.addCommand('website').subscribe(async (argvArg) => {
logger.log('info', `running watch task for a gitzone website project`);
const tsWatch = new TsWatch('website');
await tsWatch.start();
/**
* Init command - force run wizard (overwrite existing config)
*/
tswatchCli.addCommand('init').subscribe(async (argvArg) => {
logger.log('info', 'Running tswatch configuration wizard');
const config = await runInit();
if (config) {
logger.log('ok', 'Configuration created successfully');
}
});
export const runCli = async () => {

199
ts/tswatch.init.ts Normal file
View File

@@ -0,0 +1,199 @@
import * as plugins from './tswatch.plugins.js';
import * as paths from './tswatch.paths.js';
import * as interfaces from './interfaces/index.js';
import { ConfigHandler } from './tswatch.classes.confighandler.js';
import { logger } from './tswatch.logging.js';
const CONFIG_KEY = '@git.zone/tswatch';
/**
* Interactive init wizard for creating tswatch configuration
*/
export class TswatchInit {
private configHandler: ConfigHandler;
private smartInteract: plugins.smartinteract.SmartInteract;
constructor() {
this.configHandler = new ConfigHandler();
this.smartInteract = new plugins.smartinteract.SmartInteract([]);
}
/**
* Run the interactive init wizard
*/
public async run(): Promise<interfaces.ITswatchConfig | null> {
console.log('\n=== tswatch Configuration Wizard ===\n');
// Ask for template choice
const templateAnswer = await this.smartInteract.askQuestion({
name: 'template',
type: 'list',
message: 'Select a configuration template:',
default: 'npm',
choices: [
{ name: 'npm - Watch ts/ and test/, run npm test', value: 'npm' },
{ name: 'test - Watch ts/ and test/, run npm run test2', value: 'test' },
{ name: 'service - Watch ts/, restart npm run startTs', value: 'service' },
{ name: 'element - Dev server + bundling for web components', value: 'element' },
{ name: 'website - Full stack: backend + frontend + assets', value: 'website' },
{ name: 'custom - Configure watchers manually', value: 'custom' },
],
});
const template = templateAnswer.value as string;
let config: interfaces.ITswatchConfig;
if (template === 'custom') {
config = await this.runCustomWizard();
} else {
// Get preset config
const preset = this.configHandler.getPreset(template);
if (!preset) {
console.error(`Unknown template: ${template}`);
return null;
}
config = { ...preset, preset: template as interfaces.ITswatchConfig['preset'] };
}
// Save to npmextra.json
await this.saveConfig(config);
console.log('\nConfiguration saved to npmextra.json');
console.log('Run "tswatch" to start watching.\n');
return config;
}
/**
* Run custom configuration wizard
*/
private async runCustomWizard(): Promise<interfaces.ITswatchConfig> {
const config: interfaces.ITswatchConfig = {};
// Ask about server
const serverAnswer = await this.smartInteract.askQuestion({
name: 'enableServer',
type: 'confirm',
message: 'Enable development server?',
default: false,
});
if (serverAnswer.value) {
const portAnswer = await this.smartInteract.askQuestion({
name: 'port',
type: 'input',
message: 'Server port:',
default: '3002',
});
const serveDirAnswer = await this.smartInteract.askQuestion({
name: 'serveDir',
type: 'input',
message: 'Directory to serve:',
default: './dist_watch/',
});
config.server = {
enabled: true,
port: parseInt(portAnswer.value as string, 10),
serveDir: serveDirAnswer.value as string,
liveReload: true,
};
}
// Add watchers
config.watchers = [];
let addMore = true;
while (addMore) {
console.log('\n--- Add a watcher ---');
const nameAnswer = await this.smartInteract.askQuestion({
name: 'name',
type: 'input',
message: 'Watcher name:',
default: `watcher-${config.watchers.length + 1}`,
});
const watchAnswer = await this.smartInteract.askQuestion({
name: 'watch',
type: 'input',
message: 'Glob pattern(s) to watch (comma-separated):',
default: './ts/**/*',
});
const commandAnswer = await this.smartInteract.askQuestion({
name: 'command',
type: 'input',
message: 'Command to execute:',
default: 'npm run test',
});
const restartAnswer = await this.smartInteract.askQuestion({
name: 'restart',
type: 'confirm',
message: 'Restart command on each change (vs queue)?',
default: true,
});
// Parse watch patterns
const watchPatterns = (watchAnswer.value as string)
.split(',')
.map((p) => p.trim())
.filter((p) => p.length > 0);
config.watchers.push({
name: nameAnswer.value as string,
watch: watchPatterns.length === 1 ? watchPatterns[0] : watchPatterns,
command: commandAnswer.value as string,
restart: restartAnswer.value as boolean,
debounce: 300,
runOnStart: true,
});
const moreAnswer = await this.smartInteract.askQuestion({
name: 'addMore',
type: 'confirm',
message: 'Add another watcher?',
default: false,
});
addMore = moreAnswer.value as boolean;
}
return config;
}
/**
* Save configuration to npmextra.json
*/
private async saveConfig(config: interfaces.ITswatchConfig): Promise<void> {
const npmextraPath = plugins.path.join(paths.cwd, 'npmextra.json');
// Read existing npmextra.json if it exists
let existingConfig: Record<string, any> = {};
try {
const smartfsInstance = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
const content = await smartfsInstance.file(npmextraPath).encoding('utf8').read() as string;
existingConfig = JSON.parse(content);
} catch {
// File doesn't exist or is invalid, start fresh
}
// Update with new tswatch config
existingConfig[CONFIG_KEY] = config;
// Write back
const smartfsInstance = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
await smartfsInstance.file(npmextraPath).encoding('utf8').write(JSON.stringify(existingConfig, null, 2));
}
}
/**
* Run the init wizard
*/
export const runInit = async (): Promise<interfaces.ITswatchConfig | null> => {
const init = new TswatchInit();
return init.run();
};

View File

@@ -2,20 +2,22 @@
import * as path from 'path';
export { path };
// @gitzone scope
// @git.zone scope
import * as tsbundle from '@git.zone/tsbundle';
export { tsbundle };
// @apiglobal scope
// @api.global scope
import * as typedserver from '@api.global/typedserver';
export { typedserver };
// @pushrocks scope
// @push.rocks scope
import * as lik from '@push.rocks/lik';
import * as npmextra from '@push.rocks/npmextra';
import * as smartcli from '@push.rocks/smartcli';
import * as smartdelay from '@push.rocks/smartdelay';
import * as smartfs from '@push.rocks/smartfs';
import * as smartinteract from '@push.rocks/smartinteract';
import * as smartlog from '@push.rocks/smartlog';
import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
import * as smartshell from '@push.rocks/smartshell';
@@ -24,9 +26,11 @@ import * as taskbuffer from '@push.rocks/taskbuffer';
export {
lik,
npmextra,
smartcli,
smartdelay,
smartfs,
smartinteract,
smartlog,
smartlogDestinationLocal,
smartshell,