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: CLI event hook flags #4457

Merged
merged 14 commits into from May 5, 2022
5 changes: 5 additions & 0 deletions cli/help.md
Expand Up @@ -73,6 +73,11 @@ Basic options:
--watch.skipWrite Do not write files to disk when watching
--watch.exclude <files> Exclude files from being watched
--watch.include <files> Limit watching to specified files
--watch.onStart <cmd> Shell command to run on `"START"` event
--watch.onBundleStart <cmd> Shell command to run on `"BUNDLE_START"` event
--watch.onBundleEnd <cmd> Shell command to run on `"BUNDLE_END"` event
--watch.onEnd <cmd> Shell command to run on `"END"` event
--watch.onError <cmd> Shell command to run on `"ERROR"` event
--validate Validate output

Examples:
Expand Down
8 changes: 8 additions & 0 deletions cli/run/watch-cli.ts
Expand Up @@ -15,6 +15,7 @@ import loadAndParseConfigFile from './loadConfigFile';
import loadConfigFromCommand from './loadConfigFromCommand';
import { getResetScreen } from './resetScreen';
import { printTimings } from './timings';
import { createWatchHooks } from './watchHooks';

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

onExit(close);
process.on('uncaughtException', close);
Expand Down Expand Up @@ -84,6 +86,7 @@ export async function watch(command: Record<string, any>): Promise<void> {
case 'ERROR':
warnings.flush();
handleError(event.error, true);
runWatchHook('onError');
break;

case 'START':
Expand All @@ -93,6 +96,8 @@ export async function watch(command: Record<string, any>): Promise<void> {
}
resetScreen(underline(`rollup v${rollup.VERSION}`));
}
runWatchHook('onStart');

break;

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

case 'BUNDLE_END':
Expand All @@ -119,12 +125,14 @@ export async function watch(command: Record<string, any>): Promise<void> {
)}`
)
);
runWatchHook('onBundleEnd');
if (event.result && event.result.getTimings) {
printTimings(event.result.getTimings());
}
break;

case 'END':
runWatchHook('onEnd');
if (!silent && isTTY) {
stderr(`\n[${dateTime()}] waiting for changes...`);
}
Expand Down
34 changes: 34 additions & 0 deletions cli/run/watchHooks.ts
@@ -0,0 +1,34 @@
import { execSync } from 'child_process';
import type { RollupWatchHooks } from '../../src/rollup/types';
import { bold, cyan } from '../../src/utils/colors';
import { stderr } from '../logging';

function extractWatchHooks(
command: Record<string, any>
): Partial<Record<RollupWatchHooks, string>> {
if (!Array.isArray(command.watch)) return {};

return command.watch
.filter(value => typeof value === 'object')
.reduce((acc, keyValueOption) => ({ ...acc, ...keyValueOption }), {});
}

export function createWatchHooks(command: Record<string, any>): (hook: RollupWatchHooks) => void {
const watchHooks = extractWatchHooks(command);

return function (hook: RollupWatchHooks): void {
if (watchHooks[hook]) {
const cmd = watchHooks[hook]!;

if (!command.silent) {
stderr(cyan(`watch.${hook} ${bold(`$ ${cmd}`)}`));
}

try {
execSync(cmd, { stdio: command.silent ? 'ignore' : 'inherit' });
} catch (e) {
stderr((e as Error).message);
}
}
};
}
13 changes: 13 additions & 0 deletions docs/01-command-line-reference.md
Expand Up @@ -379,6 +379,11 @@ Many options have command line equivalents. In those cases, any arguments passed
--watch.skipWrite Do not write files to disk when watching
--watch.exclude <files> Exclude files from being watched
--watch.include <files> Limit watching to specified files
--watch.onStart <cmd> Shell command to run on `"START"` event
--watch.onBundleStart <cmd> Shell command to run on `"BUNDLE_START"` event
--watch.onBundleEnd <cmd> Shell command to run on `"BUNDLE_END"` event
--watch.onEnd <cmd> Shell command to run on `"END"` event
--watch.onError <cmd> Shell command to run on `"ERROR"` event
--validate Validate output
```

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

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).

