UNPKG

mwtsc

Version:

tsc wrapper for midway development

272 lines (244 loc) 7.69 kB
const { fork, spawn } = require('child_process'); const { filterFileChangedText, debug, convertPosixToGnu, } = require('./util'); const { CHILD_PROCESS_EXCEPTION_EXIT_CODE } = require('./constants'); const path = require('path'); const fs = require('fs'); // is windows const isWin = process.platform === 'win32'; const consoleOutput = require('./output'); /** * * @param {*} tscArgs * @param {*} options * options.onFirstWatchCompileSuccess watch 下第一次编译成功 * options.onWatchCompileSuccess watch 下编译成功(非第一次) * options.onWatchCompileFail watch 下编译失败 * options.onCompileSuccess 非 watch 下编译成功,比如直接执行 tsc * @returns */ const forkTsc = (tscArgs = [], options = {}) => { let firstStarted = false; let tscPath = isWin ? 'tsc.cmd' : 'tsc'; const localTscPath = path.join( process.cwd(), 'node_modules', '.bin', tscPath ); if (fs.existsSync(localTscPath)) { tscPath = localTscPath; } const isWatchMode = tscArgs.includes('--watch'); const child = spawn(tscPath, tscArgs, { stdio: ['pipe', 'pipe', 'inherit'], cwd: options.cwd, shell: isWin ? true : undefined, }); debug(`fork tsc process, pid = ${child.pid}`); const totalFileChangedList = new Set(); child.stdout.on('data', data => { data = data.toString('utf8'); if (!isWatchMode) { console.log(data); return; } const [text, fileChangedList] = filterFileChangedText(data); if (fileChangedList.length) { for (const file of fileChangedList) { totalFileChangedList.add(file); } } if (/TS\d{4,5}/.test(text)) { // has error console.log(text); // 如果已经启动了,则传递成功消息给子进程 options.onWatchCompileFail && options.onWatchCompileFail(); // 失败后清空 totalFileChangedList.clear(); } else { console.log(text); /** * 为了减少 tsc 误判,最后一条输出会带有错误信息的数字提示,所以使用正则来简单判断 * 如果 tsc 改了,这里也要改 */ if (/\s\d+\s/.test(text) && /\s0\s/.test(text)) { if (!firstStarted) { firstStarted = true; // emit start options.onFirstWatchCompileSuccess && options.onFirstWatchCompileSuccess(); } else { // 如果已经启动了,则传递成功消息给子进程 options.onWatchCompileSuccess && options.onWatchCompileSuccess(Array.from(totalFileChangedList)); } // 传递后清空 totalFileChangedList.clear(); } } }); child.on('exit', (code, signal) => { if (code === 0) { options.onCompileSuccess && options.onCompileSuccess(); } }); return child; }; const forkRun = (runCmdPath, runArgs = [], options = {}) => { // 判断路径是否是包名还是路径 if (!runCmdPath.startsWith('.') && !runCmdPath.startsWith('/')) { runCmdPath = require.resolve(runCmdPath, { paths: [options.cwd || process.cwd()], }); } let runChild; let onServerReadyCallback; /** * 记录上一次启动状态, undefined 代表未启动 * 有可能自定义启动入口不会触发 server-ready * 只在 --keepalive 下有效 */ let lastBootstrapStatus = undefined; // 先将所有 parentArgs 转换为 GNU 格式 const gnuStyleParentArgs = convertPosixToGnu(options.parentArgs || []); // 从 options.parentArgs 中获取需要的 execArgv,parentArgs 是 POSIX 风格,需要过滤后转成 GNU 风格 const filterExecArgv = ['--inspect', '--inspect-brk']; // 过滤出需要的参数 const requiredExecArgv = gnuStyleParentArgs.filter(arg => filterExecArgv.some(filterArg => arg.startsWith(filterArg)) ); const isPerfInit = runArgs.includes('--perf-init'); function innerFork(isFirstFork = false) { const startTime = Date.now(); // 准备 execArgv const execArgv = process.execArgv .concat(['-r', 'source-map-support/register']) .concat(requiredExecArgv); runChild = fork('wrap.js', runArgs, { stdio: 'inherit', cwd: __dirname, env: { CHILD_CMD_PATH: runCmdPath, CHILD_CWD: options.cwd || process.cwd(), MWTSC_DEVELOPMENT_ENVIRONMENT: 'true', ...process.env, }, execArgv: execArgv, }); debug(`fork run process, pid = ${runChild.pid}`); let currentDebugUrl; const onServerReady = async data => { try { if (data.title === 'server-ready') { onServerReadyCallback && (await onServerReadyCallback( data, isFirstFork, Date.now() - startTime, currentDebugUrl )); lastBootstrapStatus = true; } else if (data.title === 'debug-url') { currentDebugUrl = data.debugUrl; } else if (data.title === 'perf-init') { if (isPerfInit) { // perf init consoleOutput.renderPerfInit(data.data); } } } catch (err) { console.error(err); lastBootstrapStatus = false; } }; runChild.on('message', onServerReady); if (runArgs.includes('--keepalive')) { runChild.once('exit', code => { if (code === CHILD_PROCESS_EXCEPTION_EXIT_CODE) { consoleOutput.renderKeepAlive(); if (lastBootstrapStatus === undefined || lastBootstrapStatus) { // 只有上一次启动成功了,才继续保活拉起,如果启动就失败了,就停止重启 innerFork(false); } } else if (code !== 0) { lastBootstrapStatus = false; } }); } } innerFork(true); // 从参数中获取超时时间,默认 2000ms const killTimeout = (() => { const index = runArgs.indexOf('--kill-timeout'); if (index !== -1 && runArgs[index + 1]) { const timeout = parseInt(runArgs[index + 1], 10); return isNaN(timeout) ? 2000 : timeout; } return 2000; })(); const killRunningChild = async () => { if (!runChild || runChild.exitCode !== null) { // 进程已退出 debug('child process already exited'); return; } return new Promise(resolve => { const now = Date.now(); debug(`send SIGINT to child process ${runChild.pid}`); // 发送退出消息给子进程 runChild.send({ title: 'server-kill', }); // 设置超时处理 const timeoutHandle = setTimeout(() => { try { // 超时后强制结束进程 debug( `send SIGKILL to child process ${runChild.pid} +${ Date.now() - now }ms` ); runChild.kill('SIGKILL'); } catch (err) { debug( `send SIGKILL to child process error, msg = ${err.message}, pid = ${runChild.pid}` ); } }, killTimeout); // 监听进程退出 runChild.once('exit', () => { debug( `child process ${runChild.pid} exited +${Date.now() - now}ms` ); clearTimeout(timeoutHandle); resolve(); }); }); }; return { async kill() { await killRunningChild(); }, async restart() { // 杀进程 await killRunningChild(); // 重新拉起来 innerFork(false); }, forkChild() { innerFork(false); }, onServerReady(readyCallback) { onServerReadyCallback = readyCallback; }, getRealChild() { return runChild; }, }; }; exports.forkTsc = forkTsc; exports.forkRun = forkRun;