BREAKING CHANGE(tswatch): refactor tswatch to a config-driven design (load config from npmextra.json) and add interactive init wizard; change TsWatch public API and enhance Watcher behavior
This commit is contained in:
Binary file not shown.
@@ -1,68 +0,0 @@
|
||||
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
||||
# * For C, use cpp
|
||||
# * For JavaScript, use typescript
|
||||
# Special requirements:
|
||||
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||
language: typescript
|
||||
|
||||
# whether to use the project's gitignore file to ignore files
|
||||
# Added on 2025-04-07
|
||||
ignore_all_files_in_gitignore: true
|
||||
# list of additional paths to ignore
|
||||
# same syntax as gitignore, so you can use * and **
|
||||
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||
# Added (renamed) on 2025-04-07
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
project_name: "tswatch"
|
||||
13
changelog.md
13
changelog.md
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
18
package.json
18
package.json
@@ -18,19 +18,21 @@
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^3.1.2",
|
||||
"@git.zone/tstest": "^3.1.3",
|
||||
"@types/node": "^25.0.0"
|
||||
"@git.zone/tsbuild": "^4.1.2",
|
||||
"@git.zone/tstest": "^3.1.6",
|
||||
"@types/node": "^25.0.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@api.global/typedserver": "^7.11.1",
|
||||
"@git.zone/tsbundle": "^2.6.3",
|
||||
"@git.zone/tsrun": "^2.0.0",
|
||||
"@api.global/typedserver": "^8.3.0",
|
||||
"@git.zone/tsbundle": "^2.8.3",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@push.rocks/early": "^4.0.4",
|
||||
"@push.rocks/lik": "^6.2.2",
|
||||
"@push.rocks/smartcli": "^4.0.19",
|
||||
"@push.rocks/npmextra": "^5.1.2",
|
||||
"@push.rocks/smartcli": "^4.0.20",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartfs": "^1.2.0",
|
||||
"@push.rocks/smartfs": "^1.3.1",
|
||||
"@push.rocks/smartinteract": "^2.1.0",
|
||||
"@push.rocks/smartlog": "^3.1.10",
|
||||
"@push.rocks/smartlog-destination-local": "^9.0.2",
|
||||
"@push.rocks/smartshell": "^3.3.0",
|
||||
|
||||
6229
pnpm-lock.yaml
generated
6229
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
122
readme.hints.md
122
readme.hints.md
@@ -1,51 +1,93 @@
|
||||
# tswatch Project Hints
|
||||
|
||||
## Core Architecture
|
||||
- tswatch is a TypeScript file watcher with multiple operation modes
|
||||
- Main class `TsWatch` orchestrates different watch modes
|
||||
- `Watcher` class handles individual file watching and command execution
|
||||
## Core Architecture (v3.x - Config-Driven)
|
||||
|
||||
## Available Watch Modes
|
||||
1. **npm/node** (default): Runs `npm test` on changes
|
||||
2. **test**: Runs `npm run test2` on changes
|
||||
3. **element**: Web component development with dev server on port 3002
|
||||
4. **service**: Runs `npm run startTs` for service projects
|
||||
5. **website**: Full website mode with bundling and asset processing
|
||||
6. **echo**: Test mode that runs `npm -v` (for testing)
|
||||
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` (v5.x) for file watching - class is `Smartwatch`
|
||||
- Uses `@push.rocks/smartfs` (v1.x) for directory operations - uses `SmartFs` with `SmartFsProviderNode`
|
||||
|
||||
- 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 in element mode
|
||||
- Element/website modes watch multiple `ts*/` directories
|
||||
- All modes support both command execution and function callbacks
|
||||
- Uses `@api.global/typedserver` for development server
|
||||
|
||||
## CLI Entry Points
|
||||
- `cli.js` -> Main CLI entry point
|
||||
- `ts/tswatch.cli.ts` -> CLI implementation with smartcli
|
||||
- Default command triggers npm mode
|
||||
### Watcher Features
|
||||
|
||||
## Project Structure Expectations
|
||||
- `ts/` - Backend TypeScript files
|
||||
- `ts_web/` - Frontend TypeScript files (element/website modes)
|
||||
- `html/` - HTML templates (element/website modes)
|
||||
- `assets/` - Static assets (website mode only)
|
||||
- `dist_watch/` - Output for element mode
|
||||
- `dist_serve/` - Output for website mode
|
||||
- **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
|
||||
|
||||
## Development Server Details
|
||||
- Port: 3002
|
||||
- Features: CORS, gzip compression, live reload injection
|
||||
- Only available in element mode via `typedserver` property
|
||||
### Server Features
|
||||
|
||||
## Common Issues to Watch For
|
||||
- The test mode runs `test2` script, not `test`
|
||||
- Website mode restarts the entire server process on backend changes
|
||||
- Element mode rebuilds and reloads on any ts* folder change
|
||||
- Port configurable (default: 3002)
|
||||
- CORS enabled
|
||||
- Gzip compression
|
||||
- Live reload injection (configurable)
|
||||
- SPA fallback support
|
||||
|
||||
## Migration Notes (v2.2.x)
|
||||
- Replaced `@push.rocks/smartchok` with `@push.rocks/smartwatch` (v5.x)
|
||||
- Replaced `@push.rocks/smartfile` with `@push.rocks/smartfs` for directory listing
|
||||
- Class names: `Smartwatch` for file watching, `SmartFs` + `SmartFsProviderNode` for filesystem ops
|
||||
- Directory paths are converted to glob patterns (`/path/to/dir/` → `/path/to/dir/**/*`) for smartwatch compatibility
|
||||
## 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
|
||||
|
||||
485
readme.md
485
readme.md
@@ -1,176 +1,264 @@
|
||||
# @git.zone/tswatch
|
||||
|
||||
A TypeScript file watcher that automatically recompiles and executes your project when files change. Designed to streamline development workflows for various TypeScript project types.
|
||||
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.
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
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
|
||||
## ✨ Features
|
||||
|
||||
- 🔄 **Automatic recompilation** on file changes
|
||||
- 🚀 **Multiple project modes**: npm packages, web elements, services, and websites
|
||||
- 🌐 **Built-in development server** with live reload for web projects
|
||||
- ⚡ **Fast bundling** with esbuild integration
|
||||
- 🛠️ **Flexible CLI and programmatic API**
|
||||
- 📦 **Zero configuration** for standard project structures
|
||||
- 🔄 **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
|
||||
|
||||
Install `@git.zone/tswatch` globally or as a development dependency:
|
||||
## 📦 Installation
|
||||
|
||||
```bash
|
||||
# Global installation
|
||||
# Global installation (recommended for CLI usage)
|
||||
pnpm install -g @git.zone/tswatch
|
||||
|
||||
# As a dev dependency
|
||||
pnpm install --save-dev @git.zone/tswatch
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Using the Wizard
|
||||
|
||||
```bash
|
||||
# Watch and run tests on changes (default behavior)
|
||||
tswatch
|
||||
|
||||
# Watch a web element project with dev server
|
||||
tswatch element
|
||||
|
||||
# Watch a service project
|
||||
tswatch service
|
||||
# Run the interactive wizard to create your configuration
|
||||
tswatch init
|
||||
```
|
||||
|
||||
## CLI Commands
|
||||
The wizard will guide you through creating a `npmextra.json` configuration with your chosen preset or custom watchers.
|
||||
|
||||
### `tswatch` or `tswatch npm`
|
||||
### Using Presets
|
||||
|
||||
Watches TypeScript files and runs `npm test` on changes. This is the default mode.
|
||||
If you already have a configuration, just run:
|
||||
|
||||
```bash
|
||||
tswatch
|
||||
# or explicitly
|
||||
tswatch npm
|
||||
```
|
||||
|
||||
### `tswatch element`
|
||||
This reads your config from `npmextra.json` under the `@git.zone/tswatch` key and starts watching.
|
||||
|
||||
Sets up a development environment for web components/elements:
|
||||
- Starts a dev server on port 3002
|
||||
- Bundles TypeScript to `dist_watch/`
|
||||
- Enables live reload
|
||||
- Watches all `ts*/` folders
|
||||
## ⚙️ Configuration
|
||||
|
||||
tswatch uses `npmextra.json` for configuration. Add your config under the `@git.zone/tswatch` key:
|
||||
|
||||
```json
|
||||
{
|
||||
"@git.zone/tswatch": {
|
||||
"preset": "npm"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Available Presets
|
||||
|
||||
| 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 |
|
||||
|
||||
### Full Configuration Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"@git.zone/tswatch": {
|
||||
"preset": "element",
|
||||
|
||||
"server": {
|
||||
"enabled": true,
|
||||
"port": 3002,
|
||||
"serveDir": "./dist_watch/",
|
||||
"liveReload": true
|
||||
},
|
||||
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
#### `ITswatchConfig`
|
||||
|
||||
| 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 element
|
||||
tswatch
|
||||
```
|
||||
|
||||
### `tswatch service`
|
||||
### `tswatch init`
|
||||
|
||||
Watches TypeScript files in `./ts/` and runs `npm run startTs` on changes. Ideal for backend services.
|
||||
Force-run the configuration wizard (creates or overwrites existing config).
|
||||
|
||||
```bash
|
||||
tswatch service
|
||||
tswatch init
|
||||
```
|
||||
|
||||
### `tswatch website`
|
||||
## 💻 Programmatic API
|
||||
|
||||
Full website development mode:
|
||||
- Bundles TypeScript files to `dist_serve/`
|
||||
- Processes HTML files
|
||||
- Handles assets
|
||||
- Runs `npm run startTs` for server-side code
|
||||
|
||||
```bash
|
||||
tswatch website
|
||||
```
|
||||
|
||||
### `tswatch test`
|
||||
|
||||
Runs `npm run test2` whenever files change. Useful for projects with custom test scripts.
|
||||
|
||||
```bash
|
||||
tswatch test
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
tswatch expects certain project structures depending on the mode:
|
||||
|
||||
### NPM/Node Projects
|
||||
|
||||
```
|
||||
project/
|
||||
├── ts/ # TypeScript source files
|
||||
├── test/ # Test files
|
||||
└── package.json # With "test" script
|
||||
```
|
||||
|
||||
### Element Projects
|
||||
|
||||
```
|
||||
project/
|
||||
├── ts/ # Backend TypeScript
|
||||
├── ts_web/ # Frontend TypeScript
|
||||
├── html/ # HTML templates
|
||||
│ └── index.ts # Entry point
|
||||
└── dist_watch/ # Output directory (auto-created)
|
||||
```
|
||||
|
||||
### Website Projects
|
||||
|
||||
```
|
||||
project/
|
||||
├── ts/ # Backend TypeScript
|
||||
├── ts_web/ # Frontend TypeScript
|
||||
│ └── index.ts # Entry point
|
||||
├── html/ # HTML files
|
||||
│ └── index.html
|
||||
├── assets/ # Static assets
|
||||
└── dist_serve/ # Output directory
|
||||
```
|
||||
|
||||
## Programmatic API
|
||||
|
||||
### Basic Usage
|
||||
### Basic Usage with Config
|
||||
|
||||
```typescript
|
||||
import { TsWatch } from '@git.zone/tswatch';
|
||||
|
||||
// Create and start a watcher
|
||||
const watcher = 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,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await watcher.start();
|
||||
|
||||
// Stop when done
|
||||
// Later: stop watching
|
||||
await watcher.stop();
|
||||
```
|
||||
|
||||
### Available Watch Modes
|
||||
### Load from Config File
|
||||
|
||||
The `TsWatch` class accepts the following modes:
|
||||
```typescript
|
||||
import { TsWatch } from '@git.zone/tswatch';
|
||||
|
||||
| Mode | Description |
|
||||
|------|-------------|
|
||||
| `node` | Runs `npm test` on changes (default) |
|
||||
| `test` | Runs `npm run test2` on changes |
|
||||
| `element` | Web component development with dev server |
|
||||
| `service` | Runs `npm run startTs` for services |
|
||||
| `website` | Full website mode with bundling |
|
||||
| `echo` | Test mode that runs `npm -v` |
|
||||
// Load configuration from npmextra.json
|
||||
const watcher = TsWatch.fromConfig();
|
||||
|
||||
### Custom Watchers
|
||||
if (watcher) {
|
||||
await watcher.start();
|
||||
}
|
||||
```
|
||||
|
||||
For more granular control, use the `Watcher` class directly:
|
||||
### 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 customWatcher = new Watcher({
|
||||
filePathToWatch: './src',
|
||||
commandToExecute: 'npm run build',
|
||||
timeout: 5000 // Optional timeout in ms
|
||||
// Create from config object
|
||||
const watcher = Watcher.fromConfig({
|
||||
name: 'my-watcher',
|
||||
watch: ['./src/**/*', './lib/**/*'],
|
||||
command: 'npm run compile',
|
||||
restart: true,
|
||||
});
|
||||
|
||||
await customWatcher.start();
|
||||
await watcher.start();
|
||||
```
|
||||
|
||||
### Using Function Callbacks
|
||||
@@ -179,79 +267,126 @@ await customWatcher.start();
|
||||
import { Watcher } from '@git.zone/tswatch';
|
||||
|
||||
const watcher = new Watcher({
|
||||
filePathToWatch: './src',
|
||||
name: 'custom-handler',
|
||||
filePathToWatch: './src/**/*',
|
||||
functionToCall: async () => {
|
||||
console.log('Files changed!');
|
||||
// Your custom logic here
|
||||
}
|
||||
console.log('Files changed! Running custom logic...');
|
||||
// Your custom build/test/deploy logic here
|
||||
},
|
||||
debounce: 500,
|
||||
runOnStart: true,
|
||||
});
|
||||
|
||||
await watcher.start();
|
||||
```
|
||||
|
||||
### Watcher Options
|
||||
## 📁 Project Structures
|
||||
|
||||
| Option | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `filePathToWatch` | `string` | Path to watch for changes |
|
||||
| `commandToExecute` | `string` | Shell command to run on changes |
|
||||
| `functionToCall` | `() => Promise<any>` | Async function to call on changes |
|
||||
| `timeout` | `number` | Optional timeout in milliseconds |
|
||||
### NPM Package / Node.js Library
|
||||
|
||||
## Development Server
|
||||
```
|
||||
project/
|
||||
├── ts/ # TypeScript source files
|
||||
├── test/ # Test files
|
||||
├── package.json # With "test" script
|
||||
└── npmextra.json # tswatch config
|
||||
```
|
||||
|
||||
Element mode includes a built-in development server:
|
||||
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
|
||||
- **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
|
||||
- **Features**: CORS enabled, gzip compression, live reload
|
||||
- **Serve directory**: `./dist_watch/`
|
||||
- **Serve Directory**: `./dist_watch/`
|
||||
- **Live Reload**: Enabled
|
||||
|
||||
Access your project at `http://localhost:3002` when running in element mode.
|
||||
## 🔧 Configuration Tips
|
||||
|
||||
## Configuration Tips
|
||||
|
||||
1. **TypeScript Config**: Ensure your `tsconfig.json` is properly configured for your target environment
|
||||
2. **Package Scripts**: Define appropriate scripts in `package.json`:
|
||||
- `test`: For npm mode
|
||||
- `test2`: For test mode
|
||||
- `startTs`: For service/website modes
|
||||
- `build`: For general compilation
|
||||
|
||||
3. **File Organization**: Keep TypeScript files in `ts/` (backend) and `ts_web/` (frontend) directories
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### Developing a Node.js Library
|
||||
|
||||
```bash
|
||||
tswatch npm
|
||||
```
|
||||
|
||||
Automatically runs tests when you modify source files.
|
||||
|
||||
### Building a Web Component
|
||||
|
||||
```bash
|
||||
tswatch element
|
||||
```
|
||||
|
||||
Get instant feedback with live reload while developing custom elements.
|
||||
|
||||
### Creating a Backend Service
|
||||
|
||||
```bash
|
||||
tswatch service
|
||||
```
|
||||
|
||||
Automatically restart your service on code changes.
|
||||
|
||||
### Full-Stack Web Application
|
||||
|
||||
```bash
|
||||
tswatch website
|
||||
```
|
||||
|
||||
Handle both frontend and backend compilation with asset processing.
|
||||
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
|
||||
|
||||
|
||||
105
test/test.ts
105
test/test.ts
@@ -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();
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tswatch',
|
||||
version: '2.3.13',
|
||||
version: '3.0.0',
|
||||
description: 'A development tool for automatically watching and re-compiling TypeScript projects upon detecting file changes, enhancing developer workflows.'
|
||||
}
|
||||
|
||||
12
ts/index.ts
12
ts/index.ts
@@ -1,7 +1,11 @@
|
||||
import * as early from '@push.rocks/early';
|
||||
early.start('tswatch');
|
||||
export * from './tswatch.classes.tswatch.js';
|
||||
export * from './tswatch.cli.js';
|
||||
early.stop();
|
||||
|
||||
export * from './tswatch.classes.watcher.js';
|
||||
export * from './tswatch.classes.tswatch.js';
|
||||
export * from './tswatch.classes.watcher.js';
|
||||
export * from './tswatch.classes.confighandler.js';
|
||||
export * from './tswatch.cli.js';
|
||||
export * from './tswatch.init.js';
|
||||
export * from './interfaces/index.js';
|
||||
|
||||
early.stop();
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './interfaces.watchmodes.js';
|
||||
export * from './interfaces.config.js';
|
||||
|
||||
61
ts/interfaces/interfaces.config.ts
Normal file
61
ts/interfaces/interfaces.config.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Configuration for a single watcher
|
||||
*/
|
||||
export interface IWatcherConfig {
|
||||
/** Name for this watcher (used in logging) */
|
||||
name: string;
|
||||
/** Glob pattern(s) to watch */
|
||||
watch: string | string[];
|
||||
/** Shell command to execute on changes */
|
||||
command?: string;
|
||||
/** If true, kill previous process before restarting (default: true) */
|
||||
restart?: boolean;
|
||||
/** Debounce delay in ms (default: 300) */
|
||||
debounce?: number;
|
||||
/** If true, run the command immediately on start (default: true) */
|
||||
runOnStart?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for the development server
|
||||
*/
|
||||
export interface IServerConfig {
|
||||
/** Whether the server is enabled */
|
||||
enabled: boolean;
|
||||
/** Server port (default: 3002) */
|
||||
port?: number;
|
||||
/** Directory to serve (default: ./dist_watch/) */
|
||||
serveDir?: string;
|
||||
/** Whether to inject live reload script (default: true) */
|
||||
liveReload?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for a bundle operation
|
||||
*/
|
||||
export interface IBundleConfig {
|
||||
/** Name for this bundle (used in logging) */
|
||||
name?: string;
|
||||
/** Entry point file */
|
||||
from: string;
|
||||
/** Output file */
|
||||
to: string;
|
||||
/** Additional patterns to watch that trigger this bundle */
|
||||
watchPatterns?: string[];
|
||||
/** If true, trigger server reload after bundling (default: true) */
|
||||
triggerReload?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main tswatch configuration
|
||||
*/
|
||||
export interface ITswatchConfig {
|
||||
/** Array of watcher configurations */
|
||||
watchers?: IWatcherConfig[];
|
||||
/** Development server configuration */
|
||||
server?: IServerConfig;
|
||||
/** Bundle configurations */
|
||||
bundles?: IBundleConfig[];
|
||||
/** Use a preset configuration (overridden by explicit watchers/server/bundles) */
|
||||
preset?: 'element' | 'website' | 'npm' | 'service' | 'test';
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export type TWatchModes = 'test' | 'node' | 'service' | 'element' | 'website' | 'echo';
|
||||
185
ts/tswatch.classes.confighandler.ts
Normal file
185
ts/tswatch.classes.confighandler.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -3,221 +3,185 @@ import * as paths from './tswatch.paths.js';
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
|
||||
import { Watcher } from './tswatch.classes.watcher.js';
|
||||
import { ConfigHandler } from './tswatch.classes.confighandler.js';
|
||||
import { logger } from './tswatch.logging.js';
|
||||
|
||||
// Create smartfs instance for directory operations
|
||||
const smartfs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
|
||||
|
||||
/**
|
||||
* Lists all folders in a directory
|
||||
* TsWatch - Config-driven file watcher
|
||||
*
|
||||
* Reads configuration from npmextra.json under the key '@git.zone/tswatch'
|
||||
* and sets up watchers, bundles, and dev server accordingly.
|
||||
*/
|
||||
const listFolders = async (dirPath: string): Promise<string[]> => {
|
||||
const entries = await smartfs.directory(dirPath).list();
|
||||
return entries
|
||||
.filter((entry) => entry.isDirectory)
|
||||
.map((entry) => entry.name);
|
||||
};
|
||||
|
||||
export class TsWatch {
|
||||
public watchmode: interfaces.TWatchModes;
|
||||
public config: interfaces.ITswatchConfig;
|
||||
public watcherMap = new plugins.lik.ObjectMap<Watcher>();
|
||||
public typedserver: plugins.typedserver.TypedServer;
|
||||
public typedserver: plugins.typedserver.TypedServer | null = null;
|
||||
|
||||
constructor(watchmodeArg: interfaces.TWatchModes) {
|
||||
this.watchmode = watchmodeArg;
|
||||
private tsbundle = new plugins.tsbundle.TsBundle();
|
||||
private htmlHandler = new plugins.tsbundle.HtmlHandler();
|
||||
private assetsHandler = new plugins.tsbundle.AssetsHandler();
|
||||
|
||||
constructor(configArg: interfaces.ITswatchConfig) {
|
||||
this.config = configArg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create TsWatch from npmextra.json configuration
|
||||
*/
|
||||
public static fromConfig(cwdArg?: string): TsWatch | null {
|
||||
const configHandler = new ConfigHandler(cwdArg);
|
||||
const config = configHandler.loadConfig();
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new TsWatch(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* starts the TsWatch instance
|
||||
*/
|
||||
public async start() {
|
||||
const tsbundle = new plugins.tsbundle.TsBundle();
|
||||
const assetsHandler = new plugins.tsbundle.AssetsHandler();
|
||||
const htmlHandler = new plugins.tsbundle.HtmlHandler();
|
||||
switch (this.watchmode) {
|
||||
case 'test':
|
||||
/**
|
||||
* this strategy runs test whenever there is a change in the ts directory
|
||||
*/
|
||||
this.watcherMap.add(
|
||||
new Watcher({
|
||||
filePathToWatch: paths.cwd,
|
||||
commandToExecute: 'npm run test2',
|
||||
timeout: null,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'node':
|
||||
this.watcherMap.add(
|
||||
new Watcher({
|
||||
filePathToWatch: paths.cwd,
|
||||
commandToExecute: 'npm run test',
|
||||
timeout: null,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'element':
|
||||
await (async () => {
|
||||
/**
|
||||
* this strategy runs a standard server and bundles the ts files to a dist_watch directory
|
||||
*/
|
||||
// lets create a standard server
|
||||
logger.log(
|
||||
'info',
|
||||
'bundling TypeScript files to "dist_watch" Note: This is for development only!',
|
||||
);
|
||||
this.typedserver = new plugins.typedserver.TypedServer({
|
||||
cors: true,
|
||||
injectReload: true,
|
||||
serveDir: plugins.path.join(paths.cwd, './dist_watch/'),
|
||||
port: 3002,
|
||||
compression: true,
|
||||
spaFallback: true,
|
||||
securityHeaders: {
|
||||
crossOriginOpenerPolicy: 'same-origin',
|
||||
crossOriginEmbedderPolicy: 'require-corp',
|
||||
},
|
||||
});
|
||||
logger.log('info', 'Starting tswatch with config-driven mode');
|
||||
|
||||
const bundleAndReloadElement = async () => {
|
||||
await tsbundle.build(paths.cwd, './html/index.ts', './dist_watch/bundle.js', {
|
||||
bundler: 'esbuild',
|
||||
});
|
||||
await this.typedserver.reload();
|
||||
};
|
||||
this.watcherMap.add(
|
||||
new Watcher({
|
||||
filePathToWatch: plugins.path.join(paths.cwd, './ts_web/'),
|
||||
functionToCall: async () => {
|
||||
await bundleAndReloadElement();
|
||||
},
|
||||
timeout: null,
|
||||
}),
|
||||
);
|
||||
|
||||
// lets get the other ts folders
|
||||
let tsfolders = await listFolders(paths.cwd);
|
||||
tsfolders = tsfolders.filter(
|
||||
(itemArg) => itemArg.startsWith('ts') && itemArg !== 'ts_web',
|
||||
);
|
||||
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
});
|
||||
for (const tsfolder of tsfolders) {
|
||||
logger.log('info', `creating watcher for folder ${tsfolder}`);
|
||||
this.watcherMap.add(
|
||||
new Watcher({
|
||||
filePathToWatch: plugins.path.join(paths.cwd, `./${tsfolder}/`),
|
||||
functionToCall: async () => {
|
||||
logger.log('info', `building ${tsfolder}`);
|
||||
await smartshellInstance.exec(`(cd ${paths.cwd} && npm run build)`);
|
||||
await bundleAndReloadElement();
|
||||
},
|
||||
timeout: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
this.watcherMap.add(
|
||||
new Watcher({
|
||||
filePathToWatch: plugins.path.join(paths.cwd, './html/'),
|
||||
functionToCall: async () => {
|
||||
await htmlHandler.processHtml({
|
||||
from: plugins.path.join(paths.cwd, './html/index.html'),
|
||||
to: plugins.path.join(paths.cwd, './dist_watch/index.html'),
|
||||
minify: false,
|
||||
});
|
||||
await bundleAndReloadElement();
|
||||
},
|
||||
timeout: null,
|
||||
}),
|
||||
);
|
||||
})();
|
||||
break;
|
||||
case 'website':
|
||||
await (async () => {
|
||||
const websiteExecution = new plugins.smartshell.SmartExecution('npm run startTs');
|
||||
const bundleAndReloadWebsite = async () => {
|
||||
await tsbundle.build(paths.cwd, './ts_web/index.ts', './dist_serve/bundle.js', {
|
||||
bundler: 'esbuild',
|
||||
});
|
||||
};
|
||||
let tsfolders = await listFolders(paths.cwd);
|
||||
tsfolders = tsfolders.filter(
|
||||
(itemArg) => itemArg.startsWith('ts') && itemArg !== 'ts_web',
|
||||
);
|
||||
for (const tsfolder of tsfolders) {
|
||||
this.watcherMap.add(
|
||||
new Watcher({
|
||||
filePathToWatch: plugins.path.join(paths.cwd, `./${tsfolder}/`),
|
||||
functionToCall: async () => {
|
||||
await websiteExecution.restart();
|
||||
await bundleAndReloadWebsite();
|
||||
},
|
||||
timeout: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
this.watcherMap.add(
|
||||
new Watcher({
|
||||
filePathToWatch: plugins.path.join(paths.cwd, './ts_web/'),
|
||||
functionToCall: async () => {
|
||||
await bundleAndReloadWebsite();
|
||||
},
|
||||
timeout: null,
|
||||
}),
|
||||
);
|
||||
this.watcherMap.add(
|
||||
new Watcher({
|
||||
filePathToWatch: plugins.path.join(paths.cwd, './html/'),
|
||||
functionToCall: async () => {
|
||||
await htmlHandler.processHtml({
|
||||
from: plugins.path.join(paths.cwd, './html/index.html'),
|
||||
to: plugins.path.join(paths.cwd, './dist_serve/index.html'),
|
||||
minify: false,
|
||||
});
|
||||
await bundleAndReloadWebsite();
|
||||
},
|
||||
timeout: null,
|
||||
}),
|
||||
);
|
||||
this.watcherMap.add(
|
||||
new Watcher({
|
||||
filePathToWatch: plugins.path.join(paths.cwd, './assets/'),
|
||||
functionToCall: async () => {
|
||||
await assetsHandler.processAssets();
|
||||
await bundleAndReloadWebsite();
|
||||
},
|
||||
timeout: null,
|
||||
}),
|
||||
);
|
||||
})();
|
||||
break;
|
||||
case 'service':
|
||||
this.watcherMap.add(
|
||||
new Watcher({
|
||||
filePathToWatch: plugins.path.join(paths.cwd, './ts/'),
|
||||
commandToExecute: 'npm run startTs',
|
||||
timeout: null,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'echo':
|
||||
const tsWatchInstanceEchoSomething = new Watcher({
|
||||
filePathToWatch: plugins.path.join(paths.cwd, './ts'),
|
||||
commandToExecute: 'npm -v',
|
||||
timeout: null,
|
||||
});
|
||||
this.watcherMap.add(tsWatchInstanceEchoSomething);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
// Start server if configured
|
||||
if (this.config.server?.enabled) {
|
||||
await this.startServer();
|
||||
}
|
||||
this.watcherMap.forEach(async (watcher) => {
|
||||
|
||||
// Setup bundles and their watchers
|
||||
if (this.config.bundles && this.config.bundles.length > 0) {
|
||||
await this.setupBundles();
|
||||
}
|
||||
|
||||
// Setup watchers from config
|
||||
if (this.config.watchers && this.config.watchers.length > 0) {
|
||||
await this.setupWatchers();
|
||||
}
|
||||
|
||||
// Start all watchers
|
||||
await this.watcherMap.forEach(async (watcher) => {
|
||||
await watcher.start();
|
||||
});
|
||||
|
||||
// Start server after watchers are ready
|
||||
if (this.typedserver) {
|
||||
await this.typedserver.start();
|
||||
logger.log('ok', `Dev server started on port ${this.config.server?.port || 3002}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the development server
|
||||
*/
|
||||
private async startServer() {
|
||||
const serverConfig = this.config.server!;
|
||||
const port = serverConfig.port || 3002;
|
||||
const serveDir = serverConfig.serveDir || './dist_watch/';
|
||||
|
||||
logger.log('info', `Setting up dev server on port ${port}, serving ${serveDir}`);
|
||||
|
||||
this.typedserver = new plugins.typedserver.TypedServer({
|
||||
cors: true,
|
||||
injectReload: serverConfig.liveReload !== false,
|
||||
serveDir: plugins.path.join(paths.cwd, serveDir),
|
||||
port: port,
|
||||
compression: true,
|
||||
spaFallback: true,
|
||||
securityHeaders: {
|
||||
crossOriginOpenerPolicy: 'same-origin',
|
||||
crossOriginEmbedderPolicy: 'require-corp',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup bundle watchers
|
||||
*/
|
||||
private async setupBundles() {
|
||||
for (const bundleConfig of this.config.bundles!) {
|
||||
const name = bundleConfig.name || `bundle-${bundleConfig.from}`;
|
||||
logger.log('info', `Setting up bundle: ${name}`);
|
||||
|
||||
// Determine what patterns to watch
|
||||
const watchPatterns = bundleConfig.watchPatterns || [
|
||||
plugins.path.dirname(bundleConfig.from) + '/**/*',
|
||||
];
|
||||
|
||||
// Create the bundle function
|
||||
const bundleFunction = async () => {
|
||||
logger.log('info', `[${name}] bundling...`);
|
||||
|
||||
// Determine bundle type based on file extension
|
||||
const fromPath = bundleConfig.from;
|
||||
const toPath = bundleConfig.to;
|
||||
|
||||
if (fromPath.endsWith('.html')) {
|
||||
// HTML processing
|
||||
await this.htmlHandler.processHtml({
|
||||
from: plugins.path.join(paths.cwd, fromPath),
|
||||
to: plugins.path.join(paths.cwd, toPath),
|
||||
minify: false,
|
||||
});
|
||||
} else if (fromPath.endsWith('/') || !fromPath.includes('.')) {
|
||||
// Assets directory copy
|
||||
await this.assetsHandler.processAssets();
|
||||
} else {
|
||||
// TypeScript bundling
|
||||
await this.tsbundle.build(paths.cwd, fromPath, toPath, {
|
||||
bundler: 'esbuild',
|
||||
});
|
||||
}
|
||||
|
||||
logger.log('ok', `[${name}] bundle complete`);
|
||||
|
||||
// Trigger reload if configured and server is running
|
||||
if (bundleConfig.triggerReload !== false && this.typedserver) {
|
||||
await this.typedserver.reload();
|
||||
}
|
||||
};
|
||||
|
||||
// Run initial bundle
|
||||
await bundleFunction();
|
||||
|
||||
// Create watcher for this bundle
|
||||
this.watcherMap.add(
|
||||
new Watcher({
|
||||
name: name,
|
||||
filePathToWatch: watchPatterns.map((p) => plugins.path.join(paths.cwd, p)),
|
||||
functionToCall: bundleFunction,
|
||||
runOnStart: false, // Already ran above
|
||||
debounce: 300,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup watchers from config
|
||||
*/
|
||||
private async setupWatchers() {
|
||||
for (const watcherConfig of this.config.watchers!) {
|
||||
logger.log('info', `Setting up watcher: ${watcherConfig.name}`);
|
||||
|
||||
// Convert watch paths to absolute
|
||||
const watchPaths = Array.isArray(watcherConfig.watch)
|
||||
? watcherConfig.watch
|
||||
: [watcherConfig.watch];
|
||||
|
||||
const absolutePaths = watchPaths.map((p) => plugins.path.join(paths.cwd, p));
|
||||
|
||||
this.watcherMap.add(
|
||||
new Watcher({
|
||||
name: watcherConfig.name,
|
||||
filePathToWatch: absolutePaths,
|
||||
commandToExecute: watcherConfig.command,
|
||||
restart: watcherConfig.restart ?? true,
|
||||
debounce: watcherConfig.debounce ?? 300,
|
||||
runOnStart: watcherConfig.runOnStart ?? true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,7 +192,7 @@ export class TsWatch {
|
||||
if (this.typedserver) {
|
||||
await this.typedserver.stop();
|
||||
}
|
||||
this.watcherMap.forEach(async (watcher) => {
|
||||
await this.watcherMap.forEach(async (watcher) => {
|
||||
await watcher.stop();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
import * as plugins from './tswatch.plugins.js';
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
import { logger } from './tswatch.logging.js';
|
||||
|
||||
export interface IWatcherConstructorOptions {
|
||||
filePathToWatch: string;
|
||||
/** Name for this watcher (used in logging) */
|
||||
name?: string;
|
||||
/** Path(s) to watch - can be a single path or array */
|
||||
filePathToWatch: string | string[];
|
||||
/** Shell command to execute on changes */
|
||||
commandToExecute?: string;
|
||||
/** Function to call on changes */
|
||||
functionToCall?: () => Promise<any>;
|
||||
/** Timeout for the watcher */
|
||||
timeout?: number;
|
||||
/** If true, kill previous process before restarting (default: true) */
|
||||
restart?: boolean;
|
||||
/** Debounce delay in ms (default: 300) */
|
||||
debounce?: number;
|
||||
/** If true, run the command immediately on start (default: true) */
|
||||
runOnStart?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,53 +35,148 @@ export class Watcher {
|
||||
private currentExecution: plugins.smartshell.IExecResultStreaming;
|
||||
private smartwatchInstance = new plugins.smartwatch.Smartwatch([]);
|
||||
private options: IWatcherConstructorOptions;
|
||||
private debounceTimer: NodeJS.Timeout | null = null;
|
||||
private isExecuting = false;
|
||||
private pendingExecution = false;
|
||||
|
||||
constructor(optionsArg: IWatcherConstructorOptions) {
|
||||
this.options = optionsArg;
|
||||
this.options = {
|
||||
restart: true,
|
||||
debounce: 300,
|
||||
runOnStart: true,
|
||||
...optionsArg,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Watcher from config
|
||||
*/
|
||||
public static fromConfig(config: interfaces.IWatcherConfig): Watcher {
|
||||
const watchPaths = Array.isArray(config.watch) ? config.watch : [config.watch];
|
||||
return new Watcher({
|
||||
name: config.name,
|
||||
filePathToWatch: watchPaths,
|
||||
commandToExecute: config.command,
|
||||
restart: config.restart ?? true,
|
||||
debounce: config.debounce ?? 300,
|
||||
runOnStart: config.runOnStart ?? true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the watcher name for logging
|
||||
*/
|
||||
private getName(): string {
|
||||
return this.options.name || 'unnamed';
|
||||
}
|
||||
|
||||
/**
|
||||
* start the file
|
||||
*/
|
||||
public async start() {
|
||||
logger.log('info', `trying to start watcher for ${this.options.filePathToWatch}`);
|
||||
const name = this.getName();
|
||||
logger.log('info', `[${name}] starting watcher`);
|
||||
await this.setupCleanup();
|
||||
console.log(`Looking at ${this.options.filePathToWatch} for changes`);
|
||||
// Convert directory path to glob pattern for smartwatch
|
||||
const watchPath = this.options.filePathToWatch.endsWith('/')
|
||||
? `${this.options.filePathToWatch}**/*`
|
||||
: `${this.options.filePathToWatch}/**/*`;
|
||||
this.smartwatchInstance.add([watchPath]);
|
||||
|
||||
// Convert paths to glob patterns
|
||||
const paths = Array.isArray(this.options.filePathToWatch)
|
||||
? this.options.filePathToWatch
|
||||
: [this.options.filePathToWatch];
|
||||
|
||||
const watchPatterns = paths.map((p) => {
|
||||
// Convert directory path to glob pattern for smartwatch
|
||||
if (p.endsWith('/')) {
|
||||
return `${p}**/*`;
|
||||
}
|
||||
// If it's already a glob pattern, use as-is
|
||||
if (p.includes('*')) {
|
||||
return p;
|
||||
}
|
||||
// Otherwise assume it's a directory
|
||||
return `${p}/**/*`;
|
||||
});
|
||||
|
||||
logger.log('info', `[${name}] watching patterns: ${watchPatterns.join(', ')}`);
|
||||
this.smartwatchInstance.add(watchPatterns);
|
||||
await this.smartwatchInstance.start();
|
||||
|
||||
const changeObservable = await this.smartwatchInstance.getObservableFor('change');
|
||||
changeObservable.subscribe(() => {
|
||||
this.updateCurrentExecution();
|
||||
this.handleChange();
|
||||
});
|
||||
await this.updateCurrentExecution();
|
||||
logger.log('info', `watcher started for ${this.options.filePathToWatch}`);
|
||||
|
||||
// Run on start if configured
|
||||
if (this.options.runOnStart) {
|
||||
await this.executeCommand();
|
||||
}
|
||||
|
||||
logger.log('info', `[${name}] watcher started`);
|
||||
}
|
||||
|
||||
/**
|
||||
* updates the current execution
|
||||
* Handle file change with debouncing
|
||||
*/
|
||||
private async updateCurrentExecution() {
|
||||
if (this.options.commandToExecute) {
|
||||
if (this.currentExecution) {
|
||||
logger.log('ok', `reexecuting ${this.options.commandToExecute}`);
|
||||
this.currentExecution.kill();
|
||||
} else {
|
||||
logger.log('ok', `executing ${this.options.commandToExecute} for the first time`);
|
||||
private handleChange() {
|
||||
const name = this.getName();
|
||||
|
||||
// Clear existing debounce timer
|
||||
if (this.debounceTimer) {
|
||||
clearTimeout(this.debounceTimer);
|
||||
}
|
||||
|
||||
// Set new debounce timer
|
||||
this.debounceTimer = setTimeout(async () => {
|
||||
this.debounceTimer = null;
|
||||
|
||||
// If currently executing and not in restart mode, mark pending
|
||||
if (this.isExecuting && !this.options.restart) {
|
||||
logger.log('info', `[${name}] change detected, queuing execution`);
|
||||
this.pendingExecution = true;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.executeCommand();
|
||||
|
||||
// If there was a pending execution, run it
|
||||
if (this.pendingExecution) {
|
||||
this.pendingExecution = false;
|
||||
await this.executeCommand();
|
||||
}
|
||||
}, this.options.debounce);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the command or function
|
||||
*/
|
||||
private async executeCommand() {
|
||||
const name = this.getName();
|
||||
|
||||
if (this.options.commandToExecute) {
|
||||
if (this.currentExecution && this.options.restart) {
|
||||
logger.log('ok', `[${name}] restarting: ${this.options.commandToExecute}`);
|
||||
this.currentExecution.kill();
|
||||
} else if (!this.currentExecution) {
|
||||
logger.log('ok', `[${name}] executing: ${this.options.commandToExecute}`);
|
||||
}
|
||||
|
||||
this.isExecuting = true;
|
||||
this.currentExecution = await this.smartshellInstance.execStreaming(
|
||||
this.options.commandToExecute,
|
||||
);
|
||||
} else {
|
||||
console.log('no executionCommand set');
|
||||
|
||||
// Track when execution completes
|
||||
this.currentExecution.childProcess.on('exit', () => {
|
||||
this.isExecuting = false;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.options.functionToCall) {
|
||||
this.options.functionToCall();
|
||||
} else {
|
||||
console.log('no functionToCall set.');
|
||||
this.isExecuting = true;
|
||||
try {
|
||||
await this.options.functionToCall();
|
||||
} finally {
|
||||
this.isExecuting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +211,9 @@ export class Watcher {
|
||||
* stops the watcher
|
||||
*/
|
||||
public async stop() {
|
||||
if (this.debounceTimer) {
|
||||
clearTimeout(this.debounceTimer);
|
||||
}
|
||||
await this.smartwatchInstance.stop();
|
||||
if (this.currentExecution && !this.currentExecution.childProcess.killed) {
|
||||
this.currentExecution.kill();
|
||||
|
||||
@@ -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
199
ts/tswatch.init.ts
Normal 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();
|
||||
};
|
||||
@@ -2,20 +2,22 @@
|
||||
import * as path from 'path';
|
||||
export { path };
|
||||
|
||||
// @gitzone scope
|
||||
// @git.zone scope
|
||||
import * as tsbundle from '@git.zone/tsbundle';
|
||||
export { tsbundle };
|
||||
|
||||
// @apiglobal scope
|
||||
// @api.global scope
|
||||
import * as typedserver from '@api.global/typedserver';
|
||||
|
||||
export { typedserver };
|
||||
|
||||
// @pushrocks scope
|
||||
// @push.rocks scope
|
||||
import * as lik from '@push.rocks/lik';
|
||||
import * as npmextra from '@push.rocks/npmextra';
|
||||
import * as smartcli from '@push.rocks/smartcli';
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartfs from '@push.rocks/smartfs';
|
||||
import * as smartinteract from '@push.rocks/smartinteract';
|
||||
import * as smartlog from '@push.rocks/smartlog';
|
||||
import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
|
||||
import * as smartshell from '@push.rocks/smartshell';
|
||||
@@ -24,9 +26,11 @@ import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||
|
||||
export {
|
||||
lik,
|
||||
npmextra,
|
||||
smartcli,
|
||||
smartdelay,
|
||||
smartfs,
|
||||
smartinteract,
|
||||
smartlog,
|
||||
smartlogDestinationLocal,
|
||||
smartshell,
|
||||
|
||||
Reference in New Issue
Block a user