From 0c75914668e63187bccd08f0c6d7b27c65ee0a13 Mon Sep 17 00:00:00 2001 From: Harris Miller Date: Wed, 4 May 2022 22:15:43 -0600 Subject: [PATCH] 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 --- cli/help.md | 5 +++ cli/run/watch-cli.ts | 8 +++++ cli/run/watchHooks.ts | 36 +++++++++++++++++++ docs/01-command-line-reference.md | 13 +++++++ src/rollup/types.d.ts | 2 ++ .../watch/watch-event-hooks-error/_config.js | 18 ++++++++++ .../watch/watch-event-hooks-error/main.js | 4 +++ .../watch-event-hooks-error/rollup.config.js | 7 ++++ .../watch/watch-event-hooks-error/wrapper.js | 5 +++ .../watch/watch-event-hooks/_config.js | 36 +++++++++++++++++++ .../samples/watch/watch-event-hooks/main.js | 3 ++ .../watch/watch-event-hooks/rollup.config.js | 7 ++++ .../watch/watch-event-hooks/wrapper.js | 5 +++ 13 files changed, 149 insertions(+) create mode 100644 cli/run/watchHooks.ts create mode 100644 test/cli/samples/watch/watch-event-hooks-error/_config.js create mode 100644 test/cli/samples/watch/watch-event-hooks-error/main.js create mode 100644 test/cli/samples/watch/watch-event-hooks-error/rollup.config.js create mode 100644 test/cli/samples/watch/watch-event-hooks-error/wrapper.js create mode 100644 test/cli/samples/watch/watch-event-hooks/_config.js create mode 100644 test/cli/samples/watch/watch-event-hooks/main.js create mode 100644 test/cli/samples/watch/watch-event-hooks/rollup.config.js create mode 100644 test/cli/samples/watch/watch-event-hooks/wrapper.js diff --git a/cli/help.md b/cli/help.md index a6f24792880..e687b59fb0c 100644 --- a/cli/help.md +++ b/cli/help.md @@ -73,6 +73,11 @@ Basic options: --watch.skipWrite Do not write files to disk when watching --watch.exclude Exclude files from being watched --watch.include Limit watching to specified files +--watch.onStart Shell command to run on `"START"` event +--watch.onBundleStart Shell command to run on `"BUNDLE_START"` event +--watch.onBundleEnd Shell command to run on `"BUNDLE_END"` event +--watch.onEnd Shell command to run on `"END"` event +--watch.onError Shell command to run on `"ERROR"` event --validate Validate output Examples: diff --git a/cli/run/watch-cli.ts b/cli/run/watch-cli.ts index 8ad23b4976a..fddc954bf24 100644 --- a/cli/run/watch-cli.ts +++ b/cli/run/watch-cli.ts @@ -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): Promise { process.env.ROLLUP_WATCH = 'true'; @@ -24,6 +25,7 @@ export async function watch(command: Record): Promise { 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); @@ -84,6 +86,7 @@ export async function watch(command: Record): Promise { case 'ERROR': warnings.flush(); handleError(event.error, true); + runWatchHook('onError'); break; case 'START': @@ -93,6 +96,8 @@ export async function watch(command: Record): Promise { } resetScreen(underline(`rollup v${rollup.VERSION}`)); } + runWatchHook('onStart'); + break; case 'BUNDLE_START': @@ -107,6 +112,7 @@ export async function watch(command: Record): Promise { cyan(`bundles ${bold(input)} → ${bold(event.output.map(relativeId).join(', '))}...`) ); } + runWatchHook('onBundleStart'); break; case 'BUNDLE_END': @@ -119,12 +125,14 @@ export async function watch(command: Record): Promise { )}` ) ); + 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...`); } diff --git a/cli/run/watchHooks.ts b/cli/run/watchHooks.ts new file mode 100644 index 00000000000..6965ba731c2 --- /dev/null +++ b/cli/run/watchHooks.ts @@ -0,0 +1,36 @@ +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 +): Partial> { + if (!Array.isArray(command.watch)) return {}; + + return command.watch + .filter(value => typeof value === 'object') + .reduce((acc, keyValueOption) => ({ ...acc, ...keyValueOption }), {}); +} + +export function createWatchHooks(command: Record): (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 { + // !! important - use stderr for all writes from execSync + const stdio = [process.stdin, process.stderr, process.stderr]; + execSync(cmd, { stdio: command.silent ? 'ignore' : stdio }); + } catch (e) { + stderr((e as Error).message); + } + } + }; +} diff --git a/docs/01-command-line-reference.md b/docs/01-command-line-reference.md index 1b255e445ef..99fc9321057 100755 --- a/docs/01-command-line-reference.md +++ b/docs/01-command-line-reference.md @@ -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 Exclude files from being watched --watch.include Limit watching to specified files +--watch.onStart Shell command to run on `"START"` event +--watch.onBundleStart Shell command to run on `"BUNDLE_START"` event +--watch.onBundleEnd Shell command to run on `"BUNDLE_END"` event +--watch.onEnd Shell command to run on `"END"` event +--watch.onError Shell command to run on `"ERROR"` event --validate Validate output ``` @@ -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 `, `--watch.onBundleStart `, `--watch.onBundleEnd `, `--watch.onEnd `, `--watch.onError ` + +When in watch mode, run a shell command `` 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: diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index b4c4673212b..c88d53b4182 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -864,6 +864,8 @@ export interface ChokidarOptions { usePolling?: boolean; } +export type RollupWatchHooks = 'onError' | 'onStart' | 'onBundleStart' | 'onBundleEnd' | 'onEnd'; + export interface WatcherOptions { buildDelay?: number; chokidar?: ChokidarOptions; diff --git a/test/cli/samples/watch/watch-event-hooks-error/_config.js b/test/cli/samples/watch/watch-event-hooks-error/_config.js new file mode 100644 index 00000000000..06797470b83 --- /dev/null +++ b/test/cli/samples/watch/watch-event-hooks-error/_config.js @@ -0,0 +1,18 @@ +const { assertIncludes } = require('../../../../utils.js'); + +module.exports = { + description: 'onError event hook shell commands write to stderr', + command: 'node wrapper.js -cw --watch.onError "echo error"', + abortOnStderr(data) { + if (data.includes('waiting for changes')) { + return true; + } + }, + stderr(stderr) { + assertIncludes( + stderr, + `watch.onError $ echo error +error` + ); + } +}; diff --git a/test/cli/samples/watch/watch-event-hooks-error/main.js b/test/cli/samples/watch/watch-event-hooks-error/main.js new file mode 100644 index 00000000000..81e7000be89 --- /dev/null +++ b/test/cli/samples/watch/watch-event-hooks-error/main.js @@ -0,0 +1,4 @@ +// missing `=` to trigger onError +var main 42; + +export { main as default }; diff --git a/test/cli/samples/watch/watch-event-hooks-error/rollup.config.js b/test/cli/samples/watch/watch-event-hooks-error/rollup.config.js new file mode 100644 index 00000000000..d6f089f1d52 --- /dev/null +++ b/test/cli/samples/watch/watch-event-hooks-error/rollup.config.js @@ -0,0 +1,7 @@ +export default { + input: 'main.js', + output: { + dir: '_actual', + format: 'es' + } +}; diff --git a/test/cli/samples/watch/watch-event-hooks-error/wrapper.js b/test/cli/samples/watch/watch-event-hooks-error/wrapper.js new file mode 100644 index 00000000000..8f733cc8815 --- /dev/null +++ b/test/cli/samples/watch/watch-event-hooks-error/wrapper.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +process.stdout.isTTY = true; +process.stderr.isTTY = true; +require('../../../../../dist/bin/rollup'); diff --git a/test/cli/samples/watch/watch-event-hooks/_config.js b/test/cli/samples/watch/watch-event-hooks/_config.js new file mode 100644 index 00000000000..84a302e5d0f --- /dev/null +++ b/test/cli/samples/watch/watch-event-hooks/_config.js @@ -0,0 +1,36 @@ +const { assertIncludes } = require('../../../../utils.js'); + +module.exports = { + description: 'event hook shell commands write to stderr', + command: + 'node wrapper.js -cw --watch.onStart "echo start" --watch.onBundleStart "echo bundleStart" --watch.onBundleEnd "echo bundleEnd" --watch.onEnd "echo end"', + abortOnStderr(data) { + process.stderr.write(data); + if (data.includes('waiting for changes')) { + return true; + } + }, + stderr(stderr) { + // assert each hook individually + assertIncludes( + stderr, + `watch.onStart $ echo start +start` + ); + assertIncludes( + stderr, + `watch.onBundleStart $ echo bundleStart +bundleStart` + ); + assertIncludes( + stderr, + `watch.onBundleEnd $ echo bundleEnd +bundleEnd` + ); + assertIncludes( + stderr, + `watch.onEnd $ echo end +end` + ); + } +}; diff --git a/test/cli/samples/watch/watch-event-hooks/main.js b/test/cli/samples/watch/watch-event-hooks/main.js new file mode 100644 index 00000000000..c3e30233192 --- /dev/null +++ b/test/cli/samples/watch/watch-event-hooks/main.js @@ -0,0 +1,3 @@ +var main = 42; + +export { main as default }; diff --git a/test/cli/samples/watch/watch-event-hooks/rollup.config.js b/test/cli/samples/watch/watch-event-hooks/rollup.config.js new file mode 100644 index 00000000000..d6f089f1d52 --- /dev/null +++ b/test/cli/samples/watch/watch-event-hooks/rollup.config.js @@ -0,0 +1,7 @@ +export default { + input: 'main.js', + output: { + dir: '_actual', + format: 'es' + } +}; diff --git a/test/cli/samples/watch/watch-event-hooks/wrapper.js b/test/cli/samples/watch/watch-event-hooks/wrapper.js new file mode 100644 index 00000000000..8f733cc8815 --- /dev/null +++ b/test/cli/samples/watch/watch-event-hooks/wrapper.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +process.stdout.isTTY = true; +process.stderr.isTTY = true; +require('../../../../../dist/bin/rollup');