UNPKG

@mega-apps/cli

Version:

Mom builder for all mega modules apps. The recommended basic operation dependency package is attached, and users can check and repair defects in actual applications.

521 lines (457 loc) 16.4 kB
const fs = require("fs"); const fse = require("fs-extra"); const path = require("path"); const {spawn} = require("child_process"); const _ = require("lodash"); // 使用了@mega-apps/package-sort 的压缩版本 const {sortPackageJson} = require("./package-sort.min"); const noopFunc = () => { }; class Helper { constructor(workdir = process.cwd(), logger = console, prefixName = "") { this.name = "helper"; this.prefixName = prefixName; this.workdir = workdir; this.memoryBackup = {}; this.logger = { info: noopFunc, success: noopFunc, error: noopFunc, debug: noopFunc, warn: noopFunc, ...logger || {}, }; // console.debug(`info:`,{ workdir,prefixName }); } getFileOfWorkDirAbsUrl(fileName) { return path.join(this.workdir, fileName); } dirIsExist(dirPath) { if (!fse.pathExistsSync(filePath)) return false; const stat = fs.statSync(filePath); return stat.isDirectory(); } fileIsExist(filePath) { if (!fse.pathExistsSync(filePath)) return false; const stat = fs.statSync(filePath); return stat.isFile(); } workdirFileIsExist(fileName) { return this.fileIsExist(this.getFileOfWorkDirAbsUrl(fileName)); } getFileContent(filePath) { let content = ""; const fileExist = this.fileIsExist(filePath); this.logger.debug(`[${fileExist}]: ${filePath} will be read ...`); if (fileExist) { try { content = fs.readFileSync(filePath, "utf8").toString(); } catch (err) { console.error(err); throw err; } } return content; } getProjectFileContent(fileName) { const filePath = this.getFileOfWorkDirAbsUrl(fileName); return this.getFileContent(filePath); } getBuildInConfigDirSubFileContent(fileName) { const filePath = path.join(__dirname, "../config", fileName); return this.getFileContent(filePath); } async installPeerDependencies({env = {}, hooks = {}} = {}) { const {installPackage} = require("@antfu/install-pkg"); const getHooks = (key) => { try { const fns = Array.from(hooks[key] || []); if (fns) { return fns; } } catch (e) { } return []; } const filePath = path.join(__dirname, "./deps.json"); const depsPkg = JSON.parse(fs.readFileSync(filePath).toString()); const {dependencies} = depsPkg; const packageList = []; Object.keys(dependencies).forEach((pkgName) => { const pkgVersion = dependencies[`${pkgName}`]; packageList.push(`${pkgName}@${pkgVersion}`) }) getHooks("enter").forEach(fn => fn([{dependencies}, hooks])); await installPackage(packageList, { packageManager: "yarn", dev: true, silent: true, }); getHooks("exit").forEach(fn => fn([{dependencies}, hooks])); } removeFile(fileName) { if (this.workdirFileIsExist(fileName)) { try { const file = this.getFileOfWorkDirAbsUrl(fileName); this.logger.debug(`${file} will be removed ...`); fse.removeSync(file); } catch (err) { console.error(err); throw err; } } } copySync(...args) { fse.copySync(...args) } copyFile(fileName, overwrite = false) { try { const srcFile = path.join(__dirname, "../config", fileName); if (this.fileIsExist(srcFile)) { if (overwrite) { this.removeFile(fileName); } const destFile = this.getFileOfWorkDirAbsUrl(fileName); if (!this.fileIsExist(destFile)) { fse.copySync(srcFile, destFile, {overwrite}); } } } catch (err) { console.error(err); throw err; } } isCLIPkg(fileName, pkgName) { if (this.fileIsExist(fileName)) { try { const filePath = this.getFileOfWorkDirAbsUrl(fileName); const json = JSON.parse(fs.readFileSync(filePath).toString()); return json["name"] === pkgName; } catch (e) { throw e; } } return false; } injectModuleInFile(filePath, {injectModule = true} = {}) { if (!this.fileIsExist(filePath)) return; const content = fs.readFileSync(filePath).toString(); const injectCode = `require('@mega-apps/cli/bin/inject-module-require.js')`; if (content.includes(injectCode)) return; let regex = null; const caseMatch = (regex) => { return (content.match(regex) || []).length > 0; } // case1: regex = /^#!\/usr\/bin\/env node/; if (caseMatch(regex)) { return fs.writeFileSync(filePath, content.replace(regex, ` #!/usr/bin/env node ${injectCode}; `.trim())); } // case2: regex = /^'use strict';/; if (caseMatch(regex)) { return fs.writeFileSync(filePath, content.replace(regex, ` 'use strict'; ${injectCode}; `.trim())); } // case3: regex = /^"use strict";/; if (caseMatch(regex)) { return fs.writeFileSync(filePath, content.replace(regex, ` "use strict"; ${injectCode}; `.trim())); } } replaceJsonFileContent(fileName, partJson = {}, format = (jsonStr) => jsonStr, overwrite = false) { try { const filePath = this.getFileOfWorkDirAbsUrl(fileName); const json = JSON.parse(fs.readFileSync(filePath).toString()); const newJson = overwrite ? {...partJson} : {...json, ...partJson}; const content = format(JSON.stringify(newJson, null, 2)); fs.writeFileSync(filePath, content); } catch (err) { console.error(err); throw err; } } /** * 更新文件JSON内容 * @param fileName 文件路径 * @param partJsonAndOpList 操作列表 * @param format 格式化JSON操作 */ updateJsonFileContent(fileName, partJsonAndOpList = [], format = (jsonStr) => jsonStr) { try { const filePath = this.getFileOfWorkDirAbsUrl(fileName); let json = JSON.parse(fs.readFileSync(filePath).toString()); // 根据 partJsonAndOpList 操作 partJsonAndOpList.forEach(({op, path, value, condition = () => false}) => { const has = _.has(json, path); if (op === "force-set") { // 强制设置 _.set(json, path, value); } else if (op === "force-unset") { // 强制删除 _.unset(json, path); } else if (op === "force-update" && typeof value === "function") { // 强制更新 _.update(json, path, value); } else if (op === "has-set") { // 路径存在,才设置 has && _.set(json, path, value); } else if (op === "has-unset") { // 路径存在,才删除 has && _.unset(json, path); } else if (op === "has-update" && typeof value === "function") { // 路径存在,才更新 has && _.update(json, path, value); } else if (op === "not-has-set") { // 路径不存在,才设置 !has && _.set(json, path, value); } else { // 根据 condition 执行操作 // 无操作 } }); // 存储最新的json数据 const content = format(JSON.stringify(json, null, 2)); fs.writeFileSync(filePath, content); } catch (err) { console.error(err); throw err; } } deleteJsonFileItem(fileName, itemKeys = [], format = (jsonStr) => jsonStr) { try { const filePath = this.getFileOfWorkDirAbsUrl(fileName); const json = JSON.parse(fs.readFileSync(filePath).toString()); itemKeys.forEach((keysArr) => { keysArr.forEach((key) => { delete json[key]; }) }); const content = format(JSON.stringify(json, null, 2)); fs.writeFileSync(filePath, content); } catch (err) { console.error(err); throw err; } } createInjectModulesForFileFunc(checkFiles = [], workdir = null) { const relativeDir = workdir || this.workdir; return () => { checkFiles.forEach((fileName) => { // format bin file const filePath = path.join(relativeDir, "node_modules", fileName); this.injectModuleInFile(filePath); // fix for pnpm install deps. try { const filePath = require.resolve(fileName); this.injectModuleInFile(filePath); } catch (e) {} }) } } createRemoveUntrackedFileFunc(checkFiles = []) { return () => { checkFiles.forEach((fileName) => { if (this.workdirFileIsExist(fileName)) { this.removeFile(fileName); } }); } } createCheckFileExistCopyFunc(checkFiles = [], overwrite = false) { return () => { const flag = !checkFiles.some((file) => this.workdirFileIsExist(file)) || overwrite; if (checkFiles.length > 0 && flag) { const fileName = checkFiles[0]; this.logger.debug(`${fileName} will be copied ...`); this.copyFile(fileName, overwrite); } } } memBackupFilesFunc(checkFiles) { return () => { if (checkFiles.length > 0) { checkFiles.forEach((fileName) => { const url = this.getFileOfWorkDirAbsUrl(fileName); const content = this.getFileContent(fileName); if (content) { this.memoryBackup[`${url}`] = content; } }) } } } memRestoreFilesFunc(checkFiles) { return () => { if (checkFiles.length > 0) { checkFiles.forEach((fileName) => { const url = this.getFileOfWorkDirAbsUrl(fileName); const content = this.memoryBackup[`${url}`]; if (content) { fs.writeFileSync(url, content); } }) } } } createInjectPackageInfoFunc(checkFiles = [], json = {}) { return () => { if (checkFiles.length > 0) { checkFiles.forEach((fileName) => { if (this.workdirFileIsExist(fileName)) { this.replaceJsonFileContent(fileName, json, sortPackageJson); } }); } } } createUpdatePackageInfoFunc(checkFiles = [], opList = []) { return () => { if (checkFiles.length > 0) { checkFiles.forEach((fileName) => { if (this.workdirFileIsExist(fileName)) { this.updateJsonFileContent(fileName, opList, sortPackageJson); } }); } } } createDeleteJsonFileItemFunc(checkFiles = [], itemKeys = []) { return () => { if (checkFiles.length > 0) { checkFiles.forEach((fileName) => { if (this.workdirFileIsExist(fileName)) { this.deleteJsonFileItem(fileName, itemKeys, sortPackageJson); } }); } } } createOverwriteFileContentFunc(checkFiles = [], content = "", overwrite=true) { return () => { if (checkFiles.length > 0) { checkFiles.forEach((fileName) => { const filePath = this.getFileOfWorkDirAbsUrl(fileName); if (overwrite || !this.fileIsExist(filePath)) { fs.writeFileSync(filePath, content); } }); } } } runCheck() { try { const actionName = `${this.prefixName} Check Project config and some config files`; this.logger.info(`${actionName} running ...`); Array.from([ this.createCheckFileExistCopyFunc([ "commitlint.config.js", ...["js", "json", "yml"].map((ext) => `.commitlintrc.${ext}`), ]), this.createCheckFileExistCopyFunc([".ls-lint.yml"]), this.createCheckFileExistCopyFunc([ ...["js", "json", "yml"].map((ext) => `.huskyrc.${ext}`), ]), this.createCheckFileExistCopyFunc([".yarnrc"]), this.createCheckFileExistCopyFunc(["jsconfig.json"]), this.createCheckFileExistCopyFunc(["jest.config.js"]), this.createCheckFileExistCopyFunc(["tsconfig.json"]), this.createCheckFileExistCopyFunc([".eslintrc.js"], true), this.createCheckFileExistCopyFunc([".eslintignore"]), this.createCheckFileExistCopyFunc(["stylelint.config.js"], true), this.createCheckFileExistCopyFunc([".stylelintignore"]), this.createCheckFileExistCopyFunc([".editorconfig"]), this.createCheckFileExistCopyFunc([".gitattributes"]), this.createCheckFileExistCopyFunc(["tailwind.config.js"]), this.createCheckFileExistCopyFunc(["types/vue-shim.d.ts"]), this.createCheckFileExistCopyFunc([".nvmrc"]), // 强制删除,原因:有的时候,npm安装的依赖还是不够稳定,所以建议使用yarn来安装 this.createRemoveUntrackedFileFunc(["package-lock.json"]), // 用途:强制工程使用 yarn 来安装依赖 ...[ // 不强制覆盖 .npmrc 文件的内容, 其他配置使用yarnrc文件的内容,适应PNPM的附加参数设置 this.createOverwriteFileContentFunc([".npmrc"], `${this.getBuildInConfigDirSubFileContent("patch-npmrc")}`, false), // 写入关键信息到 package.json 中, 并格式化 package.json this.createInjectPackageInfoFunc(["package.json"], { "engines": { "node": "^14.18 || ^16.16 || ^17.8 || ^18.6", "npm": "Please use Yarn or pnPm to manage dependencies !!!", // 强制npm 使用yarn,或者 pnpm "pnpm": ">=7", // 限制pnpm 的最低版本 "yarn": ">=1.22.19" // 限制yarn 的最低版本 }, }) ], // 用途:更新 package.json, 修复一些依赖问题; 由于 Vue2.7 暂时不稳定,强制项目使用2.6.14相关的设置 ...[ this.createUpdatePackageInfoFunc(["package.json"], [ { op: "has-unset", path:["dependencies", "vue"] }, { op: "has-unset", path:["dependencies", "vuex"] }, { op: "has-unset", path:["dependencies", "vue-loader"] }, { op: "has-unset", path:["dependencies", "vue-server-renderer"] }, { op: "has-unset", path:["dependencies", "vue-template-compiler"] }, { op: "force-set", path:["dependencies", "vue"], value: "2.6.14" }, { op: "force-set", path:["dependencies", "vuex"], value: "3.6.2" }, { op: "force-set", path:["devDependencies", "vue-loader"], value: "15.9.7" }, { op: "force-set", path:["devDependencies", "vue-server-renderer"], value: "2.6.14" }, { op: "force-set", path:["devDependencies", "vue-template-compiler"], value: "2.6.14" }, { op: "force-set", path:["resolutions", "vue"], value: "2.6.14" }, { op: "force-set", path:["resolutions", "vuex"], value: "3.6.2" }, { op: "force-set", path:["resolutions", "vue-loader"], value: "15.9.7" }, { op: "force-set", path:["resolutions", "vue-server-renderer"], value: "2.6.14" }, { op: "force-set", path:["resolutions", "vue-template-compiler"], value: "2.6.14" } ]) ], // 用途: 清除不必要的处理逻辑 ...[ this.createDeleteJsonFileItemFunc(["package.json"], [["husky"], ["lint-staged"]]) ], // 用途: 备份package.json 到内存中 // 用途: 针对bin注入cli自己的module内容 ...[ this.createInjectModulesForFileFunc([ "eslint", "stylelint", "jest", "lessc", "ls-lint", "nuxt", "nuxt-ts", "stylus", "vite", "webpack" ].map(item => `.bin/${item}`)), this.createInjectModulesForFileFunc([ "stylelint", "eslint" ]) ], ]).forEach((func, index, array) => { try { func && func(); } catch (err) { this.logger.warn(`Action [${array.length}:${index}] run error:`); this.logger.error(err); } }); this.logger.success(`${actionName} perform success ...`); } catch (err) { this.logger.error(err); throw err; } } } module.exports = { /** * 检查必备的工程文件 * @param {Object|console} refLogger */ checkProjectConfigAndSomeFiles(workdir = process.cwd(), refLogger = console, prefixName = "") { const helper = new Helper(workdir, refLogger, prefixName); helper.runCheck(); }, /** * 创建一个Helper * @param workdir * @param refLogger * @param prefixName * @returns {Helper} */ createHelper(workdir = process.cwd(), refLogger = console, prefixName = "") { return new Helper(workdir, refLogger, prefixName); } };