run-script-webpack-plugin
run-script-webpack-plugin copied to clipboard
Wait till process stopped
These features would be really nice to have:
- Wait till process stopped during restart
- Kill process with SIGKILL if it didn't stop during timeout
Example:
import {ChildProcess, fork} from 'child_process';
import {Compilation, Compiler, WebpackPluginInstance} from 'webpack';
const killProcess = async ({pid, signal = 'SIGTERM', timeout}) => {
process.kill(pid, signal);
let count = 0;
do {
try {
process.kill(pid, 0);
} catch (e) {
return;
}
if ((count += 100) > timeout) {
break;
}
await new Promise(cb => setTimeout(cb, 100));
} while (true);
try {
process.kill(pid, 'SIGKILL');
} catch (e) {
return;
}
count = 0;
do {
try {
process.kill(pid, 0);
} catch (e) {
return;
}
if ((count += 100) > timeout) {
throw new Error('Timeout process kill');
}
await new Promise(cb => setTimeout(cb, 100));
} while (true);
};
export type RunScriptWebpackPluginOptions = {
autoRestart?: boolean;
args: string[];
cwd?: string;
keyboard: boolean;
name?: string;
nodeArgs: string[];
restartable?: boolean;
signal: boolean | string;
killTimeoutMs?: number;
};
function getSignal(signal: string | boolean) {
// allow users to disable sending a signal by setting to `false`...
if (signal === false) return;
if (signal === true) return 'SIGUSR2';
return signal;
}
export class RunScriptWebpackPlugin implements WebpackPluginInstance {
private readonly options: RunScriptWebpackPluginOptions;
private worker?: ChildProcess;
private _entrypoint?: string;
constructor(options: Partial<RunScriptWebpackPluginOptions> = {}) {
this.options = {
autoRestart: true,
signal: false,
killTimeoutMs: 5000,
// Only listen on keyboard in development, so the server doesn't hang forever
keyboard: process.env.NODE_ENV === 'development',
...options,
args: [...(options.args || [])],
nodeArgs: options.nodeArgs || process.execArgv,
};
if (this.options.restartable) {
this._enableRestarting();
}
}
private _enableRestarting(): void {
if (this.options.keyboard) {
process.stdin.setEncoding('utf8');
process.stdin.on('data', (data: string) => {
if (data.trim() === 'rs') {
this._restartServer();
}
});
}
}
private async _restartServer(): Promise<void> {
console.log('Restarting app...');
if (this.worker?.pid) {
const signal = getSignal(this.options.signal);
await killProcess({
pid: this.worker.pid,
signal,
timeout: this.options.killTimeoutMs,
});
}
this._startServer((worker) => {
this.worker = worker;
});
}
private afterEmit = (compilation: Compilation, cb: (err?: any) => void): void => {
if (this.worker && this.worker.connected && this.worker?.pid) {
if (this.options.autoRestart) {
this._restartServer().then(() => cb()).catch(err => cb(err));
return;
}
const signal = getSignal(this.options.signal);
if (signal) {
killProcess({
pid: this.worker.pid,
signal,
timeout: this.options.killTimeoutMs,
}).then(() => cb()).catch(err => cb(err));
}
cb();
return;
}
this.startServer(compilation, cb);
};
apply = (compiler: Compiler): void => {
compiler.hooks.afterEmit.tapAsync(
{name: 'RunScriptPlugin'},
this.afterEmit,
);
};
private startServer = (compilation: Compilation, cb: () => void): void => {
const {assets, compiler} = compilation;
const {options} = this;
let name;
const names = Object.keys(assets);
if (options.name) {
name = options.name;
if (!assets[name]) {
console.error(
`Entry ${name} not found. Try one of: ${names.join(' ')}`,
);
}
} else {
name = names[0];
if (names.length > 1) {
console.log(
`More than one entry built, selected ${name}. All names: ${names.join(
' ',
)}`,
);
}
}
if (!compiler.options.output || !compiler.options.output.path) {
throw new Error('output.path should be defined in webpack config!');
}
this._entrypoint = `${compiler.options.output.path}/${name}`;
this._startServer((worker) => {
this.worker = worker;
cb();
});
};
private _startServer(cb: (arg0: ChildProcess) => void): void {
const {args, nodeArgs, cwd} = this.options;
if (!this._entrypoint) throw new Error('run-script-webpack-plugin requires an entrypoint.');
const child = fork(this._entrypoint, args, {
execArgv: nodeArgs,
stdio: 'inherit',
cwd,
});
setTimeout(() => cb(child), 0);
}
}