diff --git a/README.md b/README.md index 4af5723..b37ed1a 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `--onSuccess COMMAND` | Executes `COMMAND` on **every successful** compilation. | | `--onFirstSuccess COMMAND` | Executes `COMMAND` on the **first successful** compilation. | +| `--onEmit COMMAND` | Executes debounced `COMMAND` on **every emitted file**, ignoring unchanged files and disregards compilation success or failure. | +| `--onEmitDebounceMs DELAY` | Delay by which to debounce `--onEmit` (default: 300). | | `--onFailure COMMAND` | Executes `COMMAND` on **every failed** compilation. | | `--onCompilationStarted COMMAND` | Executes `COMMAND` on **every compilation start** event (initial and incremental). | | `--onCompilationComplete COMMAND` | Executes `COMMAND` on **every successful or failed** compilation. | @@ -118,5 +120,9 @@ try { Notes: - The (`onSuccess`) `COMMAND` will not run if the compilation failed. +- The (`onEmit`) `COMMAND` will not run if the compilation succeeded with no changed files, unless it is the first success. +- The (`onEmit`) `COMMAND` will run even if the compilation failed, but emitted changed files. +- The (`onEmit`) `COMMAND` will not run 100 times for 100 files, due to `--onEmitDebounce` +- The (`onEmit`) `COMMAND` is not cancelling the `onSuccess`/`onFirstSuccess`/`onFailure`/`onCompilationComplete`/`onCompilationStarted` commands and vice versa. - `tsc-watch` is using the currently installed TypeScript compiler. - `tsc-watch` is not changing the compiler, just adds the new arguments, compilation is the same, and all other arguments are the same. diff --git a/src/client/client.ts b/src/client/client.ts index 29351b1..d7a24e3 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -58,6 +58,12 @@ export class TscWatchClient extends EventEmitter { this.tsc.send('run-on-success-command'); } } + + runOnEmitCommand() { + if (this.tsc) { + this.tsc.send('run-on-emit-command'); + } + } } function deserializeTscMessage(strMsg: string): [string, string?] { diff --git a/src/lib/args-manager.ts b/src/lib/args-manager.ts index a89b6d1..2fad849 100644 --- a/src/lib/args-manager.ts +++ b/src/lib/args-manager.ts @@ -48,6 +48,8 @@ export function extractArgs(inputArgs: string[]) { const onFirstSuccessCommand = extractCommandWithValue(args, '--onFirstSuccess'); const onSuccessCommand = extractCommandWithValue(args, '--onSuccess'); const onFailureCommand = extractCommandWithValue(args, '--onFailure'); + const onEmitCommand = extractCommandWithValue(args, '--onEmit'); + const onEmitDebounceMs = Number(extractCommandWithValue(args, '--onEmitDebounceMs')) || 300; const onCompilationStarted = extractCommandWithValue(args, '--onCompilationStarted'); const onCompilationComplete = extractCommandWithValue(args, '--onCompilationComplete'); const maxNodeMem = extractCommandWithValue(args, '--maxNodeMem'); @@ -67,6 +69,8 @@ export function extractArgs(inputArgs: string[]) { onFirstSuccessCommand, onSuccessCommand, onFailureCommand, + onEmitCommand, + onEmitDebounceMs, onCompilationStarted, onCompilationComplete, maxNodeMem, diff --git a/src/lib/debounce.ts b/src/lib/debounce.ts new file mode 100644 index 0000000..36b03d7 --- /dev/null +++ b/src/lib/debounce.ts @@ -0,0 +1,7 @@ +export function debounce) => void>(this: ThisParameterType, fn: T, delay = 300) { + let timer: ReturnType | undefined + return (...args: Parameters) => { + timer && clearTimeout(timer) + timer = setTimeout(() => fn.apply(this, args), delay) + } +} diff --git a/src/lib/stdout-manipulator.ts b/src/lib/stdout-manipulator.ts index 71816b2..848ab03 100644 --- a/src/lib/stdout-manipulator.ts +++ b/src/lib/stdout-manipulator.ts @@ -22,6 +22,8 @@ const newAdditionToSyntax = [ ' --onSuccess COMMAND Executes `COMMAND` on **every successful** compilation.', ' --onFirstSuccess COMMAND Executes `COMMAND` on the **first successful** compilation.', ' --onFailure COMMAND Executes `COMMAND` on **every failed** compilation.', + ' --onEmit COMMAND Executes debounced `COMMAND` on **every emitted file**, ignoring unchanged files and disregards compilation success or failure.', + ' --onEmitDebounceMs DELAY Delay by which to debounce `--onEmit` (default: 300).', ' --onCompilationStarted COMMAND Executes `COMMAND` on **every compilation start** event.', ' --onCompilationComplete COMMAND Executes `COMMAND` on **every successful or failed** compilation.', ' --noColors Removes the red/green colors from the compiler output', diff --git a/src/lib/tsc-watch.ts b/src/lib/tsc-watch.ts index fc6eef2..9c62477 100644 --- a/src/lib/tsc-watch.ts +++ b/src/lib/tsc-watch.ts @@ -4,6 +4,7 @@ import nodeCleanup, { uninstall } from 'node-cleanup'; import spawn from 'cross-spawn'; import { run } from './runner'; import { extractArgs } from './args-manager'; +import { debounce } from './debounce'; import { manipulate, detectState, deleteClear, print } from './stdout-manipulator'; import { createInterface } from 'readline'; import { ChildProcess } from 'child_process'; @@ -12,6 +13,7 @@ let firstTime = true; let firstSuccessKiller: (() => Promise) | null = null; let successKiller: (() => Promise) | null = null; let failureKiller: (() => Promise) | null = null; +let emitKiller: (() => Promise) | null = null; let compilationStartedKiller: (() => Promise) | null = null; let compilationCompleteKiller: (() => Promise) | null = null; @@ -19,6 +21,8 @@ const { onFirstSuccessCommand, onSuccessCommand, onFailureCommand, + onEmitCommand, + onEmitDebounceMs, onCompilationStarted, onCompilationComplete, maxNodeMem, @@ -71,6 +75,27 @@ function killProcesses(currentCompilationId: number, killAll: boolean): Promise< return runningKillProcessesPromise; } +let runningKillEmitProcessesPromise: Promise | null = null; +// The same as `killProcesses`, but we separate it to avoid canceling each other +function killEmitProcesses(currentEmitId: number): Promise { + if (runningKillEmitProcessesPromise) { + return runningKillEmitProcessesPromise.then(() => currentEmitId); + } + + let emitKilled = Promise.resolve(); + if (emitKiller) { + emitKilled = emitKiller(); + emitKiller = null; + } + + runningKillEmitProcessesPromise = emitKilled.then(() => { + runningKillEmitProcessesPromise = null; + return currentEmitId; + }); + + return runningKillEmitProcessesPromise; +} + function runOnCompilationStarted(): void { if (onCompilationStarted) { compilationStartedKiller = run(onCompilationStarted); @@ -101,6 +126,14 @@ function runOnSuccessCommand(): void { } } +const debouncedEmit = onEmitCommand + ? debounce(() => { emitKiller = run(onEmitCommand) }, onEmitDebounceMs) + : undefined; + +function runOnEmitCommand(): void { + debouncedEmit?.(); +} + function getTscPath(): string { let tscBin: string; try { @@ -153,6 +186,14 @@ tscProcess.stderr.pipe(process.stderr); const rl = createInterface({ input: tscProcess.stdout }); let compilationId = 0; +let emitId = 0; + +function triggerOnEmit() { + if (onEmitCommand) { + killEmitProcesses(++emitId).then((previousEmitId) => previousEmitId === emitId && runOnEmitCommand()); + } +} + rl.on('line', function (input) { if (noClear) { input = deleteClear(input); @@ -172,6 +213,7 @@ rl.on('line', function (input) { if (state.fileEmitted !== null) { Signal.emitFile(state.fileEmitted); + triggerOnEmit(); } if (compilationStarted) { @@ -201,6 +243,7 @@ rl.on('line', function (input) { firstTime = false; Signal.emitFirstSuccess(); runOnFirstSuccessCommand(); + triggerOnEmit(); } Signal.emitSuccess(); @@ -243,6 +286,12 @@ if (typeof process.on === 'function') { } break; + case 'run-on-emit-command': + if (emitKiller) { + emitKiller().then(runOnEmitCommand); + } + break; + default: console.log('Unknown message', msg); } diff --git a/src/test/args-manager.test.ts b/src/test/args-manager.test.ts index 80b6be9..01abaed 100644 --- a/src/test/args-manager.test.ts +++ b/src/test/args-manager.test.ts @@ -87,6 +87,22 @@ describe('Args Manager', () => { ).toBe('COMMAND_TO_RUN'); }); + it('Should return the onEmit', () => { + expect(extractArgs(['node', 'tsc-watch.js', '1.ts']).onEmitCommand).toBe(null); + expect( + extractArgs(['node', 'tsc-watch.js', '--onEmit', 'COMMAND_TO_RUN', '1.ts']) + .onEmitCommand, + ).toBe('COMMAND_TO_RUN'); + }); + + it('Should return the onEmitDebounceMs', () => { + expect(extractArgs(['node', 'tsc-watch.js', '1.ts']).onEmitDebounceMs).toBe(300); + expect( + extractArgs(['node', 'tsc-watch.js', '--onEmitDebounceMs', '200', '1.ts']) + .onEmitDebounceMs, + ).toBe(200); + }); + it('Should return the onCompilationComplete', () => { expect(extractArgs(['node', 'tsc-watch.js', '1.ts']).onCompilationComplete).toBe(null); expect(