├── .gitignore ├── .nvmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── client.d.ts ├── client.js ├── example ├── main.js ├── main.ts └── tsconfig.json ├── index.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── client │ ├── client.ts │ └── index.ts ├── lib │ ├── @types │ │ ├── fs-extra.ts │ │ └── string-argv.d.ts │ ├── args-manager.ts │ ├── compiler-provider.ts │ ├── debounce.ts │ ├── killer.ts │ ├── runner.ts │ ├── stdout-manipulator.ts │ └── tsc-watch.ts └── test │ ├── args-manager.test.ts │ ├── client.test.ts │ ├── compiler-provider.test.ts │ ├── driver.ts │ ├── fixtures │ ├── failing.ts │ └── passing.ts │ ├── stdout-manipulator.test.ts │ ├── test-utils.ts │ └── tsc-watch.test.ts ├── tsc-watch-client-example.js ├── tsconfig-client.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | node_modules/ 4 | npm-debug.log 5 | tmp/ 6 | dist/ 7 | types/ 8 | RELEASE_PROCESS.md -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.22.10 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '17' 4 | - '16' 5 | - '14' 6 | - '12' 7 | - '10' 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @gilamran/tsc-watch CHANGELOG 2 | ## v7.1.1 - 28/05/2025 3 | 4 | - feature: Added support for tsgo (native compilation)) 5 | 6 | # @gilamran/tsc-watch CHANGELOG 7 | ## v7.0.0 - 25/05/2025 8 | 9 | - feature: Globally installed `tsc-watch` will use the local compiler if it exists, otherwise it will use the global `tsc` compiler. 10 | - Update existing test code to pass the new version of typescript 11 | - Update existing test code to work in Mac OS(M1 chip) 12 | 13 | # @gilamran/tsc-watch CHANGELOG 14 | ## v6.3.1 - 18/05/2025 15 | 16 | - big fix: off-by-one error tsc-watch. thanks to @rodw for the PR 17 | # @gilamran/tsc-watch CHANGELOG 18 | ## v6.3.0 - 16/04/2024 19 | 20 | - Added `--noWatch` argument 21 | 22 | ## v6.2.1 - 13/11/2024 23 | - bug fix: Fixed new version of Typescript clear codes. thanks to @staszekj 24 | 25 | ## v6.2.0 - 28/03/2024 26 | - feature: Using prepublish. thanks to @llllvvuu 27 | 28 | ## v6.1.0 - 27/03/2024 29 | - feature: --noEmit. thanks to @llllvvuu 30 | 31 | ## v6.0.5 - 27/03/2024 32 | - fix: error TS6369: Option '--build' must be the first command line argument. thanks to @llllvvuu 33 | 34 | ## v6.0.4 - 29/04/2023 35 | - Automate CRLF to LF conversion 36 | 37 | ## v6.0.3 - 29/04/2023 38 | - Fixed (Again CRLF to LF) 39 | 40 | ## v6.0.1 - 28/04/2023 41 | - Upgraded to the latest versions of dev dependencies 42 | - fixed failing test on node 18 43 | 44 | ## v6.0.0 - 12/09/2022 45 | - killing processes once, and waiting for them to complete before starting a new one 46 | - added signalEmittedFiles, thanks to @pp0rtal 47 | 48 | ## v5.0.0 - 12/03/2022 49 | - Converted to Typescript 50 | ## v5.0.3 - 07/04/2022 51 | 52 | - Fixed DOS line endings [issue](https://github.com/gilamran/tsc-watch/issues/149)) thanks to @sluukkonen 53 | 54 | ## v5.0.2 - 03/04/2022 55 | 56 | - Fixed black on black issue (Thanks to @bonjourjoel for the [issue](https://github.com/gilamran/tsc-watch/issues/142)) 57 | 58 | ## v5.0.1 - 03/04/2022 59 | 60 | - bumped to version 5.0.1 to prevent dev version as the latest version 61 | 62 | ## v4.6.2 - 10/01/2022 63 | 64 | - Added `file_emitted` signal support when `--listEmittedFiles` is used [issue](https://github.com/gilamran/tsc-watch/issues/138) 65 | - Added client new event `file_emitted` with the emitted file path 66 | 67 | ## v4.6.1 - 10/3/2022 68 | 69 | - Added `--maxNodeMem` param to set manually node allocated memory [issue](https://github.com/gilamran/tsc-watch/issues/137) - (Thanks to @pp0rtal for the idea and the PR!) 70 | 71 | ## v4.6.0 - 20/12/2021 72 | 73 | - Added `silent` option - (Thanks to @axtk for the idea and @fmvilas for the PR) 74 | 75 | ## v4.5.0 - 19/8/2021 76 | 77 | - Added `onCompilationStarted` option - (Thanks to @axtk for the idea and @dko-slapdash for the PR) 78 | - Fix: enable unit tests which were turned off accidentally 79 | 80 | ## v4.4.0 - 26/5/ 2021 81 | 82 | - tsc-watch is now listening to message and reacts to them 83 | 84 | ## v4.3.1 - 26/5/2021 85 | 86 | - Fix: compiler resolving - (Thanks to @merceyz for the PR) 87 | 88 | ## v4.2.9 - 23/6/2020 89 | 90 | - Fix: upgrade cross-spawn and strip-ansi (node 8+) - (Thanks to @FauxFaux for the PR) 91 | 92 | ## v4.2.8 - 23/5/2020 93 | 94 | - Fix: spawn compiler using node, this fixes issues with yarn v2 (pnp) - (Thanks to @merceyz) 95 | 96 | ## v4.2.6 - 18/5/2020 97 | 98 | - Using number 15 instead of SIGTERM to support POSIX standard - (Thanks to @Asarew) 99 | 100 | ## v4.2.0 - 29/2/2020 101 | 102 | - Using readline instead of raw stdout buffer - (Thanks to @Janpot for the idea) 103 | 104 | ## v4.1.0 - 22/1/2020 105 | 106 | - Added the `onCompilationComplete` option - (Thanks to @ackvf for the idea) 107 | 108 | ## v4.0.0 - 19/9/2019 109 | 110 | - Terminating previous processes is now done with `SIGTERM` instead of `SIGUSR2` - (Thanks to @zontarian) 111 | 112 | ## v3.0.0 - 9/9/2019 113 | 114 | - onSuccess will run on EVERY successful compilation, also on the first success - (Thanks to @mchl-hub for the idea) 115 | 116 | ## v2.2.1 - 19/5/2019 117 | 118 | - Force kill when on windows - (Thanks to @hwwi) 119 | 120 | ## v2.2.0 - 13/5/2019 121 | 122 | - Waiting for all the child processes to showdown before closing - (Thanks to @MartinLoeper) 123 | 124 | ## v2.1.0 - 12/2/2019 125 | 126 | - Exporting TscWatchClient for multiple instance of `tsc-watch` - (Thanks to @pronebird) 127 | 128 | ## v2.0.0 - 12/2/2019 129 | 130 | - As many users requested, from now on `--onFirstSuccess` process will not get killed, only when tsc-watch is killed. (Based on @amir-arad's PR) 131 | This version fixes Issue #20, #21 and #50. 132 | 133 | ## v1.1.37 - 8/2/2019 134 | 135 | - Fixed coloring issues 136 | 137 | ## v1.1.36 - 31/1/2019 138 | 139 | - `--watch` will be added to the end of the arguments (Thanks to @barkayal) 140 | 141 | ## v1.1.35 - 28/1/2019 142 | 143 | - Clean code 144 | 145 | ## v1.1.32 - 27/11/2018 146 | 147 | - Added --noClear command to prevent clearing the screen after each compilation 148 | 149 | ## v1.0.32 - 27/11/2018 150 | 151 | - Removed chalk dependency - (Thanks to @frank-orellana) 152 | 153 | ## v1.0.31 - 27/11/2018 154 | 155 | - Upgraded ps-tree, preventing flatmap-stream attack (Thanks to @jeremyhon) 156 | 157 | ## v1.0.30 - 10/10/2018 158 | 159 | - Added custom compiler ability (Thanks to @sosoba for the PR) 160 | 161 | ## v1.0.29 - 25/09/2018 162 | 163 | - Fixed regression bug (Thanks to @onehorsetown) 164 | 165 | ## v1.0.28 - 25/09/2018 166 | 167 | - Fixed command args extraction (Thanks to @mscharley) 168 | 169 | ## v1.0.27 - 27/08/2018 170 | 171 | - Fixed process termination (Thanks to @igrayson) 172 | 173 | ## v1.0.26 - 20/07/2018 174 | 175 | - Fixed several issues with hadErrors check (Thanks to @tombousso) 176 | 177 | ## v1.0.24 - 20/07/2018 178 | 179 | - Killing onFail and onSuccess after compilation 180 | 181 | ## v1.0.23 - 1/06/2018 182 | 183 | - Typescript as a peer dependency (Thanks to @asztal) 184 | 185 | ## v1.0.22 - 1/06/2018 186 | 187 | - Added support for Typescript 2.9+ 188 | 189 | ## v1.0.21 - 16/04/2018 190 | 191 | - Removed `--noClearScreen` argument, as typescript added `--preserveWatchOutput` 192 | 193 | ## v1.0.20 - 16/04/2018 194 | 195 | - Fixed ignoring the last arg 196 | - Added `--noColors` argument 197 | - Added `--noClearScreen` argument 198 | 199 | ## v1.0.19 - 8/04/2018 200 | 201 | - Fixed passing compilation when using --pretty param (Thanks to @tomaba) 202 | 203 | ## v1.0.17 - 27/01/2018 204 | 205 | - Terminating typescript on SIGTERM event (Thanks to @amir-arad) 206 | 207 | ## v1.0.16 - 27/01/2018 208 | 209 | - Fixed stdout coloring issues on Mac (Thanks to @jonaskello) 210 | 211 | ## v1.0.15 - 18/01/2018 212 | 213 | - Added `--onFailure` argument 214 | 215 | ## v1.0.14 - 18/01/2018 216 | 217 | - Fixed Windows newline issue 218 | 219 | ## v1.0.9 - 5/12/2017 220 | 221 | - Add CHANGELOG 222 | - Fix typo in package description 223 | - Upgraded chalk version 224 | - Change main to `index.js` 225 | - Removed `yarn.lock` & `package-lock.json` 226 | - Made `index.js` executable 227 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Gil Amran 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/gilamran/tsc-watch.svg?branch=master)](https://travis-ci.com/gilamran/tsc-watch) 2 | 3 | # The nodemon for TypeScript 4 | 5 | `tsc-watch` starts the installed TypeScript compiler (`tsc`) with `--watch` parameter, with the ability to react to compilation status. 6 | `tsc-watch` was created to allow an easy dev process with TypeScript. Commonly used to restart a node server, similar to nodemon but for TypeScript. 7 | 8 | **Anything that you can do with `tsc` you can do with `tsc-watch`, the only difference is that `tsc-watch` can react to compilation status.** 9 | 10 | | Argument | Description | 11 | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | 12 | | `--onSuccess COMMAND` | Executes `COMMAND` on **every successful** compilation. | 13 | | `--onFirstSuccess COMMAND` | Executes `COMMAND` on the **first successful** compilation. | 14 | | `--onEmit COMMAND` | Executes debounced `COMMAND` on **every emitted file**, ignoring unchanged files and disregards compilation success or failure. | 15 | | `--onEmitDebounceMs DELAY` | Delay by which to debounce `--onEmit` (default: 300). | 16 | | `--onFailure COMMAND` | Executes `COMMAND` on **every failed** compilation. | 17 | | `--onCompilationStarted COMMAND` | Executes `COMMAND` on **every compilation start** event (initial and incremental). | 18 | | `--onCompilationComplete COMMAND` | Executes `COMMAND` on **every successful or failed** compilation. | 19 | | `--maxNodeMem` | Calls `node` with a specific memory limit `max_old_space_size`, to use if your project needs more memory. | 20 | | `--noColors` | By default tsc-watch adds colors the output with green
on success, and in red on failure.
Add this argument to prevent that. | 21 | | `--noWatch` | By default tsc-watch adds --watch. using this to prevent watch. | 22 | | `--noClear` | In watch mode the `tsc` compiler clears the screen before reporting
Add this argument to prevent that. | 23 | | `--signalEmittedFiles` | Will run `tsc` compiler with `--listEmittedFiles`, but hiding TSFILE lines. Use it to enable `file_emitted` event, while keeping tsc stdout silent. | 24 | | `--silent` | Do not print any messages on stdout. | 25 | | `--compiler PATH` | The `PATH` will be used instead of typescript compiler.
Default is `typescript/bin/tsc` | 26 | 27 | Notes: 28 | 29 | - That all the above `COMMAND`s will be killed on process exit. (Using `SIGTERM`) 30 | 31 | - A `COMMAND` is a single command and not multi command like `script1.sh && script2.sh` 32 | 33 | - Any child process (`COMMAND`) will be terminated before creating a new one. 34 | 35 | - To prevent the watch, use `--noWatch` or pass `--watch false` as a parameter. 36 | 37 | - tsc-watch supports tsgo (native compilation), just select the compiler path. `tsc-watch --compiler .bin/tsgo` 38 | 39 | ## Install 40 | 41 | ```sh 42 | npm install tsc-watch --save-dev 43 | ``` 44 | 45 | ```sh 46 | ## for command-line usage 47 | npm install -g typescript tsc-watch 48 | ``` 49 | 50 | ## Usage 51 | 52 | ### From Command-Line 53 | 54 | ```sh 55 | ## Watching a project (with tsconfig.json) 56 | tsc-watch --onSuccess "node ./dist/server.js" 57 | 58 | ## Beep on failure 59 | tsc-watch --onFailure "echo Beep! Compilation Failed" 60 | 61 | ## Watching a single file 62 | tsc-watch server.ts --outDir ./dist --onSuccess "node ./dist/server.js" 63 | 64 | ## Custom compiler 65 | tsc-watch --onSuccess "node ./dist/server.js" --compiler my-typescript/bin/tsc 66 | ``` 67 | 68 | ### From npm script 69 | 70 | ``` 71 | "dev-server": "tsc-watch --noClear -p ./src/tsconfig.json --onSuccess \"node ./dist/server.js\"", 72 | ``` 73 | 74 | ### From javascript 75 | 76 | You can see a detailed example [here](https://github.com/gilamran/tsc-watch/blob/master/tsc-watch-client-example.js) 77 | 78 | The client is implemented as an instance of `Node.JS`'s `EventEmitter`, with the following events: 79 | 80 | - `started` - Emitted upon the compilation start (initial or incremental). 81 | - `first_success` - Emitted upon first successful compilation. 82 | - `subsequent_success` - Emitted upon every subsequent successful compilation. 83 | - `compile_errors` - Emitted upon every failing compilation. 84 | - `file_emitted` - Emitted upon every file transpiled if `--listEmittedFiles` is used. 85 | 86 | Once subscribed to the relevant events, start the client by running `watch.start()` 87 | 88 | To kill the client, run `watch.kill()` 89 | 90 | Example usage: 91 | 92 | ```javascript 93 | // Using CommonJS: 94 | const { TscWatchClient } = require('tsc-watch/client'); 95 | // Using ES6 import: 96 | import { TscWatchClient } from 'tsc-watch/client'; 97 | 98 | const watch = new TscWatchClient(); 99 | 100 | watch.on('started', () => { 101 | console.log('Compilation started'); 102 | }); 103 | 104 | watch.on('first_success', () => { 105 | console.log('First success!'); 106 | }); 107 | 108 | watch.on('success', () => { 109 | // Your code goes here... 110 | }); 111 | 112 | watch.on('compile_errors', () => { 113 | // Your code goes here... 114 | }); 115 | 116 | watch.start('--project', '.'); 117 | 118 | try { 119 | // do something... 120 | } catch (e) { 121 | watch.kill(); // Fatal error, kill the compiler instance. 122 | } 123 | ``` 124 | 125 | Notes: 126 | 127 | - The (`onSuccess`) `COMMAND` will not run if the compilation failed. 128 | - The (`onEmit`) `COMMAND` will not run if the compilation succeeded with no changed files, unless it is the first success. 129 | - The (`onEmit`) `COMMAND` will run even if the compilation failed, but emitted changed files. 130 | - The (`onEmit`) `COMMAND` will not run 100 times for 100 files, due to `--onEmitDebounce` 131 | - The (`onEmit`) `COMMAND` is not cancelling the `onSuccess`/`onFirstSuccess`/`onFailure`/`onCompilationComplete`/`onCompilationStarted` commands and vice versa. 132 | - `tsc-watch` is using the currently installed TypeScript compiler. 133 | - `tsc-watch` is not changing the compiler, just adds the new arguments, compilation is the same, and all other arguments are the same. 134 | - if `tsc-watch` was installed globally, it will use the local compiler if it exists, otherwise it will use the global `tsc` compiler. 135 | -------------------------------------------------------------------------------- /client.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist/client" 2 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/client'); 2 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | console.log('Generating random number every sec'); 2 | setInterval(() => console.log(Math.random()), 1000); 3 | -------------------------------------------------------------------------------- /example/main.ts: -------------------------------------------------------------------------------- 1 | console.log('Generating random number every sec'); 2 | setInterval(() => console.log(Math.random()), 1000); 3 | 4 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": false, 6 | "sourceMap": false 7 | }, 8 | "files": [ 9 | "main.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./dist/lib/tsc-watch'); 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | testMatch: ["**/src/test/*.test.ts"], 6 | testTimeout: 30000, 7 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsc-watch", 3 | "version": "7.1.1", 4 | "description": "The TypeScript compiler with onSuccess command", 5 | "scripts": { 6 | "clean": "rimraf dist && rimraf tmp", 7 | "prepublishOnly": "npm run build && crlf --set=LF index.js client.js dist/**/*", 8 | "test": "npm run build && jest --verbose --runInBand", 9 | "build": "npm run clean && npm run build-lib && npm run build-client", 10 | "build-lib": "tsc -p tsconfig.json", 11 | "build-client": "tsc -p tsconfig-client.json", 12 | "watch-lib": "tsc -p tsconfig.json --watch" 13 | }, 14 | "bin": { 15 | "tsc-watch": "dist/lib/tsc-watch.js" 16 | }, 17 | "files": [ 18 | "dist/**/*", 19 | "index.js", 20 | "client.js", 21 | "client.d.ts" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git+ssh://git@github.com/gilamran/tsc-watch.git" 26 | }, 27 | "keywords": [ 28 | "TypeScript" 29 | ], 30 | "main": "./dist/client/index.js", 31 | "types": "./dist/client/index.d.ts", 32 | "engines": { 33 | "node": ">=12.12.0" 34 | }, 35 | "author": "Gil Amran", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/gilamran/tsc-watch/issues" 39 | }, 40 | "homepage": "https://github.com/gilamran/tsc-watch#readme", 41 | "prettier": { 42 | "trailingComma": "all", 43 | "tabWidth": 2, 44 | "semi": true, 45 | "singleQuote": true, 46 | "printWidth": 100 47 | }, 48 | "dependencies": { 49 | "cross-spawn": "^7.0.6", 50 | "node-cleanup": "^2.1.2", 51 | "ps-tree": "^1.2.0", 52 | "string-argv": "^0.3.2" 53 | }, 54 | "peerDependencies": { 55 | "typescript": "*" 56 | }, 57 | "devDependencies": { 58 | "@types/cross-spawn": "^6.0.6", 59 | "@types/jest": "^29.5.14", 60 | "@types/node": "^12.12.0", 61 | "@types/node-cleanup": "^2.1.5", 62 | "@types/ps-tree": "^1.1.6", 63 | "crlf": "^1.1.1", 64 | "fs-extra": "^11.3.0", 65 | "jest": "^29.7.0", 66 | "rimraf": "^6.0.1", 67 | "ts-jest": "^29.3.4", 68 | "typescript": "^5.8.3" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/client/client.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { ChildProcess, fork, ForkOptions } from 'child_process'; 3 | import { EventEmitter } from 'events'; 4 | 5 | const tscWatchLibPath = path.join(__dirname, '..', 'lib', 'tsc-watch'); 6 | 7 | export class TscWatchClient extends EventEmitter { 8 | private tsc: ChildProcess | undefined; 9 | 10 | constructor(private tscWatchPath = tscWatchLibPath) { 11 | super(); 12 | } 13 | 14 | start(...args: string[]) { 15 | const options: ForkOptions = { stdio: 'inherit' }; 16 | this.tsc = fork(this.tscWatchPath, args, options); 17 | this.tsc.on('message', (msg: string) => { 18 | this.emit(...deserializeTscMessage(msg)); 19 | }); 20 | this.tsc.on('exit', (code: number, signal: string) => { 21 | this.emit('exit', code, signal); 22 | }); 23 | } 24 | 25 | kill() { 26 | if (this.tsc && this.tsc.kill) { 27 | this.tsc.kill(); 28 | } 29 | this.removeAllListeners(); 30 | } 31 | 32 | runOnCompilationStartedCommand() { 33 | if (this.tsc) { 34 | this.tsc.send('run-on-compilation-started-command'); 35 | } 36 | } 37 | 38 | runOnCompilationCompleteCommand() { 39 | if (this.tsc) { 40 | this.tsc.send('run-on-compilation-complete-command'); 41 | } 42 | } 43 | 44 | runOnFirstSuccessCommand() { 45 | if (this.tsc) { 46 | this.tsc.send('run-on-first-success-command'); 47 | } 48 | } 49 | 50 | runOnFailureCommand() { 51 | if (this.tsc) { 52 | this.tsc.send('run-on-failure-command'); 53 | } 54 | } 55 | 56 | runOnSuccessCommand() { 57 | if (this.tsc) { 58 | this.tsc.send('run-on-success-command'); 59 | } 60 | } 61 | 62 | runOnEmitCommand() { 63 | if (this.tsc) { 64 | this.tsc.send('run-on-emit-command'); 65 | } 66 | } 67 | } 68 | 69 | function deserializeTscMessage(strMsg: string): [string, string?] { 70 | const indexOfSeparator = strMsg.indexOf(':'); 71 | if (indexOfSeparator === -1) { 72 | return [strMsg]; 73 | } 74 | 75 | return [strMsg.substring(0, indexOfSeparator), strMsg.substring(indexOfSeparator + 1)]; 76 | } 77 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; -------------------------------------------------------------------------------- /src/lib/@types/fs-extra.ts: -------------------------------------------------------------------------------- 1 | declare module 'fs-extra'; -------------------------------------------------------------------------------- /src/lib/@types/string-argv.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'string-argv'; -------------------------------------------------------------------------------- /src/lib/args-manager.ts: -------------------------------------------------------------------------------- 1 | import { getCompilerPath } from "./compiler-provider"; 2 | 3 | function removeRunnerArgs(args: string[]): string[] { 4 | return args.splice(2); // removing "node tsc-watch.js" 5 | } 6 | 7 | function getCommandIdx(args: string[], command: string): number { 8 | const lowerCasedCommand = command.toLowerCase(); 9 | return args.map((arg) => arg.toLowerCase()).indexOf(lowerCasedCommand); 10 | } 11 | 12 | export function isCommandExist(args: string[], command: string): boolean { 13 | return getCommandIdx(args, command) >= 0; 14 | } 15 | 16 | export function hasWatchCommand(args: string[]): boolean { 17 | return isCommandExist(args, '-w') || isCommandExist(args, '--watch'); 18 | } 19 | 20 | function forceWatch(args: string[]): string[] { 21 | if (!hasWatchCommand(args)) { 22 | args.push('--watch'); 23 | } 24 | 25 | return args; 26 | } 27 | 28 | function extractCommandWithValue(args: string[], command: string): string | null { 29 | let commandValue = null; 30 | let commandIdx = getCommandIdx(args, command); 31 | if (commandIdx > -1) { 32 | commandValue = args[commandIdx + 1]; 33 | args.splice(commandIdx, 2); 34 | } 35 | return commandValue; 36 | } 37 | 38 | function extractCommand(args: string[], command: string): boolean { 39 | let commandIdx = getCommandIdx(args, command); 40 | if (commandIdx > -1) { 41 | args.splice(commandIdx, 1); 42 | return true; 43 | } 44 | return false; 45 | } 46 | 47 | export function extractArgs(inputArgs: string[]) { 48 | const cleanArgs = removeRunnerArgs(inputArgs); 49 | const noWatch = extractCommand(cleanArgs, '--noWatch'); 50 | const args = noWatch ? cleanArgs : forceWatch(cleanArgs); 51 | 52 | const onFirstSuccessCommand = extractCommandWithValue(args, '--onFirstSuccess'); 53 | const onSuccessCommand = extractCommandWithValue(args, '--onSuccess'); 54 | const onFailureCommand = extractCommandWithValue(args, '--onFailure'); 55 | const onEmitCommand = extractCommandWithValue(args, '--onEmit'); 56 | const onEmitDebounceMs = Number(extractCommandWithValue(args, '--onEmitDebounceMs')) || 300; 57 | const onCompilationStarted = extractCommandWithValue(args, '--onCompilationStarted'); 58 | const onCompilationComplete = extractCommandWithValue(args, '--onCompilationComplete'); 59 | const maxNodeMem = extractCommandWithValue(args, '--maxNodeMem'); 60 | const noColors = extractCommand(args, '--noColors'); 61 | const noClear = extractCommand(args, '--noClear'); 62 | const silent = extractCommand(args, '--silent'); 63 | const signalEmittedFiles = extractCommand(args, '--signalEmittedFiles'); 64 | const requestedToListEmittedFiles = extractCommand(args, '--listEmittedFiles'); 65 | const compiler = getCompilerPath(extractCommandWithValue(args, '--compiler')); 66 | 67 | 68 | if (signalEmittedFiles || requestedToListEmittedFiles) { 69 | if (args[0] === '--build' || args[0] === '-b') { 70 | // TS6369: Option '--build' must be the first command line argument. 71 | args.splice(1, 0, '--listEmittedFiles'); 72 | } else { 73 | args.unshift('--listEmittedFiles'); 74 | } 75 | } 76 | 77 | return { 78 | onFirstSuccessCommand, 79 | onSuccessCommand, 80 | onFailureCommand, 81 | onEmitCommand, 82 | onEmitDebounceMs, 83 | onCompilationStarted, 84 | onCompilationComplete, 85 | maxNodeMem, 86 | noColors, 87 | noClear, 88 | requestedToListEmittedFiles, 89 | signalEmittedFiles, 90 | silent, 91 | compiler, 92 | args, 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /src/lib/compiler-provider.ts: -------------------------------------------------------------------------------- 1 | export function getCompilerPath(compilerArg: string | null, resolver: NodeRequire['resolve'] = require.resolve): string { 2 | if (!compilerArg) { 3 | compilerArg = 'typescript/bin/tsc'; 4 | } 5 | 6 | try { 7 | return resolver(compilerArg, { paths: [process.cwd()] }); 8 | } catch (e) { 9 | // Local compiler not found, ignore and try global compiler 10 | } 11 | 12 | try { 13 | return resolver(compilerArg); 14 | } catch (e: any) { 15 | if (e.code === 'MODULE_NOT_FOUND') { 16 | console.error(e.message); 17 | process.exit(9); 18 | } 19 | throw e; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/debounce.ts: -------------------------------------------------------------------------------- 1 | export function debounce) => void>(this: ThisParameterType, fn: T, delay = 300) { 2 | let timer: ReturnType | undefined 3 | return (...args: Parameters) => { 4 | timer && clearTimeout(timer) 5 | timer = setTimeout(() => fn.apply(this, args), delay) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/killer.ts: -------------------------------------------------------------------------------- 1 | import psTree from 'ps-tree'; 2 | import spawn from 'cross-spawn'; 3 | import { ChildProcess, exec } from 'child_process'; 4 | 5 | let KILL_SIGNAL = '15'; // SIGTERM 6 | let hasPS = true; 7 | 8 | const isWindows = process.platform === 'win32'; 9 | 10 | // discover if the OS has `ps`, and therefore can use psTree 11 | exec('ps', function (error) { 12 | if (error) { 13 | hasPS = false; 14 | } 15 | }); 16 | 17 | export function kill(child: ChildProcess): Promise { 18 | return new Promise((resolve) => { 19 | if (isWindows) { 20 | exec(`taskkill /pid ${child.pid} /T /F`, () => resolve()); 21 | } else { 22 | if (hasPS) { 23 | psTree(child.pid, (err, kids) => { 24 | const kidsPIDs = kids.map((p) => p.PID); 25 | const args = [`-${KILL_SIGNAL}`, child.pid.toString(), ...kidsPIDs]; 26 | spawn('kill', args).on('close', resolve); 27 | }); 28 | } else { 29 | exec(`kill -${KILL_SIGNAL} ${child.pid}`, () => resolve()); 30 | } 31 | } 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/runner.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess } from 'child_process'; 2 | import spawn from 'cross-spawn'; 3 | import stringArgv from 'string-argv'; 4 | import { kill } from './killer'; 5 | 6 | function runCommand(fullCommand: string): ChildProcess { 7 | const parts: string[] = stringArgv(fullCommand); 8 | const exec = parts[0]; 9 | const args = parts.splice(1); 10 | return spawn(exec, args, { 11 | stdio: 'inherit', 12 | }); 13 | } 14 | 15 | export function run(command: string): () => Promise { 16 | const process = runCommand(command); 17 | const exitPromise = new Promise((resolve) => process.on('exit', resolve)); 18 | 19 | return () => Promise.all([kill(process), exitPromise]); 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/stdout-manipulator.ts: -------------------------------------------------------------------------------- 1 | const ANSI_REGEX = new RegExp( 2 | '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))', 3 | 'g', 4 | ); 5 | const stripAnsi = (str: string) => str.replace(ANSI_REGEX, ''); 6 | 7 | const tscUsageSyntaxRegex = / -w, --watch.*Watch input files\./; 8 | const typescriptPrettyErrorRegex = /:\d+:\d+ \- error TS\d+: /; 9 | const typescriptErrorRegex = /\(\d+,\d+\): error TS\d+: /; 10 | const typescriptEmittedFileRegex = /(TSFILE:)\s*(.*)/; 11 | 12 | // errors 13 | const compilationCompleteWithErrorRegex = 14 | / Found [^0][0-9]* error[s]?\. Watching for file changes\./; 15 | const nativeCompilationCompleteWithErrorRegex = /Found [^0]?\d* error[s]? in /; 16 | 17 | // no errors 18 | const compilationCompleteWithoutErrorRegex = / Found 0 errors\. Watching for file changes\./; 19 | 20 | // compilation started 21 | const compilationStartedRegex = 22 | /( Starting compilation in watch mode\.\.\.| File change detected\. Starting incremental compilation\.\.\.)/; 23 | 24 | const nativeCompilationStartedRegex = /build starting at /; 25 | 26 | // compilation complete 27 | const compilationCompleteRegex = 28 | /( Compilation complete\. Watching for file changes\.| Found \d+ error[s]?\. Watching for file changes\.)/; 29 | 30 | const nativeCompilationCompleteRegex = /build finished in /; 31 | 32 | const newAdditionToSyntax = [ 33 | ' -w, --watch Watch input files. [on by default, use --noWatch to disable]', 34 | ' --onSuccess COMMAND Executes `COMMAND` on **every successful** compilation.', 35 | ' --onFirstSuccess COMMAND Executes `COMMAND` on the **first successful** compilation.', 36 | ' --onFailure COMMAND Executes `COMMAND` on **every failed** compilation.', 37 | ' --onEmit COMMAND Executes debounced `COMMAND` on **every emitted file**, ignoring unchanged files and disregards compilation success or failure.', 38 | ' --onEmitDebounceMs DELAY Delay by which to debounce `--onEmit` (default: 300).', 39 | ' --onCompilationStarted COMMAND Executes `COMMAND` on **every compilation start** event.', 40 | ' --onCompilationComplete COMMAND Executes `COMMAND` on **every successful or failed** compilation.', 41 | ' --noColors Removes the red/green colors from the compiler output', 42 | ' --noClear Prevents the compiler from clearing the screen', 43 | ' --compiler PATH The PATH will be used instead of typescript compiler. Defaults typescript/bin/tsc.', 44 | ].join('\n'); 45 | 46 | function color(line: string, noClear: boolean = false): string { 47 | // coloring errors: 48 | line = line.replace(typescriptErrorRegex, (m) => `\u001B[36m${m}\u001B[39m`); // Cyan 49 | line = line.replace(typescriptPrettyErrorRegex, (m) => `\u001B[36m${m}\u001B[39m`); // Cyan 50 | 51 | // completed with error: 52 | line = line.replace(compilationCompleteWithErrorRegex, (m) => `\u001B[31m${m}\u001B[39m`); // Red 53 | line = line.replace(nativeCompilationCompleteWithErrorRegex, (m) => `\u001B[31m${m}\u001B[39m`); // Red 54 | 55 | // completed without error: 56 | line = line.replace(compilationCompleteWithoutErrorRegex, (m) => `\u001B[32m${m}\u001B[39m`); // Green 57 | 58 | // usage 59 | line = line.replace(tscUsageSyntaxRegex, (m) => `\u001B[33m${m}\u001B[39m`); // Yellow 60 | 61 | if (noClear && compilationStartedRegex.test(line)) { 62 | return '\n\n----------------------\n' + line; 63 | } 64 | 65 | return line; 66 | } 67 | 68 | type TPrintParams = { 69 | noColors?: boolean; 70 | noClear?: boolean; 71 | requestedToListEmittedFiles?: boolean; 72 | signalEmittedFiles?: boolean; 73 | }; 74 | 75 | export function print( 76 | line: string, 77 | { 78 | noColors = false, 79 | noClear = false, 80 | requestedToListEmittedFiles = false, 81 | signalEmittedFiles = false, 82 | }: TPrintParams = {}, 83 | ): void { 84 | if (signalEmittedFiles && !requestedToListEmittedFiles && line.startsWith('TSFILE:')) { 85 | return; 86 | } 87 | 88 | console.log(noColors ? line : color(line, noClear)); 89 | } 90 | 91 | export function deleteClear(line: string): string { 92 | // '\x1bc11:40:16 - Starting compilation in watch mode...' 93 | // '\x1b[2J\x1b[3J\x1b[H11:33:28 - Starting compilation in watch mode...' 94 | const result = line 95 | .replace(/^\x1b\[2J/, '') 96 | .replace(/^\x1b\[3J/, '') 97 | .replace(/^\x1b\[H/, '') 98 | .replace(/^\x1bc/, ''); 99 | return result; 100 | } 101 | 102 | export function manipulate(line: string): string { 103 | return line.replace(tscUsageSyntaxRegex, newAdditionToSyntax); 104 | } 105 | 106 | export function detectState(line: string) { 107 | const clearLine = stripAnsi(line); 108 | const compilationStarted = 109 | compilationStartedRegex.test(clearLine) || nativeCompilationStartedRegex.test(clearLine); 110 | 111 | const compilationError = 112 | compilationCompleteWithErrorRegex.test(clearLine) || 113 | nativeCompilationCompleteWithErrorRegex.test(clearLine) || 114 | typescriptErrorRegex.test(clearLine) || 115 | typescriptPrettyErrorRegex.test(clearLine); 116 | 117 | const compilationComplete = 118 | compilationCompleteRegex.test(clearLine) || nativeCompilationCompleteRegex.test(clearLine); 119 | const fileEmittedExec = typescriptEmittedFileRegex.exec(clearLine); 120 | const fileEmitted = fileEmittedExec !== null ? fileEmittedExec[2] : null; // if the regex is not null it will return an array with 3 elements 121 | 122 | return { 123 | compilationStarted, 124 | compilationError, 125 | compilationComplete, 126 | fileEmitted, 127 | }; 128 | } 129 | -------------------------------------------------------------------------------- /src/lib/tsc-watch.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import nodeCleanup, { uninstall } from 'node-cleanup'; 4 | import spawn from 'cross-spawn'; 5 | import { run } from './runner'; 6 | import { extractArgs } from './args-manager'; 7 | import { debounce } from './debounce'; 8 | import { manipulate, detectState, deleteClear, print } from './stdout-manipulator'; 9 | import { createInterface } from 'readline'; 10 | import { ChildProcess } from 'child_process'; 11 | 12 | let firstTime = true; 13 | let firstSuccessKiller: (() => Promise) | null = null; 14 | let successKiller: (() => Promise) | null = null; 15 | let failureKiller: (() => Promise) | null = null; 16 | let emitKiller: (() => Promise) | null = null; 17 | let compilationStartedKiller: (() => Promise) | null = null; 18 | let compilationCompleteKiller: (() => Promise) | null = null; 19 | 20 | const { 21 | onFirstSuccessCommand, 22 | onSuccessCommand, 23 | onFailureCommand, 24 | onEmitCommand, 25 | onEmitDebounceMs, 26 | onCompilationStarted, 27 | onCompilationComplete, 28 | maxNodeMem, 29 | noColors, 30 | noClear, 31 | requestedToListEmittedFiles, 32 | signalEmittedFiles, 33 | silent, 34 | compiler, 35 | args, 36 | } = extractArgs(process.argv); 37 | 38 | let runningKillProcessesPromise: Promise | null = null; 39 | function killProcesses(currentCompilationId: number, killAll: boolean): Promise { 40 | if (runningKillProcessesPromise) { 41 | return runningKillProcessesPromise.then(() => currentCompilationId); 42 | } 43 | 44 | const promisesToWaitFor: Promise[] = []; 45 | if (killAll && firstSuccessKiller) { 46 | promisesToWaitFor.push(firstSuccessKiller()); 47 | firstSuccessKiller = null; 48 | } 49 | 50 | if (successKiller) { 51 | promisesToWaitFor.push(successKiller()); 52 | successKiller = null; 53 | } 54 | 55 | if (failureKiller) { 56 | promisesToWaitFor.push(failureKiller()); 57 | failureKiller = null; 58 | } 59 | 60 | if (compilationStartedKiller) { 61 | promisesToWaitFor.push(compilationStartedKiller()); 62 | compilationStartedKiller = null; 63 | } 64 | 65 | if (compilationCompleteKiller) { 66 | promisesToWaitFor.push(compilationCompleteKiller()); 67 | compilationCompleteKiller = null; 68 | } 69 | 70 | runningKillProcessesPromise = Promise.all(promisesToWaitFor).then(() => { 71 | runningKillProcessesPromise = null; 72 | return currentCompilationId; 73 | }); 74 | 75 | return runningKillProcessesPromise; 76 | } 77 | 78 | let runningKillEmitProcessesPromise: Promise | null = null; 79 | // The same as `killProcesses`, but we separate it to avoid canceling each other 80 | function killEmitProcesses(currentEmitId: number): Promise { 81 | if (runningKillEmitProcessesPromise) { 82 | return runningKillEmitProcessesPromise.then(() => currentEmitId); 83 | } 84 | 85 | let emitKilled = Promise.resolve(); 86 | if (emitKiller) { 87 | emitKilled = emitKiller(); 88 | emitKiller = null; 89 | } 90 | 91 | runningKillEmitProcessesPromise = emitKilled.then(() => { 92 | runningKillEmitProcessesPromise = null; 93 | return currentEmitId; 94 | }); 95 | 96 | return runningKillEmitProcessesPromise; 97 | } 98 | 99 | function runOnCompilationStarted(): void { 100 | if (onCompilationStarted) { 101 | compilationStartedKiller = run(onCompilationStarted); 102 | } 103 | } 104 | 105 | function runOnCompilationComplete(): void { 106 | if (onCompilationComplete) { 107 | compilationCompleteKiller = run(onCompilationComplete); 108 | } 109 | } 110 | 111 | function runOnFailureCommand(): void { 112 | if (onFailureCommand) { 113 | failureKiller = run(onFailureCommand); 114 | } 115 | } 116 | 117 | function runOnFirstSuccessCommand(): void { 118 | if (onFirstSuccessCommand) { 119 | firstSuccessKiller = run(onFirstSuccessCommand); 120 | } 121 | } 122 | 123 | function runOnSuccessCommand(): void { 124 | if (onSuccessCommand) { 125 | successKiller = run(onSuccessCommand); 126 | } 127 | } 128 | 129 | const debouncedEmit = onEmitCommand 130 | ? debounce(() => { emitKiller = run(onEmitCommand) }, onEmitDebounceMs) 131 | : undefined; 132 | 133 | function runOnEmitCommand(): void { 134 | debouncedEmit?.(); 135 | } 136 | 137 | interface INodeSettings { 138 | maxNodeMem: string | null; 139 | } 140 | 141 | function spawnTsc({ maxNodeMem }: INodeSettings, args: string[]): ChildProcess { 142 | const nodeArgs = [ 143 | ...((maxNodeMem) ? [`--max_old_space_size=${maxNodeMem}`] : []), 144 | compiler, 145 | ...args 146 | ]; 147 | 148 | return spawn('node', nodeArgs); 149 | } 150 | 151 | function echoExit(code: number | null, signal: string | null) { 152 | if (signal !== null) { 153 | process.kill(process.pid, signal); 154 | } 155 | } 156 | 157 | let compilationErrorSinceStart = false; 158 | const tscProcess = spawnTsc({ maxNodeMem }, args); 159 | if (!tscProcess.stdout) { 160 | throw new Error('Unable to read Typescript stdout'); 161 | } 162 | if (!tscProcess.stderr) { 163 | throw new Error('Unable to read Typescript stderr'); 164 | } 165 | 166 | tscProcess.on('exit', echoExit); 167 | tscProcess.stderr.pipe(process.stderr); 168 | 169 | const rl = createInterface({ input: tscProcess.stdout }); 170 | 171 | let compilationId = 0; 172 | let emitId = 0; 173 | 174 | function triggerOnEmit() { 175 | if (onEmitCommand) { 176 | killEmitProcesses(++emitId).then((previousEmitId) => previousEmitId === emitId && runOnEmitCommand()); 177 | } 178 | } 179 | 180 | rl.on('line', function (input) { 181 | if (noClear) { 182 | input = deleteClear(input); 183 | } 184 | 185 | const line = manipulate(input); 186 | if (!silent) { 187 | print(line, { noColors, noClear, signalEmittedFiles, requestedToListEmittedFiles }); 188 | } 189 | const state = detectState(line); 190 | const compilationStarted = state.compilationStarted; 191 | const compilationError = state.compilationError; 192 | const compilationComplete = state.compilationComplete; 193 | 194 | compilationErrorSinceStart = 195 | (!compilationStarted && compilationErrorSinceStart) || compilationError; 196 | 197 | if (state.fileEmitted !== null) { 198 | Signal.emitFile(state.fileEmitted); 199 | triggerOnEmit(); 200 | } 201 | 202 | if (compilationStarted) { 203 | compilationId++; 204 | killProcesses(compilationId, false).then((previousCompilationId) => { 205 | if (previousCompilationId !== compilationId) { 206 | return; 207 | } 208 | runOnCompilationStarted(); 209 | Signal.emitStarted(); 210 | }); 211 | } 212 | 213 | if (compilationComplete) { 214 | compilationId++; 215 | killProcesses(compilationId, false).then((previousCompilationId) => { 216 | if (previousCompilationId !== compilationId) { 217 | return; 218 | } 219 | runOnCompilationComplete(); 220 | 221 | if (compilationErrorSinceStart) { 222 | Signal.emitFail(); 223 | runOnFailureCommand(); 224 | } else { 225 | if (firstTime) { 226 | firstTime = false; 227 | Signal.emitFirstSuccess(); 228 | runOnFirstSuccessCommand(); 229 | triggerOnEmit(); 230 | } 231 | 232 | Signal.emitSuccess(); 233 | runOnSuccessCommand(); 234 | } 235 | }); 236 | } 237 | }); 238 | 239 | if (typeof process.on === 'function') { 240 | process.on('message', (msg: string) => { 241 | switch (msg) { 242 | case 'run-on-compilation-started-command': 243 | if (compilationStartedKiller) { 244 | compilationStartedKiller().then(runOnCompilationStarted); 245 | } 246 | break; 247 | 248 | case 'run-on-compilation-complete-command': 249 | if (compilationCompleteKiller) { 250 | compilationCompleteKiller().then(runOnCompilationComplete); 251 | } 252 | break; 253 | 254 | case 'run-on-first-success-command': 255 | if (firstSuccessKiller) { 256 | firstSuccessKiller().then(runOnFirstSuccessCommand); 257 | } 258 | break; 259 | 260 | case 'run-on-failure-command': 261 | if (failureKiller) { 262 | failureKiller().then(runOnFailureCommand); 263 | } 264 | break; 265 | 266 | case 'run-on-success-command': 267 | if (successKiller) { 268 | successKiller().then(runOnSuccessCommand); 269 | } 270 | break; 271 | 272 | case 'run-on-emit-command': 273 | if (emitKiller) { 274 | emitKiller().then(runOnEmitCommand); 275 | } 276 | break; 277 | 278 | default: 279 | console.log('Unknown message', msg); 280 | } 281 | }); 282 | } 283 | 284 | const sendSignal = (msg: string) => { 285 | if (process.send) { 286 | process.send(msg); 287 | } 288 | }; 289 | 290 | const Signal = { 291 | emitStarted: () => sendSignal('started'), 292 | emitFirstSuccess: () => sendSignal('first_success'), 293 | emitSuccess: () => sendSignal('success'), 294 | emitFail: () => sendSignal('compile_errors'), 295 | emitFile: (path: string) => sendSignal(`file_emitted:${path}`), 296 | }; 297 | 298 | nodeCleanup((_exitCode: number | null, signal: string | null) => { 299 | if (signal) { 300 | tscProcess.kill(signal); 301 | } 302 | killProcesses(0, true).then(() => process.exit()); 303 | // don't call cleanup handler again 304 | uninstall(); 305 | return false; 306 | }); 307 | -------------------------------------------------------------------------------- /src/test/args-manager.test.ts: -------------------------------------------------------------------------------- 1 | import { extractArgs } from '../lib/args-manager'; 2 | 3 | describe('Args Manager', () => { 4 | it('Should remove the runner args', () => { 5 | const { args } = extractArgs(['node', 'tsc-watch.js', '-d', '1.ts']); 6 | expect(args).toEqual(['-d', '1.ts', '--watch']); 7 | }); 8 | 9 | it('Should remove custom args', () => { 10 | const { args } = extractArgs([ 11 | 'node', 12 | 'tsc-watch.js', 13 | '--compiler', 14 | 'typescript/bin/tsc', 15 | '--nocolors', 16 | '--noclear', 17 | '--onsuccess', 18 | 'MY_SUCCESS', 19 | '--onfailure', 20 | 'MY_FAILURE', 21 | '--onfirstsuccess', 22 | 'MY_FIRST', 23 | '-d', 24 | '1.ts', 25 | ]); 26 | expect(args).toEqual(['-d', '1.ts', '--watch']); 27 | }); 28 | 29 | it('Should force watch', () => { 30 | const { args } = extractArgs(['node', 'tsc-watch.js', '1.ts']); 31 | expect(args.indexOf('--watch')).toBeGreaterThan(-1); 32 | }); 33 | 34 | it('Should not change the argv order options if watch was not specified (fixes --build option)', () => { 35 | const { args } = extractArgs([ 36 | 'node', 37 | 'tsc-watch.js', 38 | '--build', 39 | '1.tsconfig.conf', 40 | '2.tsconfig.conf', 41 | ]); 42 | expect(args.indexOf('--build')).toBe(0); 43 | expect(args.indexOf('--watch')).toBe(3); 44 | }); 45 | 46 | it('Should not insert args before --build (TS6369)', () => { 47 | const { args } = extractArgs([ 48 | 'node', 49 | 'tsc-watch.js', 50 | '--signalEmittedFiles', 51 | '--build', 52 | '1.tsconfig.conf', 53 | '2.tsconfig.conf', 54 | ]); 55 | expect(args.indexOf('--build')).toBe(0); 56 | expect(args.indexOf('--listEmittedFiles')).toBe(1); 57 | }) 58 | 59 | it('Should not re-add watch', () => { 60 | const isWatchParam = (elt: string) => elt === '-w' || elt === '--watch'; 61 | expect( 62 | extractArgs(['node', 'tsc-watch.js', '-w', '1.ts']).args.filter(isWatchParam).length 63 | ).toBe(1); 64 | expect( 65 | extractArgs(['node', 'tsc-watch.js', '--watch', '1.ts']).args.filter(isWatchParam).length 66 | ).toBe(1); 67 | expect( 68 | extractArgs(['node', 'tsc-watch.js', '1.ts']).args.filter(isWatchParam).length 69 | ).toBe(1); 70 | }); 71 | 72 | it('Should return the onFirstSuccessCommand', () => { 73 | expect(extractArgs(['node', 'tsc-watch.js', '1.ts']).onFirstSuccessCommand).toBe(null); 74 | expect( 75 | extractArgs(['node', 'tsc-watch.js', '--onFirstSuccess', 'COMMAND_TO_RUN', '1.ts']) 76 | .onFirstSuccessCommand, 77 | ).toBe('COMMAND_TO_RUN'); 78 | }); 79 | 80 | it('Should return the onSuccessCommand', () => { 81 | expect(extractArgs(['node', 'tsc-watch.js', '1.ts']).onSuccessCommand).toBe(null); 82 | expect( 83 | extractArgs(['node', 'tsc-watch.js', '--onSuccess', 'COMMAND_TO_RUN', '1.ts']) 84 | .onSuccessCommand, 85 | ).toBe('COMMAND_TO_RUN'); 86 | }); 87 | 88 | it('Should return the onFailureCommand', () => { 89 | expect(extractArgs(['node', 'tsc-watch.js', '1.ts']).onFailureCommand).toBe(null); 90 | expect( 91 | extractArgs(['node', 'tsc-watch.js', '--onFailure', 'COMMAND_TO_RUN', '1.ts']) 92 | .onFailureCommand, 93 | ).toBe('COMMAND_TO_RUN'); 94 | }); 95 | 96 | it('Should return the onCompilationStarted', () => { 97 | expect(extractArgs(['node', 'tsc-watch.js', '1.ts']).onCompilationStarted).toBe(null); 98 | expect( 99 | extractArgs(['node', 'tsc-watch.js', '--onCompilationStarted', 'COMMAND_TO_RUN', '1.ts']) 100 | .onCompilationStarted, 101 | ).toBe('COMMAND_TO_RUN'); 102 | }); 103 | 104 | it('Should return the onEmit', () => { 105 | expect(extractArgs(['node', 'tsc-watch.js', '1.ts']).onEmitCommand).toBe(null); 106 | expect( 107 | extractArgs(['node', 'tsc-watch.js', '--onEmit', 'COMMAND_TO_RUN', '1.ts']) 108 | .onEmitCommand, 109 | ).toBe('COMMAND_TO_RUN'); 110 | }); 111 | 112 | it('Should return the onEmitDebounceMs', () => { 113 | expect(extractArgs(['node', 'tsc-watch.js', '1.ts']).onEmitDebounceMs).toBe(300); 114 | expect( 115 | extractArgs(['node', 'tsc-watch.js', '--onEmitDebounceMs', '200', '1.ts']) 116 | .onEmitDebounceMs, 117 | ).toBe(200); 118 | }); 119 | 120 | it('Should return the onCompilationComplete', () => { 121 | expect(extractArgs(['node', 'tsc-watch.js', '1.ts']).onCompilationComplete).toBe(null); 122 | expect( 123 | extractArgs(['node', 'tsc-watch.js', '--onCompilationComplete', 'COMMAND_TO_RUN', '1.ts']) 124 | .onCompilationComplete, 125 | ).toBe('COMMAND_TO_RUN'); 126 | }); 127 | 128 | it('Should return the maxNodeMem', () => { 129 | expect(extractArgs(['node', 'tsc-watch.js', '1.ts']).maxNodeMem).toBe(null); 130 | expect(extractArgs(['node', 'tsc-watch.js', '--maxNodeMem', '1024']).maxNodeMem).toBe('1024'); 131 | }); 132 | 133 | it('Should return the noColors', () => { 134 | expect(extractArgs(['node', 'tsc-watch.js', '1.ts']).noColors).toBe(false); 135 | expect(extractArgs(['node', 'tsc-watch.js', '--noColors', '1.ts']).noColors).toBe(true); 136 | }); 137 | 138 | it('Should return the noClear', () => { 139 | expect(extractArgs(['node', 'tsc-watch.js', '1.ts']).noClear).toBe(false); 140 | expect(extractArgs(['node', 'tsc-watch.js', '--noClear', '1.ts']).noClear).toBe(true); 141 | }); 142 | 143 | it('Should return the silent', () => { 144 | expect(extractArgs(['node', 'tsc-watch.js', '1.ts']).silent).toBe(false); 145 | expect(extractArgs(['node', 'tsc-watch.js', '--silent', '1.ts']).silent).toBe(true); 146 | }); 147 | 148 | it('Should return the signalEmittedFiles', () => { 149 | expect(extractArgs(['node', 'tsc-watch.js', '1.ts']).signalEmittedFiles).toBe(false); 150 | expect(extractArgs(['node', 'tsc-watch.js', '--signalEmittedFiles', '1.ts']).signalEmittedFiles).toBe(true); 151 | expect(extractArgs(['node', 'tsc-watch.js', '--signalEmittedFiles', '1.ts']).args.includes('--listEmittedFiles')).toBe(true); 152 | }); 153 | 154 | it('Should return requestedToListEmittedFiles when tsc native listEmittedFiles is set', () => { 155 | expect(extractArgs(['node', 'tsc-watch.js', '1.ts']).requestedToListEmittedFiles).toBe(false); 156 | expect(extractArgs(['node', 'tsc-watch.js', '--listEmittedFiles', '1.ts']).requestedToListEmittedFiles).toBe(true); 157 | expect(extractArgs(['node', 'tsc-watch.js', '--listEmittedFiles', '1.ts']).args.includes('--listEmittedFiles')).toBe(true); 158 | }); 159 | 160 | }); 161 | -------------------------------------------------------------------------------- /src/test/client.test.ts: -------------------------------------------------------------------------------- 1 | import { arch, platform } from 'os'; 2 | import { join } from 'path'; 3 | import { TscWatchClient } from '../client'; 4 | import { 5 | copyFixtures, 6 | FAILING_FILE, 7 | OUTPUT_FILE, 8 | PASSING_FILE, 9 | removeFixtures, 10 | waitFor, 11 | } from './test-utils'; 12 | import type { ChildProcess } from 'child_process'; 13 | 14 | const child_process = require('child_process'); 15 | 16 | const compareFileLocation = (a: string, b: string) => { 17 | const aParts = a.split(/[\\\/]/); 18 | const bParts = b.split(/[\\\/]/); 19 | const aLast = aParts[aParts.length - 1]; 20 | const bLast = bParts[bParts.length - 1]; 21 | return aLast.toLowerCase() === bLast.toLowerCase(); 22 | }; 23 | 24 | describe('Client Events', () => { 25 | let watchClient: TscWatchClient; 26 | let callback: jest.Mock; 27 | 28 | beforeEach(() => { 29 | const tscWatchPath = require.resolve(join('..', '..', 'dist', 'lib', 'tsc-watch')); 30 | watchClient = new TscWatchClient(tscWatchPath); 31 | callback = jest.fn(); 32 | copyFixtures(); 33 | }); 34 | 35 | afterEach(() => { 36 | watchClient.kill(); 37 | removeFixtures(); 38 | }); 39 | 40 | describe('Events', () => { 41 | it('Should emit "started" on compilation start', () => { 42 | watchClient.on('started', callback); 43 | watchClient.start('--noClear', '--outFile', OUTPUT_FILE, FAILING_FILE); 44 | return waitFor(() => callback.mock.calls.length > 0); 45 | }); 46 | 47 | it('Should emit "first_success" on first success', async () => { 48 | watchClient.on('first_success', callback); 49 | watchClient.start('--noClear', '--outFile', OUTPUT_FILE, PASSING_FILE); 50 | return waitFor(() => callback.mock.calls.length > 0); 51 | }); 52 | 53 | it('Should emit "success" on first success', async () => { 54 | watchClient.on('success', callback); 55 | watchClient.start('--noClear', '--outFile', OUTPUT_FILE, PASSING_FILE); 56 | return waitFor(() => callback.mock.calls.length > 0); 57 | }); 58 | 59 | it('Should deserialize and emit a "file_emitted" with the emitted file path', async function () { 60 | watchClient.on('file_emitted', callback); 61 | watchClient.start('--noClear', '--listEmittedFiles', '--outFile', OUTPUT_FILE, PASSING_FILE); 62 | return waitFor(() => { 63 | if (callback.mock.calls.length > 0) { 64 | const firstCall = callback.mock.calls[0]; 65 | const callFirstArg = firstCall[0]; 66 | return compareFileLocation(callFirstArg, OUTPUT_FILE); 67 | } 68 | }); 69 | }); 70 | 71 | it('Should fire "compile_errors" on when tsc compile errors occur', async () => { 72 | watchClient.on('compile_errors', callback); 73 | watchClient.start('--noClear', '--outFile', OUTPUT_FILE, FAILING_FILE); 74 | return waitFor(() => callback.mock.calls.length > 0); 75 | }); 76 | 77 | it('Should fire back "exit" event when the process is exited by a signal', async function () { 78 | const forkSpy = jest.spyOn(child_process, 'fork'); 79 | watchClient.on('exit', callback); 80 | watchClient.start('--noClear', '--outFile', OUTPUT_FILE, PASSING_FILE); 81 | const tscProcess: ChildProcess = forkSpy.mock.results[0].value; 82 | 83 | // Wait tsc-watch to be started and bound before to kill process 84 | await new Promise((resolve) => setTimeout(resolve, 1000)); 85 | process.kill(tscProcess.pid, 9); 86 | 87 | await new Promise((resolve) => setTimeout(resolve, 1000)); 88 | await waitFor(() => callback.mock.calls.length > 0); 89 | const expectedResult = 90 | platform() === 'darwin' && arch() === 'arm64' ? [null, 'SIGKILL'] : [1, null]; 91 | expect(callback.mock.calls[0]).toEqual(expectedResult); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/test/compiler-provider.test.ts: -------------------------------------------------------------------------------- 1 | import { getCompilerPath } from '../lib/compiler-provider'; 2 | 3 | describe('Compiler Provider', () => { 4 | 5 | it('Should return the custom compiler', () => { 6 | const resolver: any = jest.fn().mockImplementation((id: string) => 'TYPESCRIPT_COMPILER_PATH'); 7 | 8 | const compilerPath = getCompilerPath('path/to/custom-compiler', resolver); 9 | expect(compilerPath).toBe('TYPESCRIPT_COMPILER_PATH'); 10 | expect(resolver).toHaveBeenCalledWith('path/to/custom-compiler', { paths: [process.cwd()] }); 11 | }); 12 | 13 | it('Should return the local compiler', () => { 14 | const resolver: any = jest.fn().mockImplementation((id: string) => 'LOCAL_TYPESCRIPT_COMPILER_PATH'); 15 | 16 | 17 | const compilerPath = getCompilerPath(null, resolver); 18 | expect(compilerPath).toBe('LOCAL_TYPESCRIPT_COMPILER_PATH'); 19 | expect(resolver).toHaveBeenCalledWith('typescript/bin/tsc', { paths: [process.cwd()] }); 20 | }); 21 | 22 | it('Should return the global compiler, if local compiler is not found', () => { 23 | let callIndex = 0; 24 | const resolver: any = jest.fn().mockImplementation((id: string) => { 25 | if (callIndex === 0) { 26 | callIndex++; 27 | throw new Error('MODULE_NOT_FOUND'); 28 | } 29 | 30 | return 'GLOBAL_TYPESCRIPT_COMPILER_PATH'; 31 | }); 32 | 33 | 34 | const compilerPath = getCompilerPath(null, resolver); 35 | expect(compilerPath).toBe('GLOBAL_TYPESCRIPT_COMPILER_PATH'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/test/driver.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess, fork } from 'child_process'; 2 | import { appendFileSync } from 'fs'; 3 | import { join } from 'path'; 4 | import { copyFixtures, FAILING_FILE, OUTPUT_FILE, PASSING_FILE, removeFixtures } from './test-utils'; 5 | 6 | const noop = () => {}; 7 | 8 | export class Driver { 9 | private subscriptions: Map; 10 | private process: ChildProcess | null; 11 | 12 | constructor() { 13 | copyFixtures(); 14 | this.subscriptions = new Map(); 15 | this.process = null; 16 | } 17 | 18 | public subscribe(processEventName: string, listener: Function) { 19 | this.subscriptions.set(processEventName, listener); 20 | return this; 21 | } 22 | 23 | public startWatch({ failFirst, pretty }: { failFirst?: boolean; pretty?: boolean } = {}): this { 24 | const params = [ 25 | '--noClear', 26 | '--outFile', 27 | OUTPUT_FILE, 28 | failFirst ? FAILING_FILE : PASSING_FILE, 29 | ]; 30 | if (pretty) { 31 | params.push('--pretty'); 32 | } 33 | this.process = fork(join(process.cwd(), 'dist', 'lib', 'tsc-watch.js'), params, { 34 | stdio: 'inherit', 35 | }); 36 | 37 | this.subscriptions.forEach((handler, evName) => 38 | this.process!.on('message', (event) => (evName === event ? handler(event) : noop())), 39 | ); 40 | 41 | return this; 42 | } 43 | 44 | public modifyToAValidChange(): this { 45 | appendFileSync(PASSING_FILE, '\n '); 46 | return this; 47 | } 48 | 49 | public modifyToAnInvalidChange(): this { 50 | appendFileSync(FAILING_FILE, '{{{'); 51 | return this; 52 | } 53 | 54 | public dispose(): this { 55 | if (this.process && this.process.kill) { 56 | this.process.kill(); 57 | this.process = null; 58 | } 59 | 60 | this.subscriptions.clear(); 61 | removeFixtures(); 62 | return this; 63 | } 64 | 65 | private wait(ms: number): Promise { 66 | return new Promise((resolve) => setTimeout(resolve, ms)); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/fixtures/failing.ts: -------------------------------------------------------------------------------- 1 | class T { 2 | -------------------------------------------------------------------------------- /src/test/fixtures/passing.ts: -------------------------------------------------------------------------------- 1 | class T {} 2 | -------------------------------------------------------------------------------- /src/test/stdout-manipulator.test.ts: -------------------------------------------------------------------------------- 1 | import { detectState, print } from '../lib/stdout-manipulator'; 2 | 3 | describe('stdout-manipulator', () => { 4 | describe('detectState', () => { 5 | it('Should not detect anything for an empty line', async () => { 6 | const state = detectState(''); 7 | expect(state).toEqual({ 8 | compilationStarted: false, 9 | compilationError: false, 10 | compilationComplete: false, 11 | fileEmitted: null, 12 | }); 13 | }); 14 | 15 | it('Should not detect anything for an unknown line', async () => { 16 | const state = detectState('tsc: unknown statement'); 17 | expect(state).toEqual({ 18 | compilationStarted: false, 19 | compilationError: false, 20 | compilationComplete: false, 21 | fileEmitted: null, 22 | }); 23 | }); 24 | 25 | describe('Tested with typescript 4.5.5', () => { 26 | it('Should detect a compilation start', async () => { 27 | const { compilationStarted } = detectState('[\x1B[90m11:32:11 AM\x1B[0m] Starting compilation in watch mode...'); 28 | expect(compilationStarted).toEqual(true); 29 | }); 30 | 31 | it('Should detect a compilation error', async () => { 32 | const { compilationError } = detectState('\x1B[96mmodules/file.ts\x1B[0m:\x1B[93m17\x1B[0m:\x1B[93m49\x1B[0m - \x1B[91merror\x1B[0m\x1B[90m TS2345: \x1B[0mArgument of type \'string\' is not assignable to parameter of type \'never\'.'); 33 | expect(compilationError).toEqual(true); 34 | }); 35 | 36 | it('Should detect a compilation complete', async () => { 37 | const { compilationComplete } = detectState('[\x1B[90m11:32:26 AM\x1B[0m] Found 4 errors. Watching for file changes.'); 38 | expect(compilationComplete).toEqual(true); 39 | }); 40 | 41 | it('Should detect an emitted file', async () => { 42 | const { fileEmitted } = detectState('TSFILE: /my/dist/hello.js'); 43 | expect(fileEmitted).toEqual('/my/dist/hello.js'); 44 | }); 45 | }) 46 | }); 47 | 48 | describe('print', () => { 49 | let forkSpy: any; 50 | beforeEach(() => { 51 | forkSpy = jest.spyOn(global.console, 'log').mockImplementation(); 52 | }); 53 | 54 | afterEach(() => { 55 | jest.clearAllMocks(); 56 | }); 57 | 58 | it('Should log raw line with default params', async () => { 59 | print('raw tsc line'); 60 | expect(forkSpy.mock.calls).toEqual([['raw tsc line']]) 61 | }); 62 | 63 | it('Should not hide a normal line when signalEmittedFiles is true', async () => { 64 | print('any other line', { signalEmittedFiles: true }); 65 | expect(forkSpy.mock.calls).toEqual([['any other line']]) 66 | }); 67 | 68 | describe("TSFILE support", () => { 69 | it('Should hide a TSFILE line when signalEmittedFiles is true', async () => { 70 | print('TSFILE: /home/emitted/file.js', { signalEmittedFiles: true }); 71 | expect(forkSpy.mock.calls).toEqual([]) 72 | }); 73 | 74 | it('Should not hide a TSFILE line when signalEmittedFiles is true and native --listEmittedFiles (requestedToListEmittedFiles) is true', async () => { 75 | print('TSFILE: /home/emitted/file.js', { noColors: true, signalEmittedFiles: true, requestedToListEmittedFiles: true }); 76 | expect(forkSpy.mock.calls).toEqual([['TSFILE: /home/emitted/file.js']]) 77 | }); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/test/test-utils.ts: -------------------------------------------------------------------------------- 1 | import fsExtra from 'fs-extra'; 2 | import { join } from 'path'; 3 | 4 | export const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 5 | 6 | export const waitFor = (action: Function, timeout = 30000) => { 7 | const startTime = Date.now(); 8 | return new Promise((resolve, reject) => { 9 | const retry = () => { 10 | if (Date.now() - timeout > startTime) { 11 | reject(); 12 | } else { 13 | if (action()) { 14 | resolve(); 15 | } else { 16 | wait(50).then(retry); 17 | } 18 | } 19 | }; 20 | 21 | retry(); 22 | }); 23 | }; 24 | 25 | export const FIXTURES_DIR = join(process.cwd(), 'src', 'test', 'fixtures'); 26 | export const TMP_DIR = join(process.cwd(), 'tmp'); 27 | export const TMP_FIXTURES_DIR = join(TMP_DIR, 'fixtures'); 28 | export const OUTPUT_FILE = join(TMP_FIXTURES_DIR, 'output.js'); 29 | export const FAILING_FILE = join(TMP_FIXTURES_DIR, 'failing.ts'); 30 | export const PASSING_FILE = join(TMP_FIXTURES_DIR, 'passing.ts'); 31 | 32 | export function copyFixtures() { 33 | fsExtra.copySync(FIXTURES_DIR, TMP_FIXTURES_DIR); 34 | } 35 | 36 | export function removeFixtures() { 37 | fsExtra.removeSync(TMP_DIR); 38 | } 39 | -------------------------------------------------------------------------------- /src/test/tsc-watch.test.ts: -------------------------------------------------------------------------------- 1 | import { Driver } from './driver'; 2 | import { waitFor, wait } from './test-utils'; 3 | 4 | describe('TSC-Watch child process messages', () => { 5 | let driver: Driver; 6 | 7 | beforeEach(() => (driver = new Driver())); 8 | afterEach(() => driver.dispose()); 9 | 10 | it('Should send "started" on compilation start', async () => { 11 | const listener = jest.fn(); 12 | driver.subscribe('started', listener).startWatch(); 13 | await wait(5000); 14 | driver.modifyToAValidChange(); 15 | return waitFor(() => listener.mock.calls.length === 2); 16 | }); 17 | 18 | it('Should send "first_success" on first success', async () => { 19 | const listener = jest.fn(); 20 | driver.subscribe('first_success', listener).startWatch(); 21 | await wait(5000); 22 | driver.modifyToAValidChange(); 23 | return waitFor(() => listener.mock.calls.length === 1); 24 | }); 25 | 26 | it('Should send "success" on subsequent successes', async () => { 27 | const listener = jest.fn(); 28 | driver.subscribe('success', listener).startWatch(); 29 | await wait(5000); 30 | driver.modifyToAValidChange(); 31 | return waitFor(() => listener.mock.calls.length === 2); 32 | }); 33 | 34 | it('Should send "compile_errors" when tsc compile errors occur', async () => { 35 | const listener = jest.fn(); 36 | driver.subscribe('compile_errors', listener).startWatch({ failFirst: true }); 37 | await wait(5000); 38 | driver.modifyToAnInvalidChange(); 39 | 40 | return waitFor(() => listener.mock.calls.length === 2); 41 | }); 42 | 43 | it('Should send "compile_errors" when pretty param was set', async () => { 44 | const listener = jest.fn(); 45 | driver.subscribe('compile_errors', listener).startWatch({ failFirst: true, pretty: true }); 46 | return waitFor(() => listener.mock.calls.length === 1); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tsc-watch-client-example.js: -------------------------------------------------------------------------------- 1 | const readline = require('readline'); 2 | const { TscWatchClient } = require('./client'); 3 | 4 | const client = new TscWatchClient(); 5 | 6 | client.on('started', () => { 7 | console.log('Compilation started'); 8 | }); 9 | 10 | client.on('first_success', () => { 11 | console.log('Interactive mode'); 12 | console.log(' Press "r" to re-run the onSuccess command, esc to exit.\n'); 13 | }); 14 | 15 | client.on('success', () => { 16 | console.log('Yey success!'); 17 | }); 18 | 19 | client.on('compile_errors', () => { 20 | console.log('Ho no!'); 21 | }); 22 | 23 | client.start( 24 | '--onSuccess', 25 | 'node ./example/main.js', 26 | '--noClear', 27 | '--project', 28 | './example/tsconfig.json', 29 | ); 30 | 31 | readline.emitKeypressEvents(process.stdin); 32 | process.stdin.on('keypress', (str, key) => { 33 | if (key.name == 'escape' || (key && key.ctrl && key.name == 'c')) { 34 | client.kill(); 35 | process.stdin.pause(); 36 | } else { 37 | if (str && str.toLowerCase() === 'r') { 38 | client.runOnSuccessCommand(); 39 | } 40 | } 41 | }); 42 | 43 | process.stdin.setRawMode(true); 44 | process.stdin.resume(); 45 | -------------------------------------------------------------------------------- /tsconfig-client.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "outDir": "./dist/client", 8 | "rootDir": "./src/client", 9 | "isolatedModules": true, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "skipLibCheck": true 15 | }, 16 | "include": ["src/client/index.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "outDir": "./dist/lib", 7 | "rootDir": "./src/lib", 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "skipLibCheck": true 13 | }, 14 | "typeRoots": ["./node_modules/@types", "./@types"], 15 | "include": ["src/lib/**/*.ts"] 16 | } 17 | --------------------------------------------------------------------------------