#### `--watch.onStart <cmd>`, `--watch.onBundleStart <cmd>`, `--watch.onBundleEnd <cmd>`, `--watch.onEnd <cmd>`, `--watch.onError <cmd>`

When in watch mode, run a shell command `<cmd>` for a watch event code. See also [rollup.watch](guide/en/#rollupwatch).

```sh
rollup -c --watch --watch.onEnd="node ./afterBuildScript.js"
```

### Reading a file from stdin

When using the command line interface, Rollup can also read content from stdin:
Expand Down
2 changes: 2 additions & 0 deletions src/rollup/types.d.ts
Expand Up @@ -864,6 +864,8 @@ export interface ChokidarOptions {
usePolling?: boolean;
}

export type RollupWatchHooks = 'onError' | 'onStart' | 'onBundleStart' | 'onBundleEnd' | 'onEnd';

export interface WatcherOptions {
buildDelay?: number;
chokidar?: ChokidarOptions;
Expand Down
24 changes: 24 additions & 0 deletions test/cli/samples/watch/watch-event-hooks/_config.js
@@ -0,0 +1,24 @@
const { assertIncludes } = require('../../../../utils.js');

module.exports = {
description: 'onStart event hoot shell command executes correctly',
command:
'rollup -cw --watch --watch.onStart "echo start" --watch.onBundleStart "echo bundleStart" --watch.onBundleEnd "echo bundleEnd" --watch.onEnd "echo onEnd" --watch.onError "echo onError"',
Copy link
Member

Choose a reason for hiding this comment

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

-cw includes --watch 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

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

lol oops.

I could use some guidance on how to do this test exactly. The test/cli/samples/ suite of test all seem to do tests on either console output or bundle file output. the one I threw up there was just a POC to test all of the watch event hooks at once by just looking for the echos being written

I can get that test working or I can take another direction. Would you like me to break up each hook into its own test? should I test that --silent doesn't print to stderr?

Copy link
Member

Choose a reason for hiding this comment

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

No, I think the test is fine in principle, and testing all hooks together seems adequate to me considering me that there is not too much logic involved. You should just make sure to make it a little more robust, e.g. see my other suggestion of preprocessing the output by splitting it into lines, doing some mapping and filtering and possibly doing an deepStrictEqual on the result. Looks to me like "echo" should also work on Windows, which is definitely a bonus.

stderr(stderr) {
assertIncludes(
stderr,
`watch.onStart $ echo start
start
bundles main.js → _actual...
watch.onBundleStart $ echo bundleStart
bundleStart
created _actual in 16ms
Copy link
Member

Choose a reason for hiding this comment

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

A CLI test must not depend on things like runtime. The usual solution is to use assertIncludes for substrings like created _actual etc.

Copy link
Member

Choose a reason for hiding this comment

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

Another idea: Split the output by line, filtering empty ones and mapping the remaining ones to their first word. Then you would just need to compare

start
bundles
bundleStart
created
...

watch.onBundleEnd $ echo bundleEnd
bundleEnd
watch.onEnd $ echo onEnd
onEnd
`
);
// assert.strictEqual(stderr.slice(0, 12), `${CLEAR_SCREEN}${UNDERLINE}rollup`);
}
};
3 changes: 3 additions & 0 deletions test/cli/samples/watch/watch-event-hooks/main.js
@@ -0,0 +1,3 @@
var main = 42;

export { main as default };
7 changes: 7 additions & 0 deletions test/cli/samples/watch/watch-event-hooks/rollup.config.js
@@ -0,0 +1,7 @@
export default {
input: 'main.js',
output: {
dir: '_actual',
format: 'es'
}
};