Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: --onEmit #167

Merged
merged 11 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 inputs and regardless of typechecking success or failure. |
llllvvuu marked this conversation as resolved.
Show resolved Hide resolved
| `--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. |
Expand Down Expand Up @@ -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 if the compilation failed, but still emitted changed files.
llllvvuu marked this conversation as resolved.
Show resolved Hide resolved
- The (`onEmit`) `COMMAND` will not run 100 times for 100 files, due to `--onEmitDebounce`
- The (`onEmit`) `COMMAND` will not cancel the `onSuccess`/`onFirstSuccess`/`onFailure`/`onCompilationComplete`/`onCompilationStarted` commands and vice versa.
llllvvuu marked this conversation as resolved.
Show resolved Hide resolved
- `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.
6 changes: 6 additions & 0 deletions src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?] {
Expand Down
4 changes: 4 additions & 0 deletions src/lib/args-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -67,6 +69,8 @@ export function extractArgs(inputArgs: string[]) {
onFirstSuccessCommand,
onSuccessCommand,
onFailureCommand,
onEmitCommand,
onEmitDebounceMs,
onCompilationStarted,
onCompilationComplete,
maxNodeMem,
Expand Down
7 changes: 7 additions & 0 deletions src/lib/debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function debounce<T extends (...args: Parameters<T>) => void>(this: ThisParameterType<T>, fn: T, delay = 300) {
let timer: ReturnType<typeof setTimeout> | undefined
return (...args: Parameters<T>) => {
timer && clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
2 changes: 2 additions & 0 deletions src/lib/stdout-manipulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 inputs and regardless of typechecking success or failure.',
llllvvuu marked this conversation as resolved.
Show resolved Hide resolved
' --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',
Expand Down
49 changes: 49 additions & 0 deletions src/lib/tsc-watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -12,13 +13,16 @@ let firstTime = true;
let firstSuccessKiller: (() => Promise<void>) | null = null;
let successKiller: (() => Promise<void>) | null = null;
let failureKiller: (() => Promise<void>) | null = null;
let emitKiller: (() => Promise<void>) | null = null;
let compilationStartedKiller: (() => Promise<void>) | null = null;
let compilationCompleteKiller: (() => Promise<void>) | null = null;

const {
onFirstSuccessCommand,
onSuccessCommand,
onFailureCommand,
onEmitCommand,
onEmitDebounceMs,
onCompilationStarted,
onCompilationComplete,
maxNodeMem,
Expand Down Expand Up @@ -71,6 +75,27 @@ function killProcesses(currentCompilationId: number, killAll: boolean): Promise<
return runningKillProcessesPromise;
}

let runningKillEmitProcessesPromise: Promise<number> | null = null;
// The same as `killProcesses`, but we separate it to avoid canceling each other
function killEmitProcesses(currentEmitId: number): Promise<number> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that I understand this function... can you please explain?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just added a comment to the function

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);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand All @@ -172,6 +213,7 @@ rl.on('line', function (input) {

if (state.fileEmitted !== null) {
Signal.emitFile(state.fileEmitted);
triggerOnEmit();
}

if (compilationStarted) {
Expand Down Expand Up @@ -201,6 +243,7 @@ rl.on('line', function (input) {
firstTime = false;
Signal.emitFirstSuccess();
runOnFirstSuccessCommand();
triggerOnEmit();
}

Signal.emitSuccess();
Expand Down Expand Up @@ -243,6 +286,12 @@ if (typeof process.on === 'function') {
}
break;

case 'run-on-emit-command':
if (emitKiller) {
emitKiller().then(runOnEmitCommand);
}
break;

Comment on lines +289 to +294
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are missing the client.ts part. and also missing tests

default:
console.log('Unknown message', msg);
}
Expand Down
16 changes: 16 additions & 0 deletions src/test/args-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down