Skip to content

Commit

Permalink
fix: spawn EINVAL in windows (#18)
Browse files Browse the repository at this point in the history
* refactor: retry to use tsc --listEmittedFiles to get changed list

* fix: bind event without compile fail

* fix: windows spawn EINVAL error

* fix: windows spawn EINVAL error

* chore: add test

* chore: add test

* chore: add test

* chore: add test

* chore: add test

* chore: add test

* chore: add test

* chore: add test

* chore: add test

* chore: add test

* chore: add test

* chore: add test

* chore: add test

* chore: add test

* chore: add test
  • Loading branch information
czy88840616 committed May 4, 2024
1 parent 8b5c14d commit bfac5d7
Show file tree
Hide file tree
Showing 15 changed files with 422 additions and 152 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ temp/
dist/
pnpm-lock.yaml
yarn.lock
package-lock.json
package-lock.json
test/fixtures/add_file/a.ts
127 changes: 58 additions & 69 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,30 @@ const {
output,
colors,
debug,
getRelativeDir,
triggerMessage,
} = require('./util');
const path = require('path');
const { replaceTscAliasPaths } = require('tsc-alias');
const chokidar = require('chokidar');

function getRelativeDir(rootDir, filePathDir) {
if (!filePathDir) {
return filePathDir;
}
return path.relative(rootDir, filePathDir);
}
const fs = require('fs');

function run() {
const [runCmd, tscArgs, runArgs, isCleanDirectory] = parseArgs(process.argv);
const cwd = process.cwd();

debug(`main process running, pid = ${process.pid}`);

// 调试模式下
if (process.env.NODE_DEBUG === 'midway:debug') {
tscArgs.push(['--preserveWatchOutput']);
}

// 添加 --listEmittedFiles 参数以便于获取编译后的文件列表
if (!tscArgs.includes('--listEmittedFiles')) {
tscArgs.push('--listEmittedFiles');
}

debug(`cwd: ${cwd}`);
debug(`runCmd: ${runCmd}`);
debug(`tscArgs: ${tscArgs}`);
Expand All @@ -40,13 +43,8 @@ function run() {
let outDir;
let allowJs = false;
let tsconfig;
let fileDeleteWatcher;
let fileChangeWatcher;
let fileDirDeleteWatcher;
let hasPaths = false;
let tsconfigPath;
let isCompileSuccess = false;
const fileChangeList = [];

const projectIndex = tscArgs.findIndex(arg => arg === '--project');
if (projectIndex !== -1) {
Expand Down Expand Up @@ -98,24 +96,23 @@ function run() {
debug(`hasPaths: ${hasPaths}`);

let runChild;
const restart = debounce(async () => {
if (fileChangeList.length === 0) return;
const restart = debounce(async fileChangedList => {
debug(`fileChangedList: ${fileChangedList}`);
if (fileChangedList && fileChangedList.length === 0) return;
async function aliasReplace() {
if (hasPaths) {
// 这里使用全量替换,tsc 增量编译会把老文件修改回去
await replaceTscAliasPaths({
configFile: tsconfigPath,
outDir,
});
// 避免重复触发文件变化
isCompileSuccess = false;
}
output(
`${fileChangeList.length} ${colors.dim('Files has been changed.')}`,
true
);
// 清空文件列表
fileChangeList.length = 0;
if (fileChangedList) {
output(
`${fileChangedList.length} ${colors.dim('Files has been changed.')}`,
true
);
}
}

await Promise.all([runChild && runChild.kill(), aliasReplace()]);
Expand All @@ -138,24 +135,37 @@ function run() {
);
}

function cleanOutDirAndRestart() {
// 碰到任意的删除情况,直接删除整个构建目录
deleteFolderRecursive(path.join(cwd, outDir));
forkTsc(tscArgs, {
cwd,
});
function cleanOutDirAndRestart(p) {
const distPath = path.join(cwd, outDir, p);
if (!fs.existsSync(distPath)) return;
const stat = fs.statSync(distPath);
// is file
if (stat.isFile()) {
// remove file
fs.unlinkSync(distPath);
} else {
// is directory
deleteFolderRecursive(distPath);
}
runAfterTsc();
restart();
}

function onFileChange(p) {
// 这里使用了标识来判断是否编译成功,理论上会有时序问题,但是本质上事件还是同步执行的,测试下来感觉没有问题
if (!isCompileSuccess) return;
debug(`${colors.dim('File ')}${p}${colors.dim(' has been changed.')}`);
fileChangeList.push(p);
// 单个文件的 hot reload 处理
restart();
}
const sourceAbsoluteDir = path.join(cwd, sourceDir);
/**
* 不同平台的监听有很多差异,所以这里采用全文件监听的方案
* 文件的添加,修改交给 tsc 监听,这里只监听删除
*/
const fileDeleteWatcher = chokidar
.watch(sourceAbsoluteDir, {
cwd: sourceAbsoluteDir,
})
.on('all', (event, path) => {
// windows 下无法触发 unlinkDir 事件
if (event === 'unlink' || event === 'unlinkDir') {
cleanOutDirAndRestart(path);
}
});

// 启动执行 tsc 命令
const child = forkTsc(tscArgs, {
Expand Down Expand Up @@ -198,50 +208,35 @@ function run() {
);
}
console.log('');

/**
* 第一次成功之后,开始监听文件变化
* 1、监听 sourceDir 中的 ts 文件或者目录,如果被删除,则做完整的清理
* 2、处理单个文件更新,触发单个文件 HMR 逻辑
*/
fileDeleteWatcher = chokidar.watch('**/**.ts', {
cwd: path.join(cwd, sourceDir),
});

fileDeleteWatcher.on('unlink', cleanOutDirAndRestart);

fileDirDeleteWatcher = chokidar
.watch('**/**', {
cwd: path.join(cwd, sourceDir),
})
.on('unlinkDir', cleanOutDirAndRestart);

fileChangeWatcher = chokidar
.watch('**/**.js', {
cwd: path.join(cwd, outDir),
})
.on('change', onFileChange);
}
triggerMessage('server-first-ready');
} else {
output(
`${colors.green('Node.js server')} ${colors.dim(
'restarted in'
)} ${during} ms\n`
);
triggerMessage('server-ready');
}
}
);
}
debug('watch compile success first');
triggerMessage('watch-compile-success-first');
},
onWatchCompileSuccess: () => {
isCompileSuccess = true;
restart();
onWatchCompileSuccess: fileChangedList => {
debug('watch compile success');
triggerMessage('watch-compile-success');
restart(fileChangedList);
},
onWatchCompileFail: () => {
isCompileSuccess = false;
debug('watch compile fail');
triggerMessage('watch-compile-fail');
restart.clear();
},
onCompileSuccess: () => {
debug('compile success');
triggerMessage('compile-success');
runAfterTsc();
},
});
Expand All @@ -256,12 +251,6 @@ function run() {
if (fileDeleteWatcher) {
await fileDeleteWatcher.close();
}
if (fileChangeWatcher) {
await fileChangeWatcher.close();
}
if (fileDirDeleteWatcher) {
await fileDirDeleteWatcher.close();
}
process.exit(0);
} catch (err) {
console.error(err);
Expand Down
29 changes: 24 additions & 5 deletions lib/process.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { fork, spawn } = require('child_process');
const { filterFileChangedText, debug } = require('./util');
// is windows
const isWin = process.platform === 'win32';

Expand All @@ -17,31 +18,48 @@ const forkTsc = (tscArgs = [], options = {}) => {
const child = spawn(isWin ? 'tsc.cmd' : 'tsc', 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 (/TS\d{4,5}/.test(data)) {
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(data);
console.log(text);
// 如果已经启动了,则传递成功消息给子进程
options.onWatchCompileFail && options.onWatchCompileFail();
// 失败后清空
totalFileChangedList.clear();
} else {
console.log(data);
console.log(text);
/**
* 为了减少 tsc 误判,最后一条输出会带有错误信息的数字提示,所以使用正则来简单判断
* 如果 tsc 改了,这里也要改
*/
if (/\s\d+\s/.test(data) && /\s0\s/.test(data)) {
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();
options.onWatchCompileSuccess &&
options.onWatchCompileSuccess(Array.from(totalFileChangedList));
}
// 传递后清空
totalFileChangedList.clear();
}
}
});
Expand Down Expand Up @@ -75,6 +93,7 @@ const forkRun = (runCmdPath, runArgs = [], options = {}) => {
},
execArgv: process.execArgv.concat(['-r', 'source-map-support/register']),
});
debug(`fork run process, pid = ${runChild.pid}`);
const onServerReady = async data => {
try {
if (data.title === 'server-ready') {
Expand Down

0 comments on commit bfac5d7

Please sign in to comment.