UNPKG

hook-shell-script-webpack-plugin

Version:

A Webpack plugin for running shell scripts on any hook provided by Webpack.

144 lines (130 loc) 3.97 kB
//@ts-check const { spawn } = require('child_process'); const NAME = 'HookShellScriptPlugin'; /** * @typedef {import('webpack').Compiler} Compiler * @typedef {string | {command: string, args: string[]}} CommandAndArgs * @typedef {(...params: any[]) => CommandAndArgs} HookFunc * @typedef {CommandAndArgs | HookFunc} HookValueType */ /** * @typedef {Compiler["hooks"][T] extends import('tapable').Hook<infer A> ? A extends any[] ? A : any[] : any[]} Args<T> * @template {keyof Compiler["hooks"]} T */ class HookShellScriptPlugin { /** * Add hooks and scripts to run on each hook. * @param {Partial<{[hookName in keyof Compiler["hooks"]]: (CommandAndArgs | ((...args: Args<hookName>) => CommandAndArgs))[]}>} hooks */ constructor(hooks = {}) { this._procs = {}; this._hooks = hooks; } /** * Add callbacks for each hook. If a hook doesn't exist on the compiler, throw an error. * @param {Compiler} compiler */ apply(compiler) { this.watch = compiler.options.watch; this.logger = compiler.getInfrastructureLogger(NAME); Object.keys(this._hooks).forEach(hookName => { if (!compiler.hooks[hookName]) { this._handleError(`The hook ${hookName} does not exist on the Webpack compiler.`); } compiler.hooks[hookName].tap(NAME, (...args) => { this._hooks[hookName].forEach(s => this._handleScript(s, args)); }); }); } /** * Parse a given script into a command and arguments * @param {HookValueType} script * @param {...any[]} params * @returns {{command: string | null, args: string[]}} */ _parseScript(script, ...params) { switch (typeof script) { case 'string': { const [command, ...args] = script.split(' '); return { command, args }; } case 'function': return this._parseScript(script(...params)); case 'object': return script; default: return { command: null, args: [] }; } } /** * Run a script, cancelling an already running iteration of that script. * @param {HookValueType} script * @param {any[]} params */ _handleScript(script, params) { const { command, args = [] } = this._parseScript(script, ...params); if (!command) { this._handleError(`Missing command for script ${script}`); return; } let key = command; if (args.length > 0) { key += ` ${args.join(' ')}`; } this._log(`Running script: ${key}`); if (this._procs[key]) this._killProc(key); this._procs[key] = spawn(command, args, { stdio: 'inherit' }); this._procs[key].on('error', this._onScriptError.bind(this, key)); this._procs[key].on('exit', this._onScriptComplete.bind(this, key)); } /** * Kill a running process. * @param {string} key */ _killProc(key) { this._procs[key].kill(); } /** * Handle an error by killing the build if not in watch mode. * @param {string} msg */ _handleError(msg) { if (!this.watch) { throw new Error(`[${NAME}] ${msg}`); } // @ts-ignore this.logger.error(msg); } /** * Log a message to the console. * @param {string} msg */ _log(msg) { // @ts-ignore this.logger.info(msg); } /** * When the script has completed, log a success if there was no error while running it. * @param {string} key * @param {number} error * @param {string} msg */ _onScriptComplete(key, error, msg) { this._procs[key] = null; if (msg === 'SIGTERM' || msg === 'SIGINT') { this._log(`Killing script: ${key}`); } else if (error) { this._onScriptError(key, error); } this._log(`Completed script: ${key}`); } /** * When a script has errored out, log or throw the error. * @param {string} script * @param {string} error */ _onScriptError(script, error) { this._handleError(`Error while running \`${script}\`: ${error}`); } } module.exports = HookShellScriptPlugin;