@varlinor/cli
Version:
This package provides a set of command-line tools for managing and building front-end projects. These tools include functionalities for cleaning the node_modules directory, creating and managing project versions, removing Git tags, and generating Vue comp
547 lines (537 loc) • 21.2 kB
JavaScript
import { program } from 'commander';
import path from 'path';
import { upperFirst, camelCase, lowerCase } from 'lodash-es';
import { selectSfc, normalizePath } from '@varlinor/node-tools';
import fs from 'fs-extra';
import simpleGit from 'simple-git';
import os from 'os';
import YAML from 'yaml';
import shell from 'shelljs';
import semver from 'semver';
function cleanNodeModules(baseDir, moduleName) {
if (baseDir && moduleName) {
const pkgPath = path.join(baseDir, moduleName, "node_modules");
fs.removeSync(pkgPath);
console.log(`clean '${moduleName}/node_modules' successfully!`);
}
}
async function deleteTags(basePath, keyword = "snapshot", clearRemote = false, remoteName = "origin") {
const git = simpleGit(basePath);
try {
const tags = await git.tags();
const targetTags = tags.all.filter((tag) => tag.includes(keyword));
console.log("ready to clear tags:", targetTags);
if (targetTags.length > 0) {
await git.tag(["-d", ...targetTags]);
console.log("delete tags:[%s] successfully!", targetTags.join(","));
if (clearRemote) {
const delRemoteTags = [];
for (const tagName of targetTags) {
delRemoteTags.push(`:refs/tags/${tagName}`);
}
await git.push(remoteName, ...delRemoteTags);
console.log(`delete remote tags:${delRemoteTags.join(",")}`);
}
}
} catch (error) {
console.log(error);
}
}
function checkPrepare(baseDir) {
console.log("checking base directory...");
const pkgPath = path.join(baseDir, "package.json");
const ignore = path.join(baseDir, ".gitignore");
return fs.existsSync(pkgPath) && fs.existsSync(ignore);
}
function listAllPackageFiles(packageRoot) {
const pkgFiles = [];
if (fs.existsSync(packageRoot)) {
const subPkgs = fs.readdirSync(packageRoot, { withFileTypes: true });
subPkgs.forEach((sub) => {
const subPkg = path.join(packageRoot, sub.name, "package.json");
if (sub.isDirectory() && fs.existsSync(subPkg)) {
pkgFiles.push(subPkg);
}
});
}
return pkgFiles;
}
function parsePackageInfo(baseDir) {
const wsDef = path.join(baseDir, "pnpm-workspace.yaml");
const pkgRoot = path.join(baseDir, "packages");
console.log("loading workspace define file...");
const packagePath = [];
if (fs.existsSync(wsDef)) {
const content = fs.readFileSync(wsDef, "utf-8");
const { packages: pkgDefs } = YAML.parse(content);
pkgDefs.forEach((p) => {
let packageRoot;
if (p.endsWith("/*")) {
packageRoot = path.join(baseDir, p.slice(0, -2));
} else {
packageRoot = path.join(baseDir, p);
}
const curPkgFiles = listAllPackageFiles(packageRoot);
packagePath.push(...curPkgFiles);
});
} else if (fs.existsSync(pkgRoot)) {
const pkgFiles = listAllPackageFiles(pkgRoot);
packagePath.push(...pkgFiles);
}
console.log("get all package files...");
const pkgInfos = [];
if (packagePath.length) {
let changesetIgnoreList = [];
const changesetCfgFile = path.join(baseDir, ".changeset/config.json");
if (fs.existsSync(changesetCfgFile)) {
const changesetCfg = fs.readFileSync(changesetCfgFile, { encoding: "utf-8" });
const { ignore } = JSON.parse(changesetCfg);
changesetIgnoreList = ignore;
console.log("Ignore packages:", JSON.stringify(changesetIgnoreList));
}
packagePath.forEach((pkg) => {
const jsonstr = fs.readFileSync(pkg, { encoding: "utf-8" });
try {
const json = JSON.parse(jsonstr);
const { version, name } = json;
const ignorePackage = changesetIgnoreList && changesetIgnoreList.find((p) => p == name);
if (!ignorePackage) {
pkgInfos.push({
name,
version,
packageFile: pkg
});
}
} catch (error) {
console.error(error);
}
});
}
return pkgInfos;
}
async function collectAllPackageRegistryInfo(pkgInfos) {
if (pkgInfos) {
const promiseArr = [];
pkgInfos.forEach((pkgInfo) => {
const { name, version } = pkgInfo;
promiseArr.push(
new Promise((resolve) => {
shell.exec(`npm view ${name} versions`, (code, stdout, stderr) => {
if (code == 0) {
if (typeof stdout === "string") {
let output;
const tmpStr = stdout.replace(/[\n\s]+/g, "").replace(/'/g, '"');
if (tmpStr.indexOf("[") < 0 && tmpStr.indexOf("]") < 0) {
output = tmpStr ? [tmpStr] : [];
} else
[output = JSON.parse(tmpStr)];
resolve(output);
}
} else
[resolve([])];
});
})
);
});
const allPackageInfo = await Promise.all(promiseArr).then((results) => {
const totalInfo = [];
if (results.length === pkgInfos.length) {
results.forEach((r, idx) => {
const { name, version, packageFile } = pkgInfos[idx];
const sorted = r.length ? r.sort(semver.compare) : r;
totalInfo.push({
name,
packageFile,
localVer: version,
registryVers: sorted.reverse()
});
});
}
return totalInfo;
}).catch((err) => {
console.error(err);
});
return allPackageInfo;
}
}
async function processPackageInfos(baseDir, opts) {
const { create, info, remote, output } = opts;
let snapshot = opts.snapshot;
let changesetPreCfg;
const changesetRoot = path.join(baseDir, ".changeset");
if (checkPrepare(baseDir)) {
const pkgInfos = parsePackageInfo(baseDir);
if (info) {
pkgInfos.forEach((pkg) => {
console.log("package: %s , version in project: %s", pkg.name, pkg.version);
});
return;
}
const changesetPreCfgFile = path.join(changesetRoot, "pre.json");
if (fs.existsSync(changesetPreCfgFile) && !snapshot) {
const changesetPreCfgStr = fs.readFileSync(changesetPreCfgFile, { encoding: "utf-8" });
changesetPreCfg = JSON.parse(changesetPreCfgStr);
snapshot = changesetPreCfg.tag;
console.log("release version for tag: %s ", snapshot);
}
const preparedPkgs = await collectAllPackageRegistryInfo(pkgInfos);
if (Array.isArray(preparedPkgs)) {
if (remote) {
preparedPkgs.forEach((pkg) => {
console.log(
"package: %s , version in registry: %s",
pkg.name,
JSON.stringify(pkg.registryVers)
);
});
return;
}
if (!create && !snapshot) {
return;
}
let releaseType = "patch", prerelease = "";
if (create == "major") {
releaseType = "major";
} else if (create == "minor") {
releaseType = "minor";
} else {
releaseType = "patch";
if (snapshot) {
releaseType = "prerelease";
prerelease = snapshot;
}
}
const modifyPromiseArr = [];
preparedPkgs.forEach((pkgInfo) => {
const { name, localVer, registryVers, packageFile } = pkgInfo;
modifyPromiseArr.push(
new Promise((resolve) => {
let newVer;
if (!registryVers.length) {
newVer = semver.inc(localVer, releaseType, prerelease);
} else if (registryVers.find((r) => r == localVer)) {
newVer = semver.inc(localVer, releaseType, prerelease);
} else {
let latestVer = registryVers[0];
const localSemVer = semver.parse(localVer);
const localMainVer = `${localSemVer.major}.${localSemVer.minor}.${localSemVer.patch}`;
let snapshotKey;
if (localSemVer.prerelease.length) {
const [snapshotName, ver] = localSemVer.prerelease;
snapshotKey = snapshotName;
}
for (let i = 0; i < registryVers.length; i++) {
const rVer = registryVers[i];
const rSemver = semver.parse(rVer);
const rMain = `${rSemver.major}.${rSemver.minor}.${rSemver.patch}`;
if (semver.eq(rMain, localMainVer)) {
if (rSemver.prerelease.length && rSemver.prerelease[0] == snapshotKey) {
latestVer = rVer;
} else {
latestVer = rMain;
}
const diffType = semver.diff(localSemVer, latestVer);
if (diffType == "prerelease") {
releaseType = diffType;
} else if (diffType == "prepatch") {
releaseType = diffType;
}
break;
}
}
if (semver.lte(localVer, latestVer)) {
newVer = semver.inc(latestVer, releaseType, prerelease);
} else {
newVer = semver.inc(localVer, releaseType, prerelease);
}
}
resolve({
name,
version: newVer,
packageFile
});
})
);
});
const preparePkgs = await Promise.all(modifyPromiseArr);
console.log("Ready to update 'package.json' files:");
const successedList = updatePackageFiles(preparePkgs);
console.log("check params:[output]:", output);
if (output) {
console.log("ready to modify template package.json file!");
const targetFile = path.join(baseDir, "templates/package.json");
updateTemplatePackageFile(targetFile, successedList);
}
prepareChangesetPreInfo(changesetRoot, changesetPreCfg, releaseType, successedList);
} else {
console.log("packageInfo is empty, please check!");
}
} else {
console.log("Current path [%s] is not project root path, cannot execute this command!");
}
}
function updatePackageFiles(prepareList) {
const successed = [];
if (Array.isArray(prepareList)) {
prepareList.forEach((item) => {
const { name, version, packageFile } = item;
if (fs.existsSync(packageFile)) {
const pkgStr = fs.readFileSync(packageFile, { encoding: "utf-8" });
const packageData = JSON.parse(pkgStr);
if (name == packageData.name && semver.valid(version)) {
packageData.version = version;
fs.writeFileSync(packageFile, JSON.stringify(packageData, null, 2), "utf-8");
successed.push({ name, version });
console.log("%s has been updated --> %s | %s", name, version, packageFile);
} else {
console.warn("Package is not correct or version is illegal");
}
}
});
console.log("All package file have been updated!");
}
return successed;
}
async function prepareChangesetPreInfo(changesetRoot, preCfgData, releaseType, pkgInfos) {
if (changesetRoot && preCfgData && releaseType && Array.isArray(pkgInfos)) {
const { code, stdout } = shell.exec(`pnpm changeset --empty`);
let verFileName;
if (code == 0 && typeof stdout === "string") {
const regex = /(?:\/|\\)([^\/\\]+\.md)/;
const matchStrArr = stdout.match(regex);
if (matchStrArr && matchStrArr[1]) {
verFileName = matchStrArr[1].slice(0, -3);
}
}
if (!verFileName) {
const files = fs.readdirSync(changesetRoot, { withFileTypes: true });
const seq = files.length + 1;
verFileName = `auto-${seq}-${releaseType}`;
shell.ShellString("").to(path.join(changesetRoot, `${verFileName}.md`));
}
let changesetType = releaseType;
if (releaseType == "prerelease" || releaseType == "prepatch") {
changesetType = "patch";
}
const contentArr = ["---"];
pkgInfos.forEach((pkg) => {
const { name } = pkg;
contentArr.push(`'${name}': ${changesetType}`);
});
contentArr.push("---");
contentArr.push("");
contentArr.push(`auto generate ${releaseType} version`);
const content = contentArr.join(os.EOL);
shell.ShellString(content).to(path.join(changesetRoot, `${verFileName}.md`));
preCfgData.changesets.push(verFileName);
fs.writeFileSync(
path.join(changesetRoot, "pre.json"),
JSON.stringify(preCfgData, null, 2),
"utf-8"
);
console.log("All version info has been recorded into Changeset config: pre.json");
} else {
console.error("Parameters Error!");
}
}
function updateTemplatePackageFile(targetFile, versionList, corePackageName = "@qkt/core") {
if (Array.isArray(versionList) && fs.existsSync(targetFile)) {
const versionMap = {};
let mainVer;
versionList.forEach((v) => {
versionMap[v.name] = v.version;
if (v.name == corePackageName) {
mainVer = v.version;
}
});
const replaceVersion = (pkgs) => {
for (const key in pkgs) {
if (Object.hasOwnProperty.call(pkgs, key) && versionMap.hasOwnProperty(key)) {
pkgs[key] = versionMap[key];
}
}
return pkgs;
};
const contentStr = fs.readFileSync(targetFile, { encoding: "utf-8" });
const tempDefines = JSON.parse(contentStr);
const { dependencies, devDependencies } = tempDefines;
const tmpD = replaceVersion(dependencies);
const tmpDev = replaceVersion(devDependencies);
if (mainVer) {
tempDefines.version = mainVer;
}
tempDefines.dependencies = tmpD;
tempDefines.devDependencies = tmpDev;
fs.writeFileSync(targetFile, JSON.stringify(tempDefines, null, 2), { encoding: "utf-8" });
console.log("templates/package.json has been updated to new versions!");
}
}
async function generateSfcDefineAndEntryFiles(packageRoot, isTs = false, isOutputEntry = true, prefix = "Qkt", mode = "merge") {
if (!packageRoot) {
packageRoot = process.cwd();
}
console.log("current package root dir:", packageRoot);
const defPath = path.join(packageRoot, "components.json");
let preComDefs = [];
if (fs.existsSync(defPath)) {
const comDefsInfo = fs.readFileSync(defPath, "utf-8");
if (comDefsInfo) {
try {
preComDefs = JSON.parse(comDefsInfo);
} catch (error) {
console.error("Error parsing components.json:", error);
}
}
}
const selected = await selectSfc(packageRoot, isTs);
const srcScriptSuffix = isTs ? ".ts" : ".js";
let comDefs = [];
const newDefs = [];
const entryGenArr = [];
if (Array.isArray(selected)) {
const packageName = path.basename(packageRoot);
console.log("packageName:", packageName);
for (const file of selected) {
const name = file.endsWith(srcScriptSuffix) ? path.basename(file, srcScriptSuffix) : path.basename(file, ".vue");
const filename = path.basename(file);
const parentPath = path.dirname(file);
const parentDirName = path.basename(parentPath);
const relatedParentPath = normalizePath(parentPath).replace(normalizePath(packageRoot), "");
const basedir = `packages/${packageName}${relatedParentPath}`;
let info = {
packageName,
basedir,
filename,
importPath: `./${filename}`,
outputPath: relatedParentPath
};
if ("index.vue" === filename) {
info.exportName = `${prefix}${upperFirst(camelCase(parentDirName))}`;
info.outputFileName = `index`;
} else if ("main.vue" === filename) {
info.exportName = `${prefix}${upperFirst(camelCase(parentDirName))}`;
info.outputFileName = `index`;
entryGenArr.push(info);
} else {
info.exportName = `${prefix}${upperFirst(camelCase(name))}`;
info.outputFileName = name;
entryGenArr.push(info);
}
newDefs.push(info);
}
if (isOutputEntry && !!entryGenArr.length) {
entryGenArr.forEach((item) => {
const entryFileName = item.outputFileName === "index" ? "index" : `index-${item.outputFileName}`;
const entryFilePath = path.join(packageRoot, item.outputPath, `${entryFileName}${srcScriptSuffix}`);
if (!fs.existsSync(entryFilePath)) {
const entryContent = formatEntryContent({
exportName: item.exportName,
relatePath: item.importPath
});
fs.writeFileSync(entryFilePath, entryContent);
console.log("generate entry file:" + entryFilePath);
}
});
}
comDefs = formatComponentDefines(mode, preComDefs, newDefs, {
packageRoot,
suffix: srcScriptSuffix
});
if (comDefs.length) {
fs.writeFileSync(defPath, JSON.stringify(comDefs, null, 2));
console.log(`Components Define file [${defPath}] is generated!`);
} else {
console.log("No file need to be output!");
}
}
return { defineInfos: comDefs, selectedFiles: selected };
}
function formatEntryContent(comInfo) {
return `
/* Automatically generated by '@varlinor/cli' */
import ${comInfo.exportName} from '${comInfo.relatePath}'
${comInfo.exportName}.install = function (Vue) {
Vue.component('${comInfo.exportName}', ${comInfo.exportName})
}
export default ${comInfo.exportName}
`.trim();
}
function formatComponentDefines(mode = "merge", preComDefs, compDefines, opts = { packageRoot: string = "", suffix: string = "" }) {
if (mode === "merge") {
const newComDefs = [];
const { packageRoot, suffix } = opts;
for (const preInf of preComDefs) {
const { packageName, basedir, filename, outputPath, outputFileName } = preInf;
const cur = compDefines.find((p) => {
return p.packageName === packageName && p.basedir === basedir && p.filename === filename && p.outputPath === outputPath && p.outputFileName === outputFileName;
});
if (!cur) {
const entryFileName = outputFileName === "index" ? "index" : `index-${outputFileName}`;
const entryFile = path.join(packageRoot, outputPath, `${entryFileName}${suffix}`);
if (fs.existsSync(entryFile) || filename === "index.vue") {
newComDefs.push(preInf);
} else {
console.log("%s is not exist, remove it's defineInfo", entryFile);
}
}
}
newComDefs.push(...compDefines);
return newComDefs;
} else if (mode === "replace") {
return compDefines;
} else {
return [];
}
}
const version = "1.2.0";
program.name("qkt-cli").version(version);
program.command("clean").description("used to clean something in target directory ").option(
"-c, --current-path",
"use current path to generate, if set this parameter, '-m' and '-b' can set empty, if not, '-m' and '-b' must be valued"
).option("-b, --base-dir", "target directory which has moduleName to scan").option(
"-m, --module-name <moduleName>",
"generate entry file for packages' components, based on components.json"
).action(async (cmdObj) => {
let { currentPath, moduleName, baseDir } = cmdObj;
if (currentPath) {
const curPath = process.cwd();
moduleName = path.basename(curPath);
baseDir = normalizePath(path.dirname(curPath));
} else {
if (!moduleName || !baseDir)
throw new Error("illegal parameters, please check!");
}
cleanNodeModules(baseDir, moduleName);
});
program.command("semver").description("used to create package version, must be runned in project root directory ").option(
"-c, --create <createType>",
"used to create a version for project, major | minor | patch can be specificed! "
).option("-i, --info", "used to print current version info").option("-r, --remote", "used to print remote version info").option(
"-s, --snapshot <snapshotType>",
"snapshot type can be specificed for different branch, just like:`moduleA@0.0.1-datacore-snapshot.0`"
).option("-o, --output", "replace versions to templates/package.json").action(async (cmdObj) => {
let { create, info, remote, snapshot, output } = cmdObj;
const baseDir = process.cwd();
const opts = {
create,
info,
remote,
snapshot,
output
};
console.log("prepare process version info of packages:", JSON.stringify(opts));
processPackageInfos(baseDir, opts);
});
program.command("tag").description("used to remove useless tags, must be runned in project root directory ").option("-r, --remote", "used to clear remote tags ").option("-o, --remoteName <remoteName>", "used to specify which remote will be remove tags ").option("-f, --filterCode <filterCode>", "used to filte tags which will be removed ").action(async (cmdObj) => {
let { remote, remoteName, filterCode } = cmdObj;
const baseDir = process.cwd();
deleteTags(baseDir, filterCode, remote, remoteName);
});
program.command("generate").description("used to generate files for packages ").command("define").option("-t, --isTs", "specify source is typescript ", false).option("-e, --isOutputEntry", "enable create entry file", true).option("-p, --prefix <prefix>", "set component name prefix", "Qkt").option("-f, --force", "default is merge, when set true, will replace old define", false).action(async (cmdObj) => {
let { isTs, isOutputEntry, prefix, force } = cmdObj;
const baseDir = process.cwd();
const mode = force ? "replace" : "merge";
const prefixStr = upperFirst(lowerCase(prefix));
generateSfcDefineAndEntryFiles(baseDir, isTs, isOutputEntry, prefixStr, mode);
});
program.parse(process.argv);