Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: spawn EINVAL in windows #18

Merged
merged 19 commits into from
May 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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