@git.zone/tswatch 2.3.12 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/index.d.ts +4 -1
  3. package/dist_ts/index.js +5 -2
  4. package/dist_ts/interfaces/index.d.ts +1 -1
  5. package/dist_ts/interfaces/index.js +2 -2
  6. package/dist_ts/interfaces/interfaces.config.d.ts +58 -0
  7. package/dist_ts/interfaces/interfaces.config.js +2 -0
  8. package/dist_ts/tswatch.classes.confighandler.d.ts +30 -0
  9. package/dist_ts/tswatch.classes.confighandler.js +172 -0
  10. package/dist_ts/tswatch.classes.tswatch.d.ts +28 -3
  11. package/dist_ts/tswatch.classes.tswatch.js +135 -165
  12. package/dist_ts/tswatch.classes.watcher.d.ts +31 -3
  13. package/dist_ts/tswatch.classes.watcher.js +105 -25
  14. package/dist_ts/tswatch.cli.js +39 -28
  15. package/dist_ts/tswatch.init.d.ts +25 -0
  16. package/dist_ts/tswatch.init.js +168 -0
  17. package/dist_ts/tswatch.plugins.d.ts +3 -1
  18. package/dist_ts/tswatch.plugins.js +7 -5
  19. package/npmextra.json +12 -6
  20. package/package.json +22 -16
  21. package/readme.hints.md +88 -46
  22. package/readme.md +284 -149
  23. package/ts/00_commitinfo_data.ts +1 -1
  24. package/ts/index.ts +6 -2
  25. package/ts/interfaces/index.ts +1 -1
  26. package/ts/interfaces/interfaces.config.ts +61 -0
  27. package/ts/tswatch.classes.confighandler.ts +185 -0
  28. package/ts/tswatch.classes.tswatch.ts +161 -197
  29. package/ts/tswatch.classes.watcher.ts +134 -23
  30. package/ts/tswatch.cli.ts +37 -31
  31. package/ts/tswatch.init.ts +199 -0
  32. package/ts/tswatch.plugins.ts +7 -3
  33. package/dist_ts/interfaces/interfaces.watchmodes.d.ts +0 -1
  34. package/dist_ts/interfaces/interfaces.watchmodes.js +0 -2
  35. package/ts/interfaces/interfaces.watchmodes.ts +0 -1
@@ -1,11 +1,24 @@
1
1
  import * as plugins from './tswatch.plugins.js';
2
+ import * as interfaces from './interfaces/index.js';
2
3
  import { logger } from './tswatch.logging.js';
3
4
 
4
5
  export interface IWatcherConstructorOptions {
5
- filePathToWatch: string;
6
+ /** Name for this watcher (used in logging) */
7
+ name?: string;
8
+ /** Path(s) to watch - can be a single path or array */
9
+ filePathToWatch: string | string[];
10
+ /** Shell command to execute on changes */
6
11
  commandToExecute?: string;
12
+ /** Function to call on changes */
7
13
  functionToCall?: () => Promise<any>;
14
+ /** Timeout for the watcher */
8
15
  timeout?: number;
16
+ /** If true, kill previous process before restarting (default: true) */
17
+ restart?: boolean;
18
+ /** Debounce delay in ms (default: 300) */
19
+ debounce?: number;
20
+ /** If true, run the command immediately on start (default: true) */
21
+ runOnStart?: boolean;
9
22
  }
10
23
 
