Skip to content

Commit 0c75914

Browse files
Harris-MillerHarris Miller
andauthored
feat: CLI event hook flags (#4457)
* added error handling for execSync * now get hooks from config, abstracted into own file, with typings added * CLI only * removing an unintended import in types.d.ts * add new flags to cli/help.md * added some docs * update docs * added a cli test for watch-event-hooks * scaffold test, need to figure out how to actually write it now * need guidance * tests passing now with change for child.stdout -> process.stderr * fixed test abortOnStderr Co-authored-by: Harris Miller <[email protected]>
1 parent f44a3a3 commit 0c75914

13 files changed

Lines changed: 149 additions & 0 deletions

File tree

cli/help.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ Basic options:
7373
--watch.skipWrite Do not write files to disk when watching
7474
--watch.exclude <files> Exclude files from being watched
7575
--watch.include <files> Limit watching to specified files
76+
--watch.onStart <cmd> Shell command to run on `"START"` event
77+
--watch.onBundleStart <cmd> Shell command to run on `"BUNDLE_START"` event
78+
--watch.onBundleEnd <cmd> Shell command to run on `"BUNDLE_END"` event
79+
--watch.onEnd <cmd> Shell command to run on `"END"` event
80+
--watch.onError <cmd> Shell command to run on `"ERROR"` event
7681
--validate Validate output
7782

7883
Examples:

cli/run/watch-cli.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import loadAndParseConfigFile from './loadConfigFile';
1515
import loadConfigFromCommand from './loadConfigFromCommand';
1616
import { getResetScreen } from './resetScreen';
1717
import { printTimings } from './timings';
18+
import { createWatchHooks } from './watchHooks';
1819

1920
export async function watch(command: Record<string, any>): Promise<void> {
2021
process.env.ROLLUP_WATCH = 'true';
@@ -24,6 +25,7 @@ export async function watch(command: Record<string, any>): Promise<void> {
2425
let configWatcher: FSWatcher;
2526
let resetScreen: (heading: string) => void;
2627
const configFile = command.config ? await getConfigPath(command.config) : null;
28+
const runWatchHook = createWatchHooks(command);
2729

2830
onExit(close);
2931
process.on('uncaughtException', close);
@@ -84,6 +86,7 @@ export async function watch(command: Record<string, any>): Promise<void> {
8486
case 'ERROR':
8587
warnings.flush();
8688
handleError(event.error, true);
89+
runWatchHook('onError');
8790
break;
8891

8992
case 'START':
@@ -93,6 +96,8 @@ export async function watch(command: Record<string, any>): Promise<void> {
9396
}
9497
resetScreen(underline(`rollup v${rollup.VERSION}`));
9598
}
99+
runWatchHook('onStart');
100+
96101
break;
97102

98103
case 'BUNDLE_START':
@@ -107,6 +112,7 @@ export async function watch(command: Record<string, any>): Promise<void> {
107112
cyan(`bundles ${bold(input)}${bold(event.output.map(relativeId).join(', '))}...`)
108113
);
109114
}
115+
runWatchHook('onBundleStart');
110116
break;
111117

112118
case 'BUNDLE_END':
@@ -119,12 +125,14 @@ export async function watch(command: Record<string, any>): Promise<void> {
119125
)}`
120126
)
121127
);
128+
runWatchHook('onBundleEnd');
122129
if (event.result && event.result.getTimings) {
123130
printTimings(event.result.getTimings());
124131
}
125132
break;
126133

127134
case 'END':
135+
runWatchHook('onEnd');
128136
if (!silent && isTTY) {
129137
stderr(`\n[${dateTime()}] waiting for changes...`);
130138
}

cli/run/watchHooks.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { execSync } from 'child_process';
2+
import type { RollupWatchHooks } from '../../src/rollup/types';
3+
import { bold, cyan } from '../../src/utils/colors';
4+
import { stderr } from '../logging';
5+
6+
function extractWatchHooks(
7+
command: Record<string, any>
8+
): Partial<Record<RollupWatchHooks, string>> {
9+
if (!Array.isArray(command.watch)) return {};
10+
11+
return command.watch
12+
.filter(value => typeof value === 'object')
13+
.reduce((acc, keyValueOption) => ({ ...acc, ...keyValueOption }), {});
14+
}
15+
16+
export function createWatchHooks(command: Record<string, any>): (hook: RollupWatchHooks) => void {
17+
const watchHooks = extractWatchHooks(command);
18+
19+
return function (hook: RollupWatchHooks): void {
20+
if (watchHooks[hook]) {
21+
const cmd = watchHooks[hook]!;
22+
23+
if (!command.silent) {
24+
stderr(cyan(`watch.${hook} ${bold(`$ ${cmd}`)}`));
25+
}
26+
27+
try {
28+
// !! important - use stderr for all writes from execSync
29+
const stdio = [process.stdin, process.stderr, process.stderr];
30+
execSync(cmd, { stdio: command.silent ? 'ignore' : stdio });
31+
} catch (e) {
32+
stderr((e as Error).message);
33+
}
34+
}
35+
};
36+
}

docs/01-command-line-reference.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,11 @@ Many options have command line equivalents. In those cases, any arguments passed
379379
--watch.skipWrite Do not write files to disk when watching
380380
--watch.exclude <files> Exclude files from being watched
381381
--watch.include <files> Limit watching to specified files
382+
--watch.onStart <cmd> Shell command to run on `"START"` event
383+
--watch.onBundleStart <cmd> Shell command to run on `"BUNDLE_START"` event
384+
--watch.onBundleEnd <cmd> Shell command to run on `"BUNDLE_END"` event
385+
--watch.onEnd <cmd> Shell command to run on `"END"` event
386+
--watch.onError <cmd> Shell command to run on `"ERROR"` event
382387
--validate Validate output
383388
```
384389

@@ -496,6 +501,14 @@ Specify a virtual file extension when reading content from stdin. By default, Ro
496501

497502
Do not read files from `stdin`. Setting this flag will prevent piping content to Rollup and make sure Rollup interprets `-` and `-.[ext]` as a regular file names instead of interpreting these as the name of `stdin`. See also [Reading a file from stdin](guide/en/#reading-a-file-from-stdin).
498503

504+
#### `--watch.onStart <cmd>`, `--watch.onBundleStart <cmd>`, `--watch.onBundleEnd <cmd>`, `--watch.onEnd <cmd>`, `--watch.onError <cmd>`
505+
506+
When in watch mode, run a shell command `<cmd>` for a watch event code. See also [rollup.watch](guide/en/#rollupwatch).
507+
508+
```sh
509+
rollup -c --watch --watch.onEnd="node ./afterBuildScript.js"
510+
```
511+
499512
### Reading a file from stdin
500513

501514
When using the command line interface, Rollup can also read content from stdin:

src/rollup/types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,8 @@ export interface ChokidarOptions {
864864
usePolling?: boolean;
865865
}
866866

867+
export type RollupWatchHooks = 'onError' | 'onStart' | 'onBundleStart' | 'onBundleEnd' | 'onEnd';
868+
867869
export interface WatcherOptions {
868870
buildDelay?: number;
869871
chokidar?: ChokidarOptions;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const { assertIncludes } = require('../../../../utils.js');
2+
3+
module.exports = {
4+
description: 'onError event hook shell commands write to stderr',
5+
command: 'node wrapper.js -cw --watch.onError "echo error"',
6+
abortOnStderr(data) {
7+
if (data.includes('waiting for changes')) {
8+
return true;
9+
}
10+
},
11+
stderr(stderr) {
12+
assertIncludes(
13+
stderr,
14+
`watch.onError $ echo error
15+
error`
16+
);
17+
}
18+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// missing `=` to trigger onError
2+
var main 42;
3+
4+
export { main as default };
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default {
2+
input: 'main.js',
3+
output: {
4+
dir: '_actual',
5+
format: 'es'
6+
}
7+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env node
2+
3+
process.stdout.isTTY = true;
4+
process.stderr.isTTY = true;
5+
require('../../../../../dist/bin/rollup');
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const { assertIncludes } = require('../../../../utils.js');
2+
3+
module.exports = {
4+
description: 'event hook shell commands write to stderr',
5+
command:
6+
'node wrapper.js -cw --watch.onStart "echo start" --watch.onBundleStart "echo bundleStart" --watch.onBundleEnd "echo bundleEnd" --watch.onEnd "echo end"',
7+
abortOnStderr(data) {
8+
process.stderr.write(data);
9+
if (data.includes('waiting for changes')) {
10+
return true;
11+
}
12+
},
13+
stderr(stderr) {
14+
// assert each hook individually
15+
assertIncludes(
16+
stderr,
17+
`watch.onStart $ echo start
18+
start`
19+
);
20+
assertIncludes(
21+
stderr,
22+
`watch.onBundleStart $ echo bundleStart
23+
bundleStart`
24+
);
25+
assertIncludes(
26+
stderr,
27+
`watch.onBundleEnd $ echo bundleEnd
28+
bundleEnd`
29+
);
30+
assertIncludes(
31+
stderr,
32+
`watch.onEnd $ echo end
33+
end`
34+
);
35+
}
36+
};

0 commit comments

Comments
 (0)