@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
JavaScript
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, `
${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);
}
};