packages/preset-monorepo/src/index.ts (108 lines of code) (raw):

import { runCLI } from 'jest-cli'; import * as path from 'path'; import { writeFileSync, readdir, stat } from 'fs'; import { promisify } from 'util'; import { fork } from 'child_process'; const readdirAsync = promisify(readdir); const statAsync = promisify(stat); const JEST_CONFIG_PATH = path.resolve(__dirname, 'mono-root.jest.config.js'); // 创建一个子进程来解析单个子包的jest配置 // 因为breezr instance的一些资源很难释放,在主进程做会把内存泄漏带到主进程来 // 比如,我们会加载每个子包的node_modules里的breezr-service等模块,这些模块加载以后就不会释放,保存在模块缓存中 // 主进程仅仅需要解析出的jest配置(一个普通对象)而已,因此我们将实例化breezr instance的工作放在子进程中,方便资源的释放 async function forkChildProcessToGetJestConfigForPackage(packageDir: string) { return new Promise((res, rej) => { const forked = fork(path.resolve(__dirname, 'childProcess.js')); let jestConfig: object | undefined, error: object | undefined; forked.on('message', ({ success, data, err }) => { if (success === true) { jestConfig = data; } else { error = err; } forked.kill(); }); forked.on('error', err => { error = err; forked.kill(); }); forked.on('exit', () => { if (typeof jestConfig === 'object') { res(jestConfig); } else { rej(error || { msg: 'forked process exist. unknow error' }); } }); forked.send({ packageDir }); // 20s timeout for each process setTimeout(() => { rej({ msg: `timeout for childProcess: getJestConfigForPackage("${packageDir}")`, jestConfig, error, }); }, 20 * 1000); }); } // 同getJestConfigForPackage,但是它只是”尝试“,如果无法解析出jest配置,则返回null async function tryToGetJestConfigForPackage(packageDir: string) { const dirStats = await statAsync(packageDir); if (dirStats.isDirectory()) { try { return await forkChildProcessToGetJestConfigForPackage(packageDir); } catch (error) { // swallow errors // 有一些包可能没有配置breezr测试,甚至没有breezr,很正常 // 如果要debug,可以在这里打印error // console.log(error); return null; } } return null; } // 对参数中的所有文件夹执行tryToGetJestConfigForPackage async function detectJestConfigsFromPackageDirs(packageDirs: string[]) { const jestConfigs = (await Promise.all( packageDirs.map(async packageDir => tryToGetJestConfigForPackage(packageDir) ) )).filter(item => !!item); return jestConfigs; } // dirWithPackages 通常是monorepo中的"packages"文件夹 async function detectJestConfigsFromDirWithPackages(dirWithPackages: string) { const packageDirs = (await readdirAsync(dirWithPackages)).map(fileName => path.resolve(dirWithPackages, fileName) ); return detectJestConfigsFromPackageDirs(packageDirs); } export default async (config: IOption, cmdArgs: any) => { const { packageDirs, dirWithPackages } = config; const monoRootDir = config.monoRootDir || process.cwd(); let jestConfigs; if (packageDirs) { jestConfigs = await detectJestConfigsFromPackageDirs(packageDirs); } else if (dirWithPackages) { jestConfigs = await detectJestConfigsFromDirWithPackages(dirWithPackages); } else { jestConfigs = await detectJestConfigsFromDirWithPackages( path.resolve(monoRootDir, 'packages') ); } const rootJestConfig = { projects: jestConfigs, }; // 调用runCLI的时候,必须提供配置【路径】,直接传入配置对象会出问题 // 因此我们先把解析好的配置写到一个文件中,然后通过这个文件的路径来指定jest配置 writeFileSync( JEST_CONFIG_PATH, `module.exports=${JSON.stringify(rootJestConfig)}` ); const bootJest = async (jestConfig: any, cliArgs: any) => { await runCLI( { config: JEST_CONFIG_PATH, coverageDirectory: path.resolve(monoRootDir, 'coverage'), ...cliArgs, } as any, [monoRootDir] ); }; const plugins = [ [ require.resolve('@alicloud/console-toolkit-plugin-unit-jest'), { bootJest: bootJest, }, ], ]; return { plugins, }; };