UNPKG

@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
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);