11
24
  /**
@@ -22,53 +35,148 @@ export class Watcher {
22
35
  private currentExecution: plugins.smartshell.IExecResultStreaming;
23
36
  private smartwatchInstance = new plugins.smartwatch.Smartwatch([]);
24
37
  private options: IWatcherConstructorOptions;
38
+ private debounceTimer: NodeJS.Timeout | null = null;
39
+ private isExecuting = false;
40
+ private pendingExecution = false;
25
41
 
26
42
  constructor(optionsArg: IWatcherConstructorOptions) {
27
- this.options = optionsArg;
43
+ this.options = {
44
+ restart: true,
45
+ debounce: 300,
46
+ runOnStart: true,
47
+ ...optionsArg,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Create a Watcher from config
53
+ */
54
+ public static fromConfig(config: interfaces.IWatcherConfig): Watcher {
55
+ const watchPaths = Array.isArray(config.watch) ? config.watch : [config.watch];
56
+ return new Watcher({
57
+ name: config.name,
58
+ filePathToWatch: watchPaths,
59
+ commandToExecute: config.command,
60
+ restart: config.restart ?? true,
61
+ debounce: config.debounce ?? 300,
62
+ runOnStart: config.runOnStart ?? true,
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Get the watcher name for logging
68
+ */
69
+ private getName(): string {
70
+ return this.options.name || 'unnamed';
28
71
  }
29
72
 
30
73
  /**
31
74
  * start the file
32
75
  */
33
76
  public async start() {
34
- logger.log('info', `trying to start watcher for ${this.options.filePathToWatch}`);
77
+ const name = this.getName();
78
+ logger.log('info', `[${name}] starting watcher`);
35
79
  await this.setupCleanup();
36
- console.log(`Looking at ${this.options.filePathToWatch} for changes`);
37
- // Convert directory path to glob pattern for smartwatch
38
- const watchPath = this.options.filePathToWatch.endsWith('/')
39
- ? `${this.options.filePathToWatch}**/*`
40
- : `${this.options.filePathToWatch}/**/*`;
41
- this.smartwatchInstance.add([watchPath]);
80
+
81
+ // Convert paths to glob patterns
82
+ const paths = Array.isArray(this.options.filePathToWatch)
83
+ ? this.options.filePathToWatch
84
+ : [this.options.filePathToWatch];
85
+
86
+ const watchPatterns = paths.map((p) => {
87
+ // Convert directory path to glob pattern for smartwatch
88
+ if (p.endsWith('/')) {
89
+ return `${p}**/*`;
90
+ }
91
+ // If it's already a glob pattern, use as-is
92
+ if (p.includes('*')) {
93
+ return p;
94
+ }
95
+ // Otherwise assume it's a directory
96
+ return `${p}/**/*`;
97
+ });
98
+
99
+ logger.log('info', `[${name}] watching patterns: ${watchPatterns.join(', ')}`);
100
+ this.smartwatchInstance.add(watchPatterns);
42
101
  await this.smartwatchInstance.start();
102
+
43
103
  const changeObservable = await this.smartwatchInstance.getObservableFor('change');
44
104
  changeObservable.subscribe(() => {
45
- this.updateCurrentExecution();
105
+ this.handleChange();
46
106
  });
47
- await this.updateCurrentExecution();
48
- logger.log('info', `watcher started for ${this.options.filePathToWatch}`);
107
+
108
+ // Run on start if configured
109
+ if (this.options.runOnStart) {
110
+ await this.executeCommand();
111
+ }
112
+
113
+ logger.log('info', `[${name}] watcher started`);
49
114
  }
50
115
 
51
116
  /**
52
- * updates the current execution
117
+ * Handle file change with debouncing
53
118
  */
54
- private async updateCurrentExecution() {
119
+ private handleChange() {
120
+ const name = this.getName();
121
+
122
+ // Clear existing debounce timer
123
+ if (this.debounceTimer) {
124
+ clearTimeout(this.debounceTimer);
125
+ }
126
+
127
+ // Set new debounce timer
128
+ this.debounceTimer = setTimeout(async () => {
129
+ this.debounceTimer = null;
130
+
131
+ // If currently executing and not in restart mode, mark pending
132
+ if (this.isExecuting && !this.options.restart) {
133
+ logger.log('info', `[${name}] change detected, queuing execution`);
134
+ this.pendingExecution = true;
135
+ return;
136
+ }
137
+
138
+ await this.executeCommand();
139
+
140
+ // If there was a pending execution, run it
141
+ if (this.pendingExecution) {
142
+ this.pendingExecution = false;
143
+ await this.executeCommand();
144
+ }
145
+ }, this.options.debounce);
146
+ }
147
+
148
+ /**
149
+ * Execute the command or function
150
+ */
151
+ private async executeCommand() {
152
+ const name = this.getName();
153
+
55
154
  if (this.options.commandToExecute) {
56
- if (this.currentExecution) {
57
- logger.log('ok', `reexecuting ${this.options.commandToExecute}`);
155
+ if (this.currentExecution && this.options.restart) {
156
+ logger.log('ok', `[${name}] restarting: ${this.options.commandToExecute}`);
58
157
  this.currentExecution.kill();
59
- } else {
60
- logger.log('ok', `executing ${this.options.commandToExecute} for the first time`);
158
+ } else if (!this.currentExecution) {
159
+ logger.log('ok', `[${name}] executing: ${this.options.commandToExecute}`);
61
160
  }
161
+
162
+ this.isExecuting = true;
62
163
  this.currentExecution = await this.smartshellInstance.execStreaming(
63
164
  this.options.commandToExecute,
64
165
  );
65
- } else {
66
- console.log('no executionCommand set');
166
+
167
+ // Track when execution completes
168
+ this.currentExecution.childProcess.on('exit', () => {
169
+ this.isExecuting = false;
170
+ });
67
171
  }
172
+
68
173
  if (this.options.functionToCall) {
69
- this.options.functionToCall();
70
- } else {
71
- console.log('no functionToCall set.');
174
+ this.isExecuting = true;
175
+ try {
176
+ await this.options.functionToCall();
177
+ } finally {
178
+ this.isExecuting = false;
179
+ }
72
180
  }
73
181
  }
74
182
 
@@ -103,6 +211,9 @@ export class Watcher {
103
211
  * stops the watcher
104
212
  */
105
213
  public async stop() {
214
+ if (this.debounceTimer) {
215
+ clearTimeout(this.debounceTimer);
216
+ }
106
217
  await this.smartwatchInstance.stop();
107
218
  if (this.currentExecution && !this.currentExecution.childProcess.killed) {
108
219
  this.currentExecution.kill();
package/ts/tswatch.cli.ts CHANGED
@@ -3,42 +3,48 @@ import * as paths from './tswatch.paths.js';
3
3
  import { logger } from './tswatch.logging.js';
4
4
 
5
5
  import { TsWatch } from './tswatch.classes.tswatch.js';
6
+ import { ConfigHandler } from './tswatch.classes.confighandler.js';
7
+ import { runInit } from './tswatch.init.js';
6
8
 
7
9
  const tswatchCli = new plugins.smartcli.Smartcli();
8
10
 
9
- // standard behaviour will assume gitzone setup
10
- tswatchCli.standardCommand().subscribe((argvArg) => {
11
- tswatchCli.triggerCommand('npm', {});
11
+ /**
12
+ * Standard command (no args) - run with config or launch wizard
13
+ */
14
+ tswatchCli.standardCommand().subscribe(async (argvArg) => {
15
+ const configHandler = new ConfigHandler();
16
+
17
+ if (configHandler.hasConfig()) {
18
+ // Config exists - run with it
19
+ const tsWatch = TsWatch.fromConfig();
20
+ if (tsWatch) {
21
+ logger.log('info', 'Starting tswatch with configuration from npmextra.json');
22
+ await tsWatch.start();
23
+ } else {
24
+ logger.log('error', 'Failed to load configuration');
25
+ process.exit(1);
26
+ }
27
+ } else {
28
+ // No config - launch wizard
29
+ logger.log('info', 'No tswatch configuration found in npmextra.json');
30
+ const config = await runInit();
31
+ if (config) {
32
+ // Run with the newly created config
33
+ const tsWatch = new TsWatch(config);
34
+ await tsWatch.start();
35
+ }
36
+ }
12
37
  });
13
38
 
14
- tswatchCli.addCommand('element').subscribe(async (argvArg) => {
15
- logger.log('info', `running watch task for a gitzone element project`);
16
- const tsWatch = new TsWatch('element');
17
- await tsWatch.start();
18
- });
19
-
20
- tswatchCli.addCommand('npm').subscribe(async (argvArg) => {
21
- logger.log('info', `running watch task for a gitzone element project`);
22
- const tsWatch = new TsWatch('node');
23
- await tsWatch.start();
24
- });
25
-
26
- tswatchCli.addCommand('service').subscribe(async (argvArg) => {
27
- logger.log('info', `running test task`);
28
- const tsWatch = new TsWatch('service');
29
- await tsWatch.start();
30
- });
31
-
32
- tswatchCli.addCommand('test').subscribe(async (argvArg) => {
33
- logger.log('info', `running test task`);
34
- const tsWatch = new TsWatch('test');
35
- await tsWatch.start();
36
- });
37
-
38
- tswatchCli.addCommand('website').subscribe(async (argvArg) => {
39
- logger.log('info', `running watch task for a gitzone website project`);
40
- const tsWatch = new TsWatch('website');
41
- await tsWatch.start();
39
+ /**
40
+ * Init command - force run wizard (overwrite existing config)
41
+ */
42
+ tswatchCli.addCommand('init').subscribe(async (argvArg) => {
43
+ logger.log('info', 'Running tswatch configuration wizard');
44
+ const config = await runInit();
45
+ if (config) {
46
+ logger.log('ok', 'Configuration created successfully');
47
+ }
42
48
  });
43
49
 
44
50
  export const runCli = async () => {
@@ -0,0 +1,199 @@
1
+ import * as plugins from './tswatch.plugins.js';
2
+ import * as paths from './tswatch.paths.js';
3
+ import * as interfaces from './interfaces/index.js';
4
+ import { ConfigHandler } from './tswatch.classes.confighandler.js';
5
+ import { logger } from './tswatch.logging.js';
6
+
7
+ const CONFIG_KEY = '@git.zone/tswatch';
8
+
9
+ /**
10
+ * Interactive init wizard for creating tswatch configuration
11
+ */
12
+ export class TswatchInit {
13
+ private configHandler: ConfigHandler;
14
+ private smartInteract: plugins.smartinteract.SmartInteract;
15
+
16
+ constructor() {
17
+ this.configHandler = new ConfigHandler();
18
+ this.smartInteract = new plugins.smartinteract.SmartInteract([]);
19
+ }
20
+
21
+ /**
22
+ * Run the interactive init wizard
23
+ */
24
+ public async run(): Promise<interfaces.ITswatchConfig | null> {
25
+ console.log('\n=== tswatch Configuration Wizard ===\n');
26
+
27
+ // Ask for template choice
28
+ const templateAnswer = await this.smartInteract.askQuestion({
29
+ name: 'template',
30
+ type: 'list',
31
+ message: 'Select a configuration template:',
32
+ default: 'npm',
33
+ choices: [
34
+ { name: 'npm - Watch ts/ and test/, run npm test', value: 'npm' },
35
+ { name: 'test - Watch ts/ and test/, run npm run test2', value: 'test' },
36
+ { name: 'service - Watch ts/, restart npm run startTs', value: 'service' },
37
+ { name: 'element - Dev server + bundling for web components', value: 'element' },
38
+ { name: 'website - Full stack: backend + frontend + assets', value: 'website' },
39
+ { name: 'custom - Configure watchers manually', value: 'custom' },
40
+ ],
41
+ });
42
+
43
+ const template = templateAnswer.value as string;
44
+
45
+ let config: interfaces.ITswatchConfig;
46
+
47
+ if (template === 'custom') {
48
+ config = await this.runCustomWizard();
49
+ } else {
50
+ // Get preset config
51
+ const preset = this.configHandler.getPreset(template);
52
+ if (!preset) {
53
+ console.error(`Unknown template: ${template}`);
54
+ return null;
55
+ }
56
+ config = { ...preset, preset: template as interfaces.ITswatchConfig['preset'] };
57
+ }
58
+
59
+ // Save to npmextra.json
60
+ await this.saveConfig(config);
61
+
62
+ console.log('\nConfiguration saved to npmextra.json');
63
+ console.log('Run "tswatch" to start watching.\n');
64
+
65
+ return config;
66
+ }
67
+
68
+ /**
69
+ * Run custom configuration wizard
70
+ */
71
+ private async runCustomWizard(): Promise<interfaces.ITswatchConfig> {
72
+ const config: interfaces.ITswatchConfig = {};
73
+
74
+ // Ask about server
75
+ const serverAnswer = await this.smartInteract.askQuestion({
76
+ name: 'enableServer',
77
+ type: 'confirm',
78
+ message: 'Enable development server?',
79
+ default: false,
80
+ });
81
+
82
+ if (serverAnswer.value) {
83
+ const portAnswer = await this.smartInteract.askQuestion({
84
+ name: 'port',
85
+ type: 'input',
86
+ message: 'Server port:',
87
+ default: '3002',
88
+ });
89
+
90
+ const serveDirAnswer = await this.smartInteract.askQuestion({
91
+ name: 'serveDir',
92
+ type: 'input',
93
+ message: 'Directory to serve:',
94
+ default: './dist_watch/',
95
+ });
96
+
97
+ config.server = {
98
+ enabled: true,
99
+ port: parseInt(portAnswer.value as string, 10),
100
+ serveDir: serveDirAnswer.value as string,
101
+ liveReload: true,
102
+ };
103
+ }
104
+
105
+ // Add watchers
106
+ config.watchers = [];
107
+ let addMore = true;
108
+
109
+ while (addMore) {
110
+ console.log('\n--- Add a watcher ---');
111
+
112
+ const nameAnswer = await this.smartInteract.askQuestion({
113
+ name: 'name',
114
+ type: 'input',
115
+ message: 'Watcher name:',
116
+ default: `watcher-${config.watchers.length + 1}`,
117
+ });
118
+
119
+ const watchAnswer = await this.smartInteract.askQuestion({
120
+ name: 'watch',
121
+ type: 'input',
122
+ message: 'Glob pattern(s) to watch (comma-separated):',
123
+ default: './ts/**/*',
124
+ });
125
+
126
+ const commandAnswer = await this.smartInteract.askQuestion({
127
+ name: 'command',
128
+ type: 'input',
129
+ message: 'Command to execute:',
130
+ default: 'npm run test',
131
+ });
132
+
133
+ const restartAnswer = await this.smartInteract.askQuestion({
134
+ name: 'restart',
135
+ type: 'confirm',
136
+ message: 'Restart command on each change (vs queue)?',
137
+ default: true,
138
+ });
139
+
140
+ // Parse watch patterns
141
+ const watchPatterns = (watchAnswer.value as string)
142
+ .split(',')
143
+ .map((p) => p.trim())
144
+ .filter((p) => p.length > 0);
145
+
146
+ config.watchers.push({
147
+ name: nameAnswer.value as string,
148
+ watch: watchPatterns.length === 1 ? watchPatterns[0] : watchPatterns,
149
+ command: commandAnswer.value as string,
150
+ restart: restartAnswer.value as boolean,
151
+ debounce: 300,
152
+ runOnStart: true,
153
+ });
154
+
155
+ const moreAnswer = await this.smartInteract.askQuestion({
156
+ name: 'addMore',
157
+ type: 'confirm',
158
+ message: 'Add another watcher?',
159
+ default: false,
160
+ });
161
+
162
+ addMore = moreAnswer.value as boolean;
163
+ }
164
+
165
+ return config;
166
+ }
167
+
168
+ /**
169
+ * Save configuration to npmextra.json
170
+ */
171
+ private async saveConfig(config: interfaces.ITswatchConfig): Promise<void> {
172
+ const npmextraPath = plugins.path.join(paths.cwd, 'npmextra.json');
173
+
174
+ // Read existing npmextra.json if it exists
175
+ let existingConfig: Record<string, any> = {};
176
+ try {
177
+ const smartfsInstance = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
178
+ const content = await smartfsInstance.file(npmextraPath).encoding('utf8').read() as string;
179
+ existingConfig = JSON.parse(content);
180
+ } catch {
181
+ // File doesn't exist or is invalid, start fresh
182
+ }
183
+
184
+ // Update with new tswatch config
185
+ existingConfig[CONFIG_KEY] = config;
186
+
187
+ // Write back
188
+ const smartfsInstance = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
189
+ await smartfsInstance.file(npmextraPath).encoding('utf8').write(JSON.stringify(existingConfig, null, 2));
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Run the init wizard
195
+ */
196
+ export const runInit = async (): Promise<interfaces.ITswatchConfig | null> => {
197
+ const init = new TswatchInit();
198
+ return init.run();
199
+ };
@@ -2,20 +2,22 @@
2
2
  import * as path from 'path';
3
3
  export { path };
4
4
 
5
- // @gitzone scope
5
+ // @git.zone scope
6
6
  import * as tsbundle from '@git.zone/tsbundle';
7
7
  export { tsbundle };
8
8
 
9
- // @apiglobal scope
9
+ // @api.global scope
10
10
  import * as typedserver from '@api.global/typedserver';
11
11
 
12
12
  export { typedserver };
13
13
 
14
- // @pushrocks scope
14
+ // @push.rocks scope
15
15
  import * as lik from '@push.rocks/lik';
16
+ import * as npmextra from '@push.rocks/npmextra';
16
17
  import * as smartcli from '@push.rocks/smartcli';
17
18
  import * as smartdelay from '@push.rocks/smartdelay';
18
19
  import * as smartfs from '@push.rocks/smartfs';
20
+ import * as smartinteract from '@push.rocks/smartinteract';
19
21
  import * as smartlog from '@push.rocks/smartlog';
20
22
  import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
21
23
  import * as smartshell from '@push.rocks/smartshell';
@@ -24,9 +26,11 @@ import * as taskbuffer from '@push.rocks/taskbuffer';
24
26
 
25
27
  export {
26
28
  lik,
29
+ npmextra,
27
30
  smartcli,
28
31
  smartdelay,
29
32
  smartfs,
33
+ smartinteract,
30
34
  smartlog,
31
35
  smartlogDestinationLocal,
32
36
  smartshell,
@@ -1 +0,0 @@
1
- export type TWatchModes = 'test' | 'node' | 'service' | 'element' | 'website' | 'echo';
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlcy53YXRjaG1vZGVzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvaW50ZXJmYWNlcy9pbnRlcmZhY2VzLndhdGNobW9kZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiJ9
@@ -1 +0,0 @@
1
- export type TWatchModes = 'test' | 'node' | 'service' | 'element' | 'website' | 'echo';