Compare commits

...

58 Commits

Author SHA1 Message Date
0eef560f1d fix(deps): pin smartexit to ^2.0.1 and smartshell to ^3.3.4 for PID tracking
Some checks failed
Default (tags) / security (push) Successful in 33s
Default (tags) / test (push) Failing after 39s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-04 00:17:35 +00:00
f7f42ff36c fix(watcher): always tree-kill on stop regardless of childProcess.killed flag
Some checks failed
Default (tags) / security (push) Successful in 33s
Default (tags) / test (push) Failing after 39s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
The direct bash child may already be dead from terminal SIGINT while
grandchildren (tsrun, devserver) are still running. Removing the .killed
guard ensures tree-kill always runs to clean up the entire process tree.
2026-03-04 00:09:21 +00:00
77d2e6ee57 v3.2.2
Some checks failed
Default (tags) / security (push) Successful in 44s
Default (tags) / test (push) Failing after 4m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-03 23:43:39 +00:00
e8bd8da3c7 fix(lifecycle): use ProcessLifecycle for coordinated shutdown
Replace per-Watcher SIGINT handlers with a single ProcessLifecycle.install()
in TsWatch.start(). This eliminates competing signal handler races that left
orphaned child processes. Add @push.rocks/smartexit as direct dependency.
2026-03-03 23:43:26 +00:00
91b3e273de v3.2.1
Some checks failed
Default (tags) / security (push) Successful in 45s
Default (tags) / test (push) Failing after 4m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-03 22:38:31 +00:00
e6a7b352f3 fix(watcher): ensure child processes are killed and awaited during shutdown; improve cleanup handlers; bump smartshell dependency to ^3.3.2 2026-03-03 22:38:31 +00:00
8c1b306313 v3.2.0
Some checks failed
Default (tags) / security (push) Successful in 33s
Default (tags) / test (push) Failing after 4m5s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-24 19:02:39 +00:00
a893e7a771 feat(bundle): add configurable bundle output modes and bundler options (support base64ts, production builds, includeFiles, maxLineLength) and route non-default outputs to a CustomBundleHandler 2026-02-24 19:02:39 +00:00
c330420eea v3.1.0
Some checks failed
Default (tags) / security (push) Successful in 41s
Default (tags) / test (push) Failing after 4m2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-05 14:58:27 +00:00
df8b164434 feat(dev-server): add no-cache headers to built-in development server; update docs and bump dependencies 2026-02-05 14:58:27 +00:00
6af321647d v3.0.1
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 4m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-24 11:39:18 +00:00
47cd9adc8e fix(deps): downgrade @push.rocks/smartinteract to ^2.0.16 2026-01-24 11:39:18 +00:00
721dc3b22c v3.0.0
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 4m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-24 11:29:55 +00:00
9fb3f6a872 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 2026-01-24 11:29:55 +00:00
5a702055f4 v2.3.13
Some checks failed
Default (tags) / security (push) Successful in 51s
Default (tags) / test (push) Failing after 40s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-11 21:19:20 +00:00
ebcfbc88b2 fix(@push.rocks/smartwatch): Update @push.rocks/smartwatch dependency to ^6.3.0 2025-12-11 21:19:20 +00:00
cd76756e89 v2.3.12
Some checks failed
Default (tags) / security (push) Successful in 41s
Default (tags) / test (push) Failing after 40s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-11 19:15:36 +00:00
c269f4f7e1 fix(smartwatch): Bump @push.rocks/smartwatch from ^6.2.4 to ^6.2.5 2025-12-11 19:15:36 +00:00
88595335ea v2.3.11
Some checks failed
Default (tags) / security (push) Failing after 18s
Default (tags) / test (push) Failing after 49s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-11 17:14:05 +00:00
313362b795 fix(typedserver): Add cross-origin security headers to element mode dev server 2025-12-11 17:14:05 +00:00
0bb76f32b8 v2.3.10
Some checks failed
Default (tags) / security (push) Successful in 44s
Default (tags) / test (push) Failing after 40s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-11 11:39:43 +00:00
5c7bf83f74 fix(dependencies): Bump dependency versions: @types/node to ^25.0.0 and @push.rocks/smartwatch to ^6.2.4 2025-12-11 11:39:43 +00:00
57caa96a85 v2.3.9
Some checks failed
Default (tags) / security (push) Successful in 44s
Default (tags) / test (push) Failing after 46s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-11 09:12:31 +00:00
eb64501767 fix(smartwatch): Bump @push.rocks/smartwatch dependency to ^6.2.3 2025-12-11 09:12:31 +00:00
3f90dfd1db v2.3.8
Some checks failed
Default (tags) / security (push) Successful in 48s
Default (tags) / test (push) Failing after 38s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-11 08:40:45 +00:00
b5552a7f34 fix(@push.rocks/smartwatch): Bump @push.rocks/smartwatch dependency to ^6.2.2 2025-12-11 08:40:45 +00:00
814cc70a29 v2.3.7
Some checks failed
Default (tags) / security (push) Successful in 49s
Default (tags) / test (push) Failing after 40s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-10 17:18:08 +00:00
8508eee439 fix(smartwatch): Bump @push.rocks/smartwatch dependency to ^6.2.1 2025-12-10 17:18:08 +00:00
592e1f35b2 v2.3.6
Some checks failed
Default (tags) / security (push) Successful in 49s
Default (tags) / test (push) Failing after 39s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-10 14:31:55 +00:00
94fa31a5c8 fix(dependencies): Bump @types/node to ^24.10.2 and @push.rocks/smartwatch to ^6.2.0 2025-12-10 14:31:55 +00:00
6b8ce469d2 v2.3.5
Some checks failed
Default (tags) / security (push) Successful in 44s
Default (tags) / test (push) Failing after 41s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 19:35:32 +00:00
bb1038baa0 fix(dependencies): Update @push.rocks/smartwatch dependency to ^6.1.1 2025-12-08 19:35:32 +00:00
a3db29edd7 v2.3.4
Some checks failed
Default (tags) / security (push) Successful in 1m54s
Default (tags) / test (push) Failing after 40s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 17:54:39 +00:00
2c8ed46c9e fix(dependencies.@push.rocks/smartwatch): Bump @push.rocks/smartwatch dependency to ^6.1.0 2025-12-08 17:54:39 +00:00
6c30d35de7 v2.3.3
Some checks failed
Default (tags) / security (push) Successful in 50s
Default (tags) / test (push) Failing after 1m49s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 17:07:20 +00:00
3cdd5302f8 fix(dependencies): Bump dependencies: @api.global/typedserver to ^7.11.1 and @push.rocks/smartwatch to ^6.0.0 2025-12-08 17:07:20 +00:00
42bba057cb v2.3.2
Some checks failed
Default (tags) / security (push) Successful in 48s
Default (tags) / test (push) Failing after 39s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 15:57:12 +00:00
b9cddaa3f3 fix(smartwatch): Bump @push.rocks/smartwatch dependency to ^5.1.0 2025-12-08 15:57:12 +00:00
9ecf007767 v2.3.1
Some checks failed
Default (tags) / security (push) Successful in 30s
Default (tags) / test (push) Failing after 37s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 12:31:38 +00:00
660a480317 fix(element): Enable SPA fallback in element dev server 2025-12-08 12:31:38 +00:00
ec268802ca v2.3.0
Some checks failed
Default (tags) / security (push) Successful in 49s
Default (tags) / test (push) Failing after 39s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 12:27:52 +00:00
6415a459d2 feat(typedserver): Enable compression for element development server and update @api.global/typedserver dependency 2025-12-08 12:27:52 +00:00
01bde87067 v2.2.5
Some checks failed
Default (tags) / security (push) Successful in 50s
Default (tags) / test (push) Failing after 40s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 11:32:12 +00:00
4f4c4a7538 fix(typedserver): Update @api.global/typedserver to ^7.10.2 and remove deprecated compression options from TypedServer initialization 2025-12-08 11:32:12 +00:00
0dee896201 v2.2.4
Some checks failed
Default (tags) / security (push) Successful in 53s
Default (tags) / test (push) Failing after 41s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-04 16:15:38 +00:00
29f7283f7a fix(dependencies): Bump dependency versions: @api.global/typedserver, @git.zone/tsbundle, @push.rocks/smartfs, @push.rocks/taskbuffer 2025-12-04 16:15:38 +00:00
3eb99e1f08 v2.2.3
Some checks failed
Default (tags) / security (push) Successful in 48s
Default (tags) / test (push) Failing after 37s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-04 08:30:05 +00:00
ad41fe876d fix(tswatch.classes.watcher): Convert directory watch paths to glob patterns for smartwatch compatibility 2025-12-04 08:30:05 +00:00
d6f3381e71 v2.2.2
Some checks failed
Default (tags) / security (push) Successful in 41s
Default (tags) / test (push) Failing after 39s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-01 12:03:00 +00:00
bcbf6fc139 fix(core): Replace smartchok/smartfile with smartwatch/smartfs, update watcher and plugins, and bump dependencies 2025-12-01 12:03:00 +00:00
f73ca7ac2f fix(exports): correct package.json exports field syntax
Some checks failed
Default (tags) / security (push) Successful in 32s
Default (tags) / test (push) Successful in 52s
Default (tags) / release (push) Failing after 57s
Default (tags) / metadata (push) Successful in 57s
2025-07-29 17:50:59 +00:00
75acc4b768 feat(exports): modernize package exports and expose watcher class
Some checks failed
Default (tags) / security (push) Successful in 49s
Default (tags) / test (push) Successful in 1m7s
Default (tags) / release (push) Failing after 1m18s
Default (tags) / metadata (push) Successful in 1m26s
2025-07-29 17:46:28 +00:00
83cf8594ab 2.1.5
Some checks failed
Default (tags) / security (push) Successful in 44s
Default (tags) / test (push) Successful in 1m5s
Default (tags) / release (push) Failing after 1m10s
Default (tags) / metadata (push) Successful in 1m17s
2025-07-29 17:34:00 +00:00
417dc4c763 fix(dependencies): update 2025-07-29 17:33:57 +00:00
64a86f7af3 2.1.4
Some checks failed
Default (tags) / security (push) Successful in 47s
Default (tags) / test (push) Successful in 1m5s
Default (tags) / release (push) Failing after 1m14s
Default (tags) / metadata (push) Successful in 1m21s
2025-07-29 17:32:55 +00:00
1a66e76b6f 2.1.3
Some checks failed
Default (tags) / security (push) Successful in 50s
Default (tags) / test (push) Successful in 1m1s
Default (tags) / release (push) Failing after 48s
Default (tags) / metadata (push) Successful in 53s
2025-07-29 17:32:32 +00:00
5c04dc7729 fix(documentation): Update and align documentation with internal CLI and project structure 2025-07-29 17:32:32 +00:00
ecd76bc8aa update readme 2025-07-10 14:33:10 +00:00
18 changed files with 5913 additions and 4676 deletions

