@aniyajs/rotor
Version:
基于webpack5开发的一款专注于打包、运行的工具
420 lines (363 loc) • 13.2 kB
JavaScript
;
// 确定当前构建环境
process.env.NODE_ENV = "development";
// 抛出所有未被捕获的错误
process.on("unhandledRejection", (error) => {
throw error;
});
const path = require("path");
const fs = require('fs-extra');
const chokidar = require("chokidar");
const chalk = require("chalk");
const escape = require("escape-string-regexp");
const { initRequireFileHandle, initTempHandle } = require('../utils/initialize.js');
const {
isFileExists,
depthDependFilePaths,
getFilePathsInDirectory,
clearConsole,
itemOrEvent,
funcOrStr,
} = require('../utils/common.js');
const { runWebpack, startWebpack } = require('../utils/runWebpack.js');
const paths = require('../utils/paths.js');
// 当前是否处于终端
const isInteractive = process.stdout.isTTY;
// 可忽略的依赖后缀
const dependentSuffixs = [".js", ".jsx", ".ts", ".tsx", ".json", ".less", ".scss", ".sass", ".css"];
// 匹配隐藏文件
const hiddenFileRule = /(^|[\/\\])\../;
// 匹配mock文件
const mockFileRule = new RegExp(`^${escape(paths.appMockPath)}/`);
// 检查所需文件是否存在 package.json、src/document.ejs
const fileExist = isFileExists([paths.appHtml, paths.appPackageJson], paths.appPath);
if (!fileExist) {
process.exit(1);
}
// 获取config文件夹中文件
const envConfig = getFilePathsInDirectory(paths.appConfigPath, /^config\.[^.]+/);
initRequireFileHandle()
.then(async (isInitRequire) => {
if (!isInitRequire) {
return new Error(null);
}
// 读取config文件深度依赖文件路径
// 过滤隐藏文件
const dependFiles = depthDependFilePaths(
[paths.appConfigJs, ...envConfig],
dependentSuffixs,
hiddenFileRule,
)
let FSWatcher;
let webpackProcess;
// 防抖定时器
let debounceTimer = null;
// 项目初始化
let initApp = true;
// webpack server 是否首次启动
let isFirstStart = true;
// 匹配环境变量文件
const envFileReg = /^config\.[a-zA-Z0-9]+\.(ts|js)$/;
// 正在更改的文件
let changingFiles = [];
// config 源文件内容 hash,用于快速判断是否真的变了
let lastConfigSourceHash = null;
/**
* 防抖触发 webpack 重启
* 替代 watchjs 的属性监听,用简单的 debounce 实现
*/
function scheduleWebpackRestart() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
try {
await handleWebpackRestart();
} catch (err) {
console.log(chalk.red("Restart error: ") + (err.message || err));
}
}, 1000);
}
/**
* 处理 webpack 重启逻辑
*/
async function handleWebpackRestart() {
if (!fs.existsSync(paths.appConfigJs)) {
console.log();
// 每次启动时,清除缓存文件
if (fs.pathExistsSync(paths.appTempCachePath)) {
fs.removeSync(paths.appTempCachePath);
}
if (fs.pathExistsSync(paths.appConfigTempPath)) {
fs.removeSync(paths.appConfigTempPath);
}
if (fs.pathExistsSync(paths.appEnvConfigTempPath)) {
fs.removeSync(paths.appEnvConfigTempPath);
}
// config/config.{js|ts} 文件初始不存在时
// paths.appTempMetaJs 文件写入 @aniyajs/rotor 默认配置
const metaJson = fs.readJSONSync(paths.appTempMetaJs);
delete require.cache[require.resolve("../webpack/defaultConfig")];
const memoConfig = require("../webpack/defaultConfig")(false);
const strConfig = funcOrStr({
...memoConfig,
});
fs.writeJsonSync(
paths.appTempMetaJs,
{
...metaJson,
paths: {},
lastConfig: strConfig,
},
{ spaces: 2 },
);
webpackProcess = await startWebpack(webpackProcess);
} else {
// 先快速检查变更文件的内容是否真的变了
// 避免无意义的重新编译和缓存重建
const { createHash } = require("crypto");
// 用所有被监听的 config 相关文件(包括深度依赖)来计算 hash
const data = fs.readJsonSync(paths.appTempMetaJs);
const cacheListenPaths = data.cacheListenPaths || [];
const allConfigFiles = cacheListenPaths.filter(f => fs.existsSync(f) && fs.statSync(f).isFile());
const currentHash = createHash("md5");
allConfigFiles.forEach(f => {
try {
currentHash.update(f + ":" + fs.readFileSync(f, "utf-8"));
} catch (e) {
// 文件可能已被删除
}
});
const currentDigest = currentHash.digest("hex");
if (lastConfigSourceHash && currentDigest === lastConfigSourceHash && !isFirstStart) {
// 源文件内容没变,跳过编译
return;
}
lastConfigSourceHash = currentDigest;
console.log();
// 清除缓存文件
if (fs.pathExistsSync(paths.appTempCachePath)) {
fs.removeSync(paths.appTempCachePath);
}
if (fs.pathExistsSync(paths.appConfigTempPath)) {
fs.removeSync(paths.appConfigTempPath);
}
if (fs.pathExistsSync(paths.appEnvConfigTempPath)) {
fs.removeSync(paths.appEnvConfigTempPath);
}
// 环境变量文件更改
let isChangeEnv =
changingFiles.findIndex((changingFile) =>
envConfig.find(envFile => envFile === changingFile),
) > -1;
// 初始化mock、config配置缓存文件,并拿到config缓存文件的文件及config目录
const initSuccessPaths = await initTempHandle(
envFileReg,
cacheListenPaths,
isFirstStart,
);
changingFiles = [];
const newProcess = await runWebpack(webpackProcess, initSuccessPaths, isChangeEnv);
// 只有真正重启了 webpack(返回了新进程)才清屏
if (newProcess) {
webpackProcess = newProcess;
if (isInteractive) {
clearConsole();
}
}
}
isFirstStart = false;
}
// config/config.{js|ts} 文件初始不存在时
// paths.appTempMetaJs 文件写入 @aniyajs/rotor 默认配置
if (!fs.existsSync(paths.appConfigJs)) {
const metaJson = fs.readJSONSync(paths.appTempMetaJs);
delete require.cache[require.resolve("../webpack/defaultConfig")];
const memoConfig = require("../webpack/defaultConfig")(false);
const strConfig = funcOrStr({
...memoConfig,
});
fs.writeJsonSync(
paths.appTempMetaJs,
{
...metaJson,
paths: {},
lastConfig: strConfig,
},
{ spaces: 2 },
);
webpackProcess = await startWebpack(webpackProcess);
isFirstStart = false;
}
FSWatcher = chokidar.watch(
[paths.appConfigPath, ...dependFiles],
{
ignored: hiddenFileRule, // 忽略所有隐藏文件和文件夹,不加入监听
persistent: true,
},
);
FSWatcher.on("all", async (event, listenPath) => {
if (event === "addDir" || event === "unlinkDir") {
return null;
}
if (fs.existsSync(paths.appConfigJs)) {
console.log(
chalk.green(`[${event}] ${path.relative(paths.appPath, listenPath)}`),
);
}
if (
changingFiles.findIndex(
(changingFile) => changingFile === listenPath,
) === -1
) {
changingFiles.push(listenPath);
}
if (/^unlink/.test(event)) {
changingFiles = changingFiles.filter(
(changingFile) => changingFile !== listenPath,
);
}
// 使用 debounce 替代 watchjs 属性监听
scheduleWebpackRestart();
});
FSWatcher.on("ready", () => {
const oldDependFileInfos = FSWatcher.getWatched();
let cacheListenPaths = [];
for (const oldDependFileKey in oldDependFileInfos) {
oldDependFileInfos[oldDependFileKey].forEach((oldDependFile) => {
cacheListenPaths.push(path.resolve(oldDependFileKey, oldDependFile));
});
}
// paths.appConfigJs 存在时,才对paths.appTempMetaJs简易初始化
if (fs.existsSync(paths.appConfigJs)) {
fs.writeJsonSync(
paths.appTempMetaJs,
{
cacheListenPaths,
},
{ spaces: 2 },
);
}
initApp = false;
});
FSWatcher.on("unlink", (filePath) => {
const data = fs.readJsonSync(paths.appTempMetaJs);
const cacheListenPaths = data.cacheListenPaths;
const unlinkIndex = cacheListenPaths.indexOf(filePath);
cacheListenPaths.splice(unlinkIndex, 1);
fs.writeJsonSync(
paths.appTempMetaJs,
{ ...data, cacheListenPaths },
{ spaces: 2 },
);
});
FSWatcher.on("unlinkDir", (floderPath) => {
const data = fs.readJsonSync(paths.appTempMetaJs);
const cacheListenPaths = data.cacheListenPaths;
const unlinkIndex = cacheListenPaths.indexOf(floderPath);
cacheListenPaths.splice(unlinkIndex, 1);
fs.writeJsonSync(
paths.appTempMetaJs,
{ ...data, cacheListenPaths },
{ spaces: 2 },
);
});
FSWatcher.on("add", (filePath) => {
if (!initApp) {
const data = fs.readJsonSync(paths.appTempMetaJs);
fs.writeJsonSync(
paths.appTempMetaJs,
{
...data,
cacheListenPaths: [...data.cacheListenPaths, filePath].reduce(
(pre, cur) => (pre.includes(cur) ? [...pre] : [...pre, cur]),
[],
),
},
{ spaces: 2 },
);
}
});
// change 事件的防抖定时器
let changeDebounceTimer = null;
FSWatcher.on("change", () => {
// 防抖:避免短时间内多次执行重量级的依赖解析
clearTimeout(changeDebounceTimer);
changeDebounceTimer = setTimeout(() => {
const depthFiles = depthDependFilePaths(
[paths.appConfigJs, ...envConfig],
dependentSuffixs,
hiddenFileRule,
);
const newDepthFiles = depthFiles.length ? depthFiles : [];
const { cacheListenPaths } = fs.readJsonSync(paths.appTempMetaJs);
const configFiles = fs.existsSync(paths.appConfigPath) ? fs
.readdirSync(paths.appConfigPath)
.reduce((pre, cur) => {
pre.push(path.resolve(paths.appConfigPath, cur));
return pre;
}, []) : [];
const newWatchFiles = [...newDepthFiles, ...configFiles, paths.appConfigPath].reduce((pre, cur) => {
if (!pre.includes(cur)) {
pre.push(cur);
}
return pre;
}, []);
const { removeData, addData } = itemOrEvent(
cacheListenPaths,
newWatchFiles,
);
if (removeData.length > 0 || addData.length > 0) {
const data = fs.readJsonSync(paths.appTempMetaJs);
const cacheListenPaths = data.cacheListenPaths;
if (removeData.length > 0) {
removeData.forEach((removeFile) => {
const removeIndex = cacheListenPaths.indexOf(removeFile);
cacheListenPaths.splice(removeIndex, 1);
if (fs.existsSync(paths.appConfigJs)) {
console.log(
chalk.green(
`[unlink] ${path.relative(paths.appPath, removeFile)}`,
),
);
}
changingFiles = changingFiles.filter(
(changingFile) => changingFile !== removeFile,
);
});
FSWatcher.unwatch(removeData);
}
if (addData.length > 0) {
addData.forEach((addFile) => {
cacheListenPaths.push(addFile);
});
FSWatcher.add(addData);
}
fs.writeJsonSync(
paths.appTempMetaJs,
{ ...data, cacheListenPaths },
{ spaces: 2 },
);
}
}, 500); // change 防抖 500ms
});
// 监听报错退出程序
FSWatcher.on("error", (error) => {
console.log(chalk.red("Error: ") + error.message || error);
FSWatcher && FSWatcher.close();
webpackProcess && webpackProcess.send('close');
webpackProcess && webpackProcess.kill && webpackProcess.kill();
process.exit(1);
});
// CTRL + C 退出程序
["SIGINT", "SIGTERM"].forEach(function (sig) {
process.on(sig, function () {
FSWatcher && FSWatcher.close();
webpackProcess && webpackProcess.send('close');
webpackProcess && webpackProcess.kill && webpackProcess.kill();
process.exit();
});
});
})
.catch((error) => {
console.log(error);
process.exit(1);
});