UNPKG

@aniyajs/rotor

Version:

基于webpack5开发的一款专注于打包、运行的工具

420 lines (363 loc) 13.2 kB
"use strict"; // 确定当前构建环境 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); });