View File

@@ -1,5 +1,176 @@
# Changelog
## 2026-03-03 - 3.2.1 - fix(watcher)
ensure child processes are killed and awaited during shutdown; improve cleanup handlers; bump smartshell dependency to ^3.3.2
- Await child process kill() calls in restart and stop to avoid race conditions and ensure proper termination.
- Add last-resort synchronous SIGKILL in process 'exit' handler to terminate orphaned child processes.
- Make SIGINT and timeout handlers async and await stop() to perform a clean shutdown before exiting.
- Bump @push.rocks/smartshell from ^3.3.0 to ^3.3.2 in package.json.
## 2026-02-24 - 3.2.0 - feat(bundle)
add configurable bundle output modes and bundler options (support base64ts, production builds, includeFiles, maxLineLength) and route non-default outputs to a CustomBundleHandler
- Added new ITswatchConfig fields: outputMode, bundler, production, includeFiles, maxLineLength
- tswatch now creates a CustomBundleHandler and uses it when outputMode is not 'bundle' (e.g. base64ts)
- Default bundling path now reads bundler and production from bundleConfig (defaults to 'esbuild' and false)
- Bumped dependency @git.zone/tsbundle to ^2.9.0
## 2026-02-05 - 3.1.0 - feat(dev-server)
add no-cache headers to built-in development server; update docs and bump dependencies
- Introduce noCache: true in ts/tswatch.classes.tswatch.ts to send Cache-Control: no-store, no-cache during development (prevents browser caching).
- Update documentation to describe no-caching behavior (readme.md and readme.hints.md).
- Bump dependencies: @git.zone/tstest ^3.1.8, @types/node ^25.2.1, @push.rocks/npmextra ^5.3.3, @push.rocks/taskbuffer ^4.2.0.
## 2026-01-24 - 3.0.1 - fix(deps)
downgrade @push.rocks/smartinteract to ^2.0.16
- package.json: @push.rocks/smartinteract ^2.1.0 -> ^2.0.16
## 2026-01-24 - 3.0.0 - 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
- Switch to config-driven operation: configuration read from npmextra.json under the key @git.zone/tswatch
- Added ConfigHandler for loading/merging presets and new TswatchInit interactive wizard (runInit) to create/save configuration
- Changed TsWatch constructor to accept ITswatchConfig and added TsWatch.fromConfig(cwd?) for loading from npmextra.json
- Significant public API change: previous watchmode string-based constructor/behavior removed/rewired — consumers must migrate to new config-based usage (breaking change)
- Watcher refactor: Watcher.fromConfig, named watchers, array/single path support, debounce, restart/queue handling, runOnStart, safer start/stop behavior and execution tracking
- New TypeScript interfaces: interfaces.config.ts (ITswatchConfig, IWatcherConfig, IServerConfig, IBundleConfig); removed/changed old watchmodes types
- CLI updated to use configuration if present or launch the init wizard; added init command
- Updated tests to cover ConfigHandler, Watcher, and TsWatch config-driven behavior
- Updated dependencies and plugin usage (added @push.rocks/npmextra, @push.rocks/smartinteract; bumped several @git.zone and @push.rocks package versions)
## 2025-12-11 - 2.3.13 - fix(@push.rocks/smartwatch)
Update @push.rocks/smartwatch dependency to ^6.3.0
- Bump @push.rocks/smartwatch from ^6.2.5 to ^6.3.0 in package.json
- Dependency-only change; no source files modified
## 2025-12-11 - 2.3.12 - fix(smartwatch)
Bump @push.rocks/smartwatch from ^6.2.4 to ^6.2.5
- Updated dependency @push.rocks/smartwatch to ^6.2.5 in package.json
- No source code changes; dependency version bump only
## 2025-12-11 - 2.3.11 - fix(typedserver)
Add cross-origin security headers to element mode dev server
- Set crossOriginOpenerPolicy to 'same-origin' and crossOriginEmbedderPolicy to 'require-corp' on the TypedServer used in element mode.
- Improves security and enables cross-origin isolation (e.g. for SharedArrayBuffer) during local development.
- Applies to the development server serving ./dist_watch/ on port 3002.
## 2025-12-11 - 2.3.10 - fix(dependencies)
Bump dependency versions: @types/node to ^25.0.0 and @push.rocks/smartwatch to ^6.2.4
- Update devDependency @types/node from ^24.10.2 to ^25.0.0
- Update dependency @push.rocks/smartwatch from ^6.2.3 to ^6.2.4
## 2025-12-11 - 2.3.9 - fix(smartwatch)
Bump @push.rocks/smartwatch dependency to ^6.2.3
- Updated dependency @push.rocks/smartwatch from ^6.2.2 to ^6.2.3
- No source code changes; dependency version bump only
## 2025-12-11 - 2.3.8 - fix(@push.rocks/smartwatch)
Bump @push.rocks/smartwatch dependency to ^6.2.2
- package.json: updated @push.rocks/smartwatch from ^6.2.1 to ^6.2.2
## 2025-12-10 - 2.3.7 - fix(smartwatch)
Bump @push.rocks/smartwatch dependency to ^6.2.1
- Updated dependency @push.rocks/smartwatch from ^6.2.0 to ^6.2.1 in package.json
## 2025-12-10 - 2.3.6 - fix(dependencies)
Bump @types/node to ^24.10.2 and @push.rocks/smartwatch to ^6.2.0
- Dev dependency @types/node updated from ^24.10.1 to ^24.10.2
- Dependency @push.rocks/smartwatch updated from ^6.1.1 to ^6.2.0
## 2025-12-08 - 2.3.5 - fix(dependencies)
Update @push.rocks/smartwatch dependency to ^6.1.1
- Bump @push.rocks/smartwatch from ^6.1.0 to ^6.1.1 in package.json
- Only package.json changed; no source code modifications
## 2025-12-08 - 2.3.4 - fix(dependencies.@push.rocks/smartwatch)
Bump @push.rocks/smartwatch dependency to ^6.1.0
- Updated package.json dependency @push.rocks/smartwatch from ^6.0.0 to ^6.1.0
## 2025-12-08 - 2.3.3 - fix(dependencies)
Bump dependencies: @api.global/typedserver to ^7.11.1 and @push.rocks/smartwatch to ^6.0.0
- Updated @api.global/typedserver from ^7.11.0 to ^7.11.1 (patch).
- Updated @push.rocks/smartwatch from ^5.1.0 to ^6.0.0 (major). Verify compatibility as this may include breaking changes in that dependency.
- Change is limited to package.json (dependency version updates).
## 2025-12-08 - 2.3.2 - fix(smartwatch)
Bump @push.rocks/smartwatch dependency to ^5.1.0
- Updated dependency @push.rocks/smartwatch from ^5.0.0 to ^5.1.0 in package.json
## 2025-12-08 - 2.3.1 - fix(element)
Enable SPA fallback in element dev server
- Add spaFallback: true to the TypedServer configuration used in element mode.
- Improves developer experience for single-page apps by serving the index file for unknown routes during development and supporting client-side routing.
## 2025-12-08 - 2.3.0 - feat(typedserver)
Enable compression for element development server and update @api.global/typedserver dependency
- Enable HTTP compression (compression: true) for the element mode development server (TypedServer) to improve asset delivery during development.
- Bump dependency @api.global/typedserver from ^7.10.2 to ^7.11.0 in package.json.
## 2025-12-08 - 2.2.5 - fix(typedserver)
Update @api.global/typedserver to ^7.10.2 and remove deprecated compression options from TypedServer initialization
- Bump @api.global/typedserver dependency from ^7.4.1 to ^7.10.2.
- Remove enableCompression and preferredCompressionMethod options when creating TypedServer in element mode to be compatible with the newer API.
## 2025-12-04 - 2.2.4 - fix(dependencies)
Bump dependency versions: @api.global/typedserver, @git.zone/tsbundle, @push.rocks/smartfs, @push.rocks/taskbuffer
- Upgrade @api.global/typedserver from ^3.0.80 to ^7.4.1
- Upgrade @git.zone/tsbundle from ^2.6.2 to ^2.6.3
- Upgrade @push.rocks/smartfs from ^1.1.3 to ^1.2.0
- Upgrade @push.rocks/taskbuffer from ^3.4.0 to ^3.5.0
## 2025-12-04 - 2.2.3 - fix(tswatch.classes.watcher)
Convert directory watch paths to glob patterns for smartwatch compatibility
- Watcher: convert directory paths to recursive glob patterns before adding them to Smartwatch so directories are watched recursively (e.g. /path/to/dir/ -> /path/to/dir/**/*).
- Readme: added migration note explaining that directory paths are converted to glob patterns for smartwatch compatibility.
## 2025-12-01 - 2.2.2 - fix(core)
Replace smartchok/smartfile with smartwatch/smartfs, update watcher and plugins, and bump dependencies
- Replaced @push.rocks/smartchok with @push.rocks/smartwatch and updated Watcher to use Smartwatch (add/start/stop/getObservableFor).
- Replaced @push.rocks/smartfile with @push.rocks/smartfs and added SmartFs usage (SmartFsProviderNode) plus a listFolders helper used by TsWatch.
- Updated tswatch.plugins.ts to export smartfs and smartwatch and removed smartchok/smartfile exports.
- Updated tswatch.classes.watcher.ts and tswatch.classes.tswatch.ts to use the new smartwatch/smartfs APIs and adjusted directory listing and watcher logic accordingly.
- Bumped several devDependencies and dependencies in package.json (tsbuild, tstest, @api.global/typedserver, @git.zone/tsbundle, @git.zone/tsrun, @push.rocks/* packages).
- Documentation updates: readme.md and readme.hints.md include migration notes and Issue Reporting & Security section.
## 2025-07-29 - 2.2.1 - fix(exports)
Fix package.json exports field syntax
- Fixed exports field syntax from "./" to "." for proper module resolution
## 2025-07-29 - 2.2.0 - feat(exports)
Modernize package exports and expose watcher class
- Updated package.json to use modern exports field instead of main/typings
- Exposed TsWatch watcher class through index exports
## 2025-07-29 - 2.1.3 - fix(documentation)
Update and align documentation with internal CLI and project structure
- Refined readme.md and readme.hints.md to clearly describe the various watch modes and project setup
- Ensured the CLI command mappings in tswatch.cli.ts are documented for consistent usage
- Included update to internal commit information file for clarity
## 2025-06-26 - 2.1.2 - fix(dependencies)
Update @push.rocks/smartchok dependency to ^1.1.1

View File

@@ -1,9 +1,5 @@
{
"npmci": {
"npmGlobalTools": [],
"npmAccessLevel": "public"
},
"gitzone": {
"@git.zone/cli": {
"projectType": "npm",
"module": {
"githost": "code.foss.global",
@@ -34,9 +30,19 @@
"node.js",
"development server"
]
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
}
},
"tsdoc": {
"@git.zone/tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
},
"@ship.zone/szci": {
"npmGlobalTools": []
}
}

View File

@@ -1,10 +1,11 @@
{
"name": "@git.zone/tswatch",
"version": "2.1.2",
"version": "3.2.4",
"private": false,
"description": "A development tool for automatically watching and re-compiling TypeScript projects upon detecting file changes, enhancing developer workflows.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"exports": {
".": "./dist_ts/index.js"
},
"type": "module",
"author": "Lossless GmbH",
"license": "MIT",
@@ -17,24 +18,27 @@
"buildDocs": "tsdoc"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.6.4",
"@git.zone/tstest": "^2.3.1",
"@types/node": "^24.0.4"
"@git.zone/tsbuild": "^4.1.2",
"@git.zone/tstest": "^3.1.8",
"@types/node": "^25.2.1"
},
"dependencies": {
"@api.global/typedserver": "^3.0.74",
"@git.zone/tsbundle": "^2.5.1",
"@git.zone/tsrun": "^1.3.3",
"@api.global/typedserver": "^8.3.0",
"@git.zone/tsbundle": "^2.9.0",
"@git.zone/tsrun": "^2.0.1",
"@push.rocks/early": "^4.0.4",
"@push.rocks/lik": "^6.2.2",
"@push.rocks/smartchok": "^1.1.1",
"@push.rocks/smartcli": "^4.0.11",
"@push.rocks/npmextra": "^5.3.3",
"@push.rocks/smartcli": "^4.0.20",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartfile": "^11.2.5",
"@push.rocks/smartlog": "^3.1.8",
"@push.rocks/smartexit": "^2.0.1",
"@push.rocks/smartfs": "^1.3.1",
"@push.rocks/smartinteract": "^2.0.16",
"@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartlog-destination-local": "^9.0.2",
"@push.rocks/smartshell": "^3.2.3",
"@push.rocks/taskbuffer": "^3.1.7"
"@push.rocks/smartshell": "^3.3.4",
"@push.rocks/smartwatch": "^6.3.0",
"@push.rocks/taskbuffer": "^4.2.0"
},
"files": [
"ts/**/*",

8572
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,94 @@
# tswatch Project Hints
## Core Architecture (v3.x - Config-Driven)
tswatch is now a config-driven TypeScript file watcher. Configuration is read from `npmextra.json` under the key `@git.zone/tswatch`.
### Key Classes
- **TsWatch**: Main orchestrator class, accepts `ITswatchConfig`
- **Watcher**: Handles individual file watching with debouncing and restart modes
- **ConfigHandler**: Loads and manages configuration from npmextra.json
- **TswatchInit**: Interactive wizard for creating configuration
### Configuration Structure
```json
{
"@git.zone/tswatch": {
"watchers": [
{
"name": "backend",
"watch": "./ts/**/*",
"command": "npm run startTs",
"restart": true,
"debounce": 300,
"runOnStart": true
}
],
"server": {
"enabled": true,
"port": 3002,
"serveDir": "./dist_watch/",
"liveReload": true
},
"bundles": [
{
"name": "frontend",
"from": "./html/index.ts",
"to": "./dist_watch/bundle.js",
"watchPatterns": ["./ts_web/**/*"],
"triggerReload": true
}
],
"preset": "element"
}
}
```
### Available Presets
- **npm**: Watch ts/ and test/, run `npm test`
- **test**: Watch ts/ and test/, run `npm run test2`
- **service**: Watch ts/, run `npm run startTs` with restart
- **element**: Dev server on 3002, bundle ts_web, watch html
- **website**: Full stack with backend restart + frontend bundling + assets
### CLI Commands
- `tswatch` - Run with config (or launch wizard if no config)
- `tswatch init` - Force run the configuration wizard
## Key Implementation Details
- Uses `@push.rocks/smartwatch` (v6.x) for file watching - class is `Smartwatch`
- Uses `@push.rocks/smartfs` (v1.x) for filesystem operations
- Uses `@push.rocks/npmextra` for reading npmextra.json config
- Uses `@push.rocks/smartinteract` for the init wizard
- Uses `@git.zone/tsbundle` for bundling with esbuild
- Uses `@api.global/typedserver` for development server
### Watcher Features
- **Debouncing**: Configurable delay before executing (default: 300ms)
- **Restart mode**: Kill previous process before restarting (configurable)
- **Named watchers**: All watchers have names for clear logging
- **Multiple watch patterns**: Can watch multiple glob patterns
### Server Features
- Port configurable (default: 3002)
- CORS enabled
- Gzip compression
- Live reload injection (configurable)
- SPA fallback support
- No-cache headers (prevents browser caching during development)
## Project Structure
- `ts/tswatch.classes.tswatch.ts` - Main TsWatch class
- `ts/tswatch.classes.watcher.ts` - Watcher class with debounce/restart
- `ts/tswatch.classes.confighandler.ts` - Config loading
- `ts/tswatch.init.ts` - Interactive init wizard
- `ts/tswatch.cli.ts` - CLI entry point
- `ts/interfaces/interfaces.config.ts` - Type definitions

480
readme.md
View File

@@ -1,199 +1,411 @@
# @git.zone/tswatch
A development tool for watching and re-compiling TypeScript projects automatically upon detecting changes.
A powerful, config-driven TypeScript file watcher that automatically recompiles and executes your project when files change. Built for modern TypeScript development with zero-config presets and deep customization options.
## Install
## Issue Reporting and Security
To install `@git.zone/tswatch`, ensure that you have a Node.js environment set up with npm. You can install the package globally or locally within a project using npm:
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
## ✨ Features
- 🔄 **Config-driven architecture** - Define watchers, bundles, and dev server in `npmextra.json`
-**Zero-config presets** - Get started instantly with `npm`, `element`, `service`, `website`, and `test` presets
- 🧙 **Interactive wizard** - Run `tswatch init` to generate configuration interactively
- 🌐 **Built-in dev server** - Live reload, CORS, compression, SPA fallback out of the box
- 📦 **Smart bundling** - TypeScript, HTML, and assets with esbuild integration
- 🔁 **Debounced execution** - Configurable debounce prevents command spam
- 🛑 **Process management** - Automatic restart or queue mode for long-running commands
- 🎯 **Glob patterns** - Watch any files with flexible pattern matching
## 📦 Installation
```bash
npm install @git.zone/tswatch
# Global installation (recommended for CLI usage)
pnpm install -g @git.zone/tswatch
# As a dev dependency
pnpm install --save-dev @git.zone/tswatch
```
This command will install the package and enable you to use the `tswatch` command in your terminal to watch TypeScript projects.
## 🚀 Quick Start
## Usage
### Using the Wizard
`@git.zone/tswatch` is a powerful tool designed to streamline your development workflow by monitoring your TypeScript files and automatically recompiling them when changes are detected. This utility is particularly helpful during the development phase of a TypeScript project, ensuring your project remains up-to-date, reflecting the latest changes seamlessly.
### Getting Started with tswatch
To get started, we will explore setting up a basic watcher, integrating tswatch into a Node.js environment, utilizing advanced features, and handling multiple watchers.
### Setting Up a Basic Watcher
#### Step 1: Basic Command-Line Interface Usage
After installing `@git.zone/tswatch`, you can start a watcher on your TypeScript project using its command-line interface (CLI). Here's how to initiate a watcher for different project types:
```typescript
import { runCli } from '@git.zone/tswatch';
(async () => {
await runCli();
})();
```bash
# Run the interactive wizard to create your configuration
tswatch init
```
The CLI supports several commands to watch different project types such as `element`, `npm`, `service`, and `website`. Here's how to use them:
The wizard will guide you through creating a `npmextra.json` configuration with your chosen preset or custom watchers.
- **NPM Projects**: Watch a Node.js NPM project:
### Using Presets
```bash
tswatch npm
```
If you already have a configuration, just run:
This command will start a watcher for your node module projects. Whenever changes are detected, the specified npm scripts are executed.
- **Element Projects**: Watch an element-based architecture:
```bash
tswatch element
```
This command sets up a development server with live reloading and bundles TypeScript files to a `dist_watch` directory.
- **Service Projects**: Watch a service project:
```bash
tswatch service
```
Watches TypeScript files in a service pattern, restarting the service when changes are detected.
- **Website Projects**: Perfect for full website projects:
```bash
tswatch website
```
Similar to element projects but specifically designed for full websites, recompiling TypeScript files for web deployment scenarios.
#### Step 2: Integration into a Node.js Environment
You can integrate `@git.zone/tswatch` directly into a Node.js project for automatic file watching and recompiling:
```typescript
import { TsWatch } from '@git.zone/tswatch';
const startWatch = async () => {
// Initialize watch instance for a node project
const watchInstance = new TsWatch('node');
await watchInstance.start();
};
// Optionally, stop the watcher gracefully
const stopWatch = async (watchInstance: TsWatch) => {
await watchInstance.stop();
};
```bash
tswatch
```
This setup will establish a node environment watch, automatically recompiling files as changes occur.
This reads your config from `npmextra.json` under the `@git.zone/tswatch` key and starts watching.
### Advanced Usage: Enhancing Workflow with tswatch
## ⚙️ Configuration
#### Starting a TypedServer with Watch Integration
tswatch uses `npmextra.json` for configuration. Add your config under the `@git.zone/tswatch` key:
For projects requiring a development server, integrate `typedserver` for handling HTTP server tasks:
```typescript
import { TsWatch } from '@git.zone/tswatch';
const startTypedServer = async () => {
const watchInstance = new TsWatch('element');
await watchInstance.start();
// Serve a local directory using a typedserver with CORS and compression
const server = watchInstance.typedserver;
if (server) {
await server.start();
```json
{
"@git.zone/tswatch": {
"preset": "npm"
}
};
}
```
This code watches for changes and serves the project locally, providing reload capabilities for smooth development.
### Available Presets
#### Step 3: Custom Watchers
| Preset | Description |
|--------|-------------|
| `npm` | Watch `ts/` and `test/`, run `npm test` on changes |
| `test` | Watch `ts/` and `test/`, run `npm run test2` on changes |
| `service` | Watch `ts/`, restart `npm run startTs` (ideal for backend services) |
| `element` | Dev server on port 3002 + bundling for web components |
| `website` | Full-stack: backend + frontend bundling + asset processing |
Beyond built-in commands, you can create custom watchers for detailed control over file monitoring and execution responses:
### Full Configuration Schema
```typescript
import { Watcher } from '@git.zone/tswatch';
```json
{
"@git.zone/tswatch": {
"preset": "element",
const customWatcherSetup = async () => {
const customWatcher = new Watcher({
filePathToWatch: '/path/to/watch',
commandToExecute: 'npm run custom-script',
});
"server": {
"enabled": true,
"port": 3002,
"serveDir": "./dist_watch/",
"liveReload": true
},
// Start and stop the custom watcher as needed
await customWatcher.start();
await customWatcher.stop();
};
"bundles": [
{
"name": "main-bundle",
"from": "./ts_web/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": "backend-build",
"watch": "./ts/**/*",
"command": "npm run build",
"restart": false,
"debounce": 300,
"runOnStart": true
},
{
"name": "tests",
"watch": ["./ts/**/*", "./test/**/*"],
"command": "npm test",
"restart": true,
"debounce": 300,
"runOnStart": true
}
]
}
}
```
Define specific file paths and custom shell commands with the options provided by the `Watcher` class.
### Configuration Options
#### Step 4: Handling Multiple Watchers
#### `ITswatchConfig`
To handle multiple directories or file sets, use the `ObjectMap` utility, efficiently managing multiple `Watcher` instances within the `TsWatch` framework:
| Option | Type | Description |
|--------|------|-------------|
| `preset` | `string` | Use a preset: `npm`, `test`, `service`, `element`, `website` |
| `watchers` | `IWatcherConfig[]` | Array of watcher configurations |
| `server` | `IServerConfig` | Development server configuration |
| `bundles` | `IBundleConfig[]` | Bundle configurations |
#### `IWatcherConfig`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `name` | `string` | *required* | Name for logging purposes |
| `watch` | `string \| string[]` | *required* | Glob pattern(s) to watch |
| `command` | `string` | - | Shell command to execute on changes |
| `restart` | `boolean` | `true` | Kill previous process before restarting |
| `debounce` | `number` | `300` | Debounce delay in milliseconds |
| `runOnStart` | `boolean` | `true` | Run the command immediately on start |
#### `IServerConfig`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `enabled` | `boolean` | *required* | Whether the server is enabled |
| `port` | `number` | `3002` | Server port |
| `serveDir` | `string` | `./dist_watch/` | Directory to serve |
| `liveReload` | `boolean` | `true` | Inject live reload script |
#### `IBundleConfig`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `name` | `string` | - | Name for logging purposes |
| `from` | `string` | *required* | Entry point file |
| `to` | `string` | *required* | Output file |
| `watchPatterns` | `string[]` | - | Additional patterns to watch |
| `triggerReload` | `boolean` | `true` | Trigger server reload after bundling |
## 🛠️ CLI Commands
### `tswatch`
Runs with configuration from `npmextra.json`. If no config exists, launches the interactive wizard.
```bash
tswatch
```
### `tswatch init`
Force-run the configuration wizard (creates or overwrites existing config).
```bash
tswatch init
```
## 💻 Programmatic API
### Basic Usage with Config
```typescript
import { TsWatch } from '@git.zone/tswatch';
const setupMultipleWatchers = async () => {
const tsWatchInstance = new TsWatch('node');
// Create TsWatch with inline configuration
const watcher = new TsWatch({
watchers: [
{
name: 'my-watcher',
watch: './src/**/*',
command: 'npm run build',
restart: true,
debounce: 300,
runOnStart: true,
},
],
});
// View active watchers through instance mapping
tsWatchInstance.watcherMap.forEach((watcher) => {
console.log(`Watcher listening on: ${watcher.toString()}`);
});
await watcher.start();
// Initiate all watchers
await tsWatchInstance.start();
};
// Later: stop watching
await watcher.stop();
```
Incorporate this setup for efficiently managing complex projects with varied sources and parallel build tasks.
### Load from Config File
#### Handling Timeout and Cleanup
```typescript
import { TsWatch } from '@git.zone/tswatch';
`@git.zone/tswatch` includes functions to manage process exits and timeout scenarios robustly:
// Load configuration from npmextra.json
const watcher = TsWatch.fromConfig();
if (watcher) {
await watcher.start();
}
```
### Using ConfigHandler
```typescript
import { ConfigHandler } from '@git.zone/tswatch';
const configHandler = new ConfigHandler();
// Check if config exists
if (configHandler.hasConfig()) {
const config = configHandler.loadConfig();
console.log(config);
}
// Get available presets
const presets = configHandler.getPresetNames();
console.log(presets); // ['npm', 'test', 'service', 'element', 'website']
// Get a specific preset
const npmPreset = configHandler.getPreset('npm');
```
### Using Watcher Directly
For more granular control, use the `Watcher` class:
```typescript
import { Watcher } from '@git.zone/tswatch';
const watcherWithTimeout = () => {
new Watcher({
filePathToWatch: './src/',
timeout: 10000, // 10 seconds timeout
commandToExecute: 'echo "Task completed"',
}).start();
};
// Create from config object
const watcher = Watcher.fromConfig({
name: 'my-watcher',
watch: ['./src/**/*', './lib/**/*'],
command: 'npm run compile',
restart: true,
});
await watcher.start();
```
The `timeout` option ensures processes don't run indefinitely, aiding development and potential automated testing scenarios.
### Using Function Callbacks
### Conclusion
```typescript
import { Watcher } from '@git.zone/tswatch';
By providing flexible configurations, a robust CLI, and deep integration capabilities, `@git.zone/tswatch` serves as a comprehensive solution for automating and optimizing your TypeScript development processes. Whether managing server-side environments or advanced web apps, using this tool will ensure your projects are always ready with the latest changes.
const watcher = new Watcher({
name: 'custom-handler',
filePathToWatch: './src/**/*',
functionToCall: async () => {
console.log('Files changed! Running custom logic...');
// Your custom build/test/deploy logic here
},
debounce: 500,
runOnStart: true,
});
Explore the various features and tailor the tool to fit your unique project requirements, leading to faster and more efficient development workflows.
await watcher.start();
```
## 📁 Project Structures
### NPM Package / Node.js Library
```
project/
├── ts/ # TypeScript source files
├── test/ # Test files
├── package.json # With "test" script
└── npmextra.json # tswatch config
```
Config:
```json
{
"@git.zone/tswatch": {
"preset": "npm"
}
}
```
### Backend Service
```
project/
├── ts/ # TypeScript source files
├── package.json # With "startTs" script
└── npmextra.json
```
Config:
```json
{
"@git.zone/tswatch": {
"preset": "service"
}
}
```
### Web Component / Element
```
project/
├── ts/ # Backend TypeScript (optional)
├── ts_web/ # Frontend TypeScript
├── html/
│ ├── index.ts # Web entry point
│ └── index.html
├── dist_watch/ # Output (auto-created)
└── npmextra.json
```
Config:
```json
{
"@git.zone/tswatch": {
"preset": "element"
}
}
```
Access your project at `http://localhost:3002`
### Full-Stack Website
```
project/
├── ts/ # Backend TypeScript
├── ts_web/ # Frontend TypeScript
│ └── index.ts
├── html/
│ └── index.html
├── assets/ # Static assets
├── dist_serve/ # Output
└── npmextra.json
```
Config:
```json
{
"@git.zone/tswatch": {
"preset": "website"
}
}
```
## 🌐 Development Server
The built-in development server (enabled in `element` and `website` presets) features:
- **Live Reload** - Automatically refreshes browser on changes
- **No Caching** - Prevents browser caching during development (sends `Cache-Control: no-store, no-cache` headers)
- **CORS** - Cross-origin requests enabled
- **Compression** - Gzip compression for faster loading
- **SPA Fallback** - Single-page application routing support
- **Security Headers** - Cross-origin isolation headers
Default configuration:
- **Port**: 3002
- **Serve Directory**: `./dist_watch/`
- **Live Reload**: Enabled
## 🔧 Configuration Tips
1. **Use presets for common workflows** - They're battle-tested and cover most use cases
2. **Customize with explicit config** - Override preset defaults by adding explicit `watchers`, `bundles`, or `server` config
3. **Debounce wisely** - Default 300ms works well; increase for slower builds
4. **Use `restart: false`** for one-shot commands (like builds) and `restart: true` for long-running processes (like servers)
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@@ -1,20 +1,105 @@
// tslint:disable-next-line: no-implicit-dependencies
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as tswatch from '../ts/index.js';
let testTsWatchInstance: tswatch.TsWatch;
// ============================================
// ConfigHandler Tests
// ============================================
tap.test('should create a valid TsWatch instance', async () => {
testTsWatchInstance = new tswatch.TsWatch('echo');
tap.test('ConfigHandler: should return all preset names', async () => {
const configHandler = new tswatch.ConfigHandler();
const presetNames = configHandler.getPresetNames();
expect(presetNames).toContain('npm');
expect(presetNames).toContain('test');
expect(presetNames).toContain('service');
expect(presetNames).toContain('element');
expect(presetNames).toContain('website');
expect(presetNames.length).toEqual(5);
});
tap.test('should start the tswatch instance', async () => {
await testTsWatchInstance.start();
tap.test('ConfigHandler: should return npm preset with watchers', async () => {
const configHandler = new tswatch.ConfigHandler();
const preset = configHandler.getPreset('npm');
expect(preset).toBeTruthy();
expect(preset.watchers).toBeTruthy();
expect(preset.watchers.length).toBeGreaterThan(0);
expect(preset.watchers[0].name).toEqual('npm-test');
expect(preset.watchers[0].command).toEqual('npm run test');
});
tap.test('should stop the instance', async (tools) => {
tools.delayFor(2000);
testTsWatchInstance.stop();
tap.test('ConfigHandler: should return element preset with server', async () => {
const configHandler = new tswatch.ConfigHandler();
const preset = configHandler.getPreset('element');
expect(preset).toBeTruthy();
expect(preset.server).toBeTruthy();
expect(preset.server.enabled).toBeTrue();
expect(preset.server.port).toEqual(3002);
expect(preset.bundles).toBeTruthy();
expect(preset.bundles.length).toBeGreaterThan(0);
});
tap.start();
tap.test('ConfigHandler: should return null for invalid preset', async () => {
const configHandler = new tswatch.ConfigHandler();
const preset = configHandler.getPreset('invalid');
expect(preset).toBeNull();
});
tap.test('ConfigHandler: should return correct config key', async () => {
const configHandler = new tswatch.ConfigHandler();
expect(configHandler.getConfigKey()).toEqual('@git.zone/tswatch');
});
// ============================================
// Watcher Tests
// ============================================
tap.test('Watcher: should create from config with defaults', async () => {
const watcher = tswatch.Watcher.fromConfig({
name: 'test-watcher',
watch: './ts/**/*',
command: 'echo test',
});
expect(watcher).toBeInstanceOf(tswatch.Watcher);
});
tap.test('Watcher: should handle array watch patterns', async () => {
const watcher = tswatch.Watcher.fromConfig({
name: 'multi-watch',
watch: ['./ts/**/*', './test/**/*'],
command: 'echo test',
});
expect(watcher).toBeInstanceOf(tswatch.Watcher);
});
// ============================================
// TsWatch Tests
// ============================================
let testTsWatch: tswatch.TsWatch;
tap.test('TsWatch: should create with minimal config', async () => {
testTsWatch = new tswatch.TsWatch({
watchers: [
{
name: 'echo-test',
watch: './ts/**/*',
command: 'echo "test"',
runOnStart: false,
},
],
});
expect(testTsWatch).toBeInstanceOf(tswatch.TsWatch);
expect(testTsWatch.config.watchers).toBeTruthy();
expect(testTsWatch.config.watchers.length).toEqual(1);
});
tap.test('TsWatch: should start and populate watcherMap', async () => {
await testTsWatch.start();
expect(testTsWatch.watcherMap.getArray().length).toBeGreaterThan(0);
});
tap.test('TsWatch: should stop cleanly', async (tools) => {
await tools.delayFor(500);
await testTsWatch.stop();
});
export default tap.start();

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tswatch',
version: '2.1.2',
version: '3.2.1',
description: 'A development tool for automatically watching and re-compiling TypeScript projects upon detecting file changes, enhancing developer workflows.'
}

View File

@@ -1,5 +1,11 @@
import * as early from '@push.rocks/early';
early.start('tswatch');
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,71 @@
/**
* 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;
/** Output mode: 'bundle' writes JS, 'base64ts' writes base64-encoded TS (default: 'bundle') */
outputMode?: 'bundle' | 'base64ts';
/** Bundler to use (default: 'esbuild') */
bundler?: 'esbuild' | 'rolldown' | 'rspack';
/** Whether to produce a production build (default: false) */
production?: boolean;
/** Files to include alongside the bundle */
includeFiles?: (string | { from: string; to: string })[];
/** Max chars per line for base64ts output. 0 or undefined = unlimited */
maxLineLength?: number;
}
/**
* 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,204 +3,207 @@ 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 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 customBundleHandler = new plugins.tsbundle.CustomBundleHandler();
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,
enableCompression: true,
preferredCompressionMethod: 'gzip',
});
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,
}),
);
// Install global process lifecycle handlers (SIGINT, SIGTERM, etc.)
// This is the single authority for signal handling — no per-watcher handlers.
plugins.smartexit.ProcessLifecycle.install();
const exitInstance = new plugins.smartexit.SmartExit({ silent: true });
exitInstance.addCleanupFunction(async () => {
await this.stop();
});
// lets get the other ts folders
let tsfolders = await plugins.smartfile.fs.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 plugins.smartfile.fs.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,
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 if (bundleConfig.outputMode && bundleConfig.outputMode !== 'bundle') {
// Non-default outputMode (e.g. base64ts) — use CustomBundleHandler
await this.customBundleHandler.processSingleBundle({
from: bundleConfig.from,
to: bundleConfig.to,
outputMode: bundleConfig.outputMode,
bundler: bundleConfig.bundler || 'esbuild',
production: bundleConfig.production || false,
includeFiles: bundleConfig.includeFiles,
maxLineLength: bundleConfig.maxLineLength,
});
} else {
// Standard TypeScript bundling (default)
await this.tsbundle.build(paths.cwd, fromPath, toPath, {
bundler: bundleConfig.bundler || 'esbuild',
production: bundleConfig.production || false,
});
}
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,
}),
);
}
}
@@ -211,7 +214,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;
}
/**
@@ -20,76 +33,162 @@ export class Watcher {
});
private currentExecution: plugins.smartshell.IExecResultStreaming;
private smartchokWatcher = new plugins.smartchok.Smartchok([]);
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`);
this.smartchokWatcher.add([this.options.filePathToWatch]); // __dirname refers to the directory of this very file
await this.smartchokWatcher.start();
const changeObservable = await this.smartchokWatcher.getObservableFor('change');
changeObservable.subscribe(() => {
this.updateCurrentExecution();
// 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}/**/*`;
});
await this.updateCurrentExecution();
logger.log('info', `watcher started for ${this.options.filePathToWatch}`);
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.handleChange();
});
// 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}`);
await 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;
}
}
}
/**
* this method sets up a clean exit strategy
* Sets up timeout-based cleanup if configured.
* Signal handling (SIGINT/SIGTERM) is managed globally by ProcessLifecycle in TsWatch.
*/
private async setupCleanup() {
process.on('exit', () => {
console.log('');
console.log('now exiting!');
this.stop();
process.exit(0);
});
process.on('SIGINT', () => {
console.log('');
console.log('ok! got SIGINT We are exiting! Just cleaning up to exit neatly :)');
this.stop();
process.exit(0);
});
// handle timeout
if (this.options.timeout) {
plugins.smartdelay.delayFor(this.options.timeout).then(() => {
console.log(`timed out afer ${this.options.timeout} milliseconds! exiting!`);
this.stop();
plugins.smartdelay.delayFor(this.options.timeout).then(async () => {
console.log(`timed out after ${this.options.timeout} milliseconds! exiting!`);
await this.stop();
process.exit(0);
});
}
@@ -99,9 +198,14 @@ export class Watcher {
* stops the watcher
*/
public async stop() {
await this.smartchokWatcher.stop();
if (this.currentExecution && !this.currentExecution.childProcess.killed) {
this.currentExecution.kill();
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
await this.smartwatchInstance.stop();
if (this.currentExecution) {
// Always tree-kill — even if the direct child is dead (.killed === true),
// grandchildren (e.g. tsrun, devserver) may still be running.
await 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,34 +2,40 @@
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 smartchok from '@push.rocks/smartchok';
import * as npmextra from '@push.rocks/npmextra';
import * as smartcli from '@push.rocks/smartcli';
import * as smartdelay from '@push.rocks/smartdelay';
import * as smartfile from '@push.rocks/smartfile';
import * as smartexit from '@push.rocks/smartexit';
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';
import * as smartwatch from '@push.rocks/smartwatch';
import * as taskbuffer from '@push.rocks/taskbuffer';
export {
lik,
smartchok,
npmextra,
smartcli,
smartdelay,
smartfile,
smartexit,
smartfs,
smartinteract,
smartlog,
smartlogDestinationLocal,
smartshell,
smartwatch,
taskbuffer,
};