【前端工程化】自动化篇-手写脚本一键自动tag、发包、引导登录npm

1,278 阅读11分钟

🧑‍💻 写在开头

点赞 + 收藏 === 学会🤣🤣🤣

上一篇前端怎么可以不会GitHub Action一键部署? 我们学习了如何利用GitHub Action自动发布我们的组件库文档、使用Github Action做一些实操。本篇我们将手写一个脚本实现一键更新npm版本、Git缓存更改、自动标记tag、自动发布到npm。

🥑 你能学到什么?

希望在你阅读本篇文章之后,不会觉得浪费了时间。如果你跟着读下来,你将会学到:

  • 一些node的内置库的使用,如child_processfspath
  • inquirer库的基本使用
  • npm的工程化管理
  • gittag标记
  • npm包发布流程

实现效果

飞书20240509-214720.gif

🍎 系列文章

本系列是一个从0到1的实现过程,如果您有耐心跟着实现,您可以实现一个完整的react18 + ts5 + webpack5 + 代码质量&代码风格检测&自动修复 + storybook8 + rollup + git action实现的一个完整的组件库模板项目。如果您不打算自己配置,也可以直接clone组件库仓库切换到rollup_comp分支即是完整的项目,当前实现已经足够个人使用,后续我们会新增webpack5优化、按需加载组件、实现一些常见的组件封装:包括但不限于拖拽排序、瀑布流、穿梭框、弹窗等

🍑 一、新增脚本文件&命令&核心流程

在项目的src/scripts目录下新增文件,增加release.js文件,文件内容如下

/**
 * 主函数入口
 */
async function main() {
	try {
		
	} catch (error) {
		console.error('❌ 发生错误:', error);
	}
}

main();

在项目的package.json中增加如下命令,后续我们使用npm run release一键更新npm版本、Git缓存更改、自动标记tag、自动发布到npm。

"release": "node ./scripts/release.js"

核心流程伪代码如下:

let newVersion = getNewVersion();
// 更新版本号
updateVersion(newVersion);
// git增加tag并提交
gitOperations(newVersion);
// 设置npm源
const oldRegistryUrl = await setNpmRegistry();
// 检测是否已经登录npm
await ensureNpmLoggedIn();
// 发布到npm
await publishToNpm();
// 恢复npm源
await restoreNpmRegistry(oldRegistryUrl);

🥑 二、获取当前已经发布的最新的的版本号

npm show ${packageName} version 是一个用于检索指定 npm 包的最新版本信息的命令。通过运行这个命令,可以查看指定包的最新版本号。

  • ${packageName}:这里是指要查找的 npm 包的名称。

举个例子,如果要查看包名为inquirer的最新版本号

npm show inquirer version

运行以上命令后,npm 将返回inquirer包的最新版本号

image.png

实现函数如下

const exec = util.promisify(require('child_process').exec);
/**
 * 获取最新版本号
 * @returns Promise
 */
async function getLatestVersion() {
	try {
		const {stdout} = await exec(`npm show ${packageName} version`);
		const latestVersion = stdout.trim().replace(/^v/, ''); // 删除可能存在的前导 v
		return latestVersion;
	} catch (error) {
		console.error(`❌ 获取最新版本失败: ${error.message}`);
		throw error; // 抛出错误,以便可以在调用此函数的地方捕获并处理
	}
}

解释下

const exec = util.promisify(require('child_process').exec);

这行代码的作用是将 Node.js 中的回调风格的函数转换为 Promise 风格的函数,使得可以更方便地处理异步操作。

具体解释如下:

  1. require('child_process').exec:这里是通过 require 函数引入 node 核心模块 child_process 中的 exec 方法,exec 方法用于在子进程中执行 shell 命令。
  2. util.promisify:这是 node 中的 util 模块提供的方法,用于将一个遵循回调函数规则的方法转换成返回 Promise 的函数。这里将 child_process.exec 方法转换为 Promise 风格的方法。
  3. const exec = util.promisify(require('child_process').exec);:将经过 util.promisify 处理后的 child_process.exec 方法赋值给一个新的常量 exec,从而创建了一个返回 Promise 的新函数 exec

我们更改主函数如下:

/**
 * 主函数入口
 */
async function main() {
	try {
		const latestVersion = await getLatestVersion();
	} catch (error) {
		console.error('❌ 发生错误:', error);
	}
}

main();

🍒 三、实现显示选择界面

1.inquirer的基本使用

这里使用的是inquirer库, Inquirer 用于创建交互式命令行界面。它提供了丰富的方式来与用户进行交互,例如提问问题、接收用户输入并根据用户的响应执行相应的操作。

以下是使用 Inquirer 的基本步骤:

  1. 安装 Inquirer:首先需要在项目中安装 Inquirer 模块。可以使用以下命令通过 npm 进行安装:

    npm install inquirer
    
  2. 引入 Inquirer 模块:在项目中引入 Inquirer 模块,以便在代码中使用它:

    const inquirer = require('inquirer');
    
  3. 创建问题数组:通过创建一系列问题的数组来定义需要向用户提出的问题。每个问题可以包括类型、消息、选择项等属性。

    const questions = [
      {
        type: 'input',
        name: 'username',
        message: '请输入您的用户名:'
      },
      {
        type: 'list',
        name: 'color',
        message: '请选择您喜欢的颜色:',
        choices: ['红色', '蓝色', '绿色']
      }
    ];
    
  4. 使用 Inquirer 提出问题:调用 inquirer.prompt() 方法并传入问题数组来开始与用户交互。用户将依次回答每个问题。

    inquirer.prompt(questions)
      .then(answers => {
        console.log('您输入的用户名是:', answers.username);
        console.log('您选择的颜色是:', answers.color);
      })
      .catch(error => {
        console.error('交互过程中出错:', error);
      });
    

2.实现我们需要的界面

之前我们已经介绍过了npm的工程管理命令,这里函数的逻辑就根据选择的版本类型,根据上面获取到的当前最新版包计算出新发布的版本信息。

image.png

代码如下:

/**
 * 命令行显示逻辑
 * @param {*} latestVersion
 */
async function displayOptions(latestVersion) {
	console.log('✅ 发包脚本启动【自动更新版本号、自动发布到npm】');
	console.log('!!! 使用前请确保仓库内已经是可发布状态');
	const currentVersion = parseVersion(latestVersion);
	const choices = [
		{
			name: `Major【大版本】 (${parseInt(currentVersion.major) + 1}.0.0)`,
			value: 'major',
		},
		{
			name: `Minor【小版本】 (${currentVersion.major}.${parseInt(currentVersion.minor) + 1}.0)`,
			value: 'minor',
		},
		{
			name: `Patch【修订版本】 (${currentVersion.major}.${currentVersion.minor}.${parseInt(currentVersion.patch) + 1})`,
			value: 'patch',
		},
		{name: `Prepatch【预发修订版本】`, value: 'prepatch'},
		{name: `Preminor【预发小版本】`, value: 'preminor'},
		{name: `Premajor【预发大版本】`, value: 'premajor'},
		{name: `Prerelease【预发版】`, value: 'prerelease'},
		{name: `Specific version【指定版本】`, value: 'specific'},
	];

	inquirer
		.prompt([
			{
				type: 'list',
				name: 'releaseType',
				message: '请选择版本号的更新类型:',
				choices: choices,
			},
			{
				type: 'input',
				name: 'specificVersion',
				message: '输入具体的版本号:',
				when: (answers) => answers.releaseType === 'specific',
				validate: (input) =>
					/\d+\.\d+\.\d+(-\w+\.\d+)?/.test(input) ||
					'版本号必须符合语义化版本控制规范。',
			},
		])
		.then(async (answers) => {
                    // TODO:选择后的处理逻辑
		});
}

🥝 四、实现将新版本号写入package.json

这里我们区分正常版本发布和预发布两种

let newVersion = '';
// 指定版本号,自己指定的版本号
if (answers.releaseType === 'specific') {
        newVersion = answers.specificVersion;
} else if (['major', 'minor', 'patch'].includes(answers.releaseType)) {
        // 非预发版本,上面计算出来的值
        currentVersion[answers.releaseType]++;
        newVersion = `${currentVersion.major}.${currentVersion.minor}.${currentVersion.patch}`;
} else {
        // 预发布版本,也是上面的计算逻辑,这里我们提出来了
        newVersion = getPreReleaseVersion(currentVersion, answers.releaseType);
}
// 更新版本号,写入到`package.json`中
updateVersion(newVersion);

预发布版本计算,个人开发一般也不用这种,但是我们还是实现一下

/**
 * 检测是否是预发布版本
 * @param {*} version
 */
function isPreRelease(version) {
	return /-/.test(version);
}

/**
 * 获取预发布版本号
 * @param {*} currentVersion
 * @param {*} type
 */
function getPreReleaseVersion(currentVersion, type) {
    let {major, minor, patch, preReleaseLabel, preReleaseVersion} =
            currentVersion;
    switch (type) {
        case 'prepatch':
                patch += 1;
                return `${major}.${minor}.${patch}-0`;
        case 'preminor':
                minor += 1;
                return `${major}.${minor}.0-0`;
        case 'premajor':
                major += 1;
                return `${major}.0.0-0`;
        case 'prerelease':
                if (isPreRelease(`${major}.${minor}.${patch}`)) {
                        preReleaseVersion = preReleaseVersion || 0;
                        return `${major}.${minor}.${patch}-${preReleaseLabel || 'beta'}.${preReleaseVersion + 1}`;
                } else {
                        return `${major}.${minor}.${patch}-beta.0`;
                }
        default:
                throw new Error(`❌ 不支持的预发布版本类型: ${type}`);
    }
}

写入到package.json

/**
 * 更新版本号
 * @param {*} newVersion
 */
function updateVersion(newVersion) {
	packageJson.version = newVersion;
	fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
	console.log(`✅ 版本号已更新为 ${newVersion}`);
}

🍐 五、实现Git标记tag并推送远端

这里使用的是当前分支,需要当前分支是否关联了远端分支,如果未关联,我们给他关联上,关联完成后我们给他标记tag并推送到远端。

/**
 * 标记tag
 * @param {*} newVersion
 */
function gitOperations(newVersion) {
	try {
		process.chdir(projectRootPath); // Change the current working directory to project root

		// 获取当前分支名称
		const branchName = execSync('git rev-parse --abbrev-ref HEAD')
			.toString()
			.trim();

		// 检查是否有设置 upstream(远程跟踪分支)
		let setUpstream = false;
		try {
			execSync(`git rev-parse --abbrev-ref --symbolic-full-name @{u}`);
		} catch (error) {
			// 如果没有设置 upstream,为远程的同名分支设置 upstream
			const remoteBranchExists = execSync(
				`git ls-remote --heads origin ${branchName}`,
			)
				.toString()
				.trim();
			if (remoteBranchExists) {
				execSync(`git branch --set-upstream-to=origin/${branchName}`);
			} else {
				console.error(
					`❌ 远程分支 'origin/${branchName}' 不存在,无法设置 upstream。`,
				);
				return;
			}
			setUpstream = true;
		}

		execSync(`git add .`, {stdio: 'inherit'});
		execSync(`git commit -m "chore(release): ${newVersion}"`, {
			stdio: 'inherit',
		});
		execSync(`git tag v${newVersion}`, {stdio: 'inherit'});

		// 推送改动到远程分支
		execSync(`git push`, {stdio: 'inherit'});
		if (setUpstream) {
			// 如果之前没有 upstream,并且我们为其设置了 upstream,现在也推送它
			execSync(`git push --set-upstream origin ${branchName}`, {
				stdio: 'inherit',
			});
		}
		// 推送tag到远程
		execSync(`git push origin v${newVersion}`, {stdio: 'inherit'});

		console.log(`✅ Git tag v${newVersion} 已标记`);
	} catch (error) {
		console.error(`❌ Git 操作失败: ${error.message}`);
	}
}

🍉 六、实现设置npm

处理这个是因为我们可能使用nrm更改过npm的源,或者使用.npmrc,时候如果不更换会无法登录或者脚本执行流程正确,比如改成淘宝的之类,源不对没法发布,我们先记录当前npm源,检测是否已经登录npm、未登录提示登录、发布包、然后恢复npm

// 设置npm源
const oldRegistryUrl = await setNpmRegistry();
// 检测是否已经登录npm
await ensureNpmLoggedIn();
// 发布到npm
await publishToNpm();
// 恢复npm源
await restoreNpmRegistry(oldRegistryUrl);

1.保存旧的npm源并更新

主要就是先npm config get registry,然后在npm config set registry,如果是私域的npm,你可以更改这里的url,或者直接扩展这个脚本,改成询问的方式,使用nrm切换

/**
 * 设置npm的registry到指定的URL,并返回旧的registry
 * @returns {Promise<string>} 当成功时返回旧的registry URL
 */
async function setNpmRegistry() {
    try {
            const {stdout: getRegistryStdout} = await exec(`npm config get registry`);
            const oldNpmRegistry = getRegistryStdout.trim();

            const NPM_REGISTRY_URL = 'https://registry.npmjs.org/';
            await exec(`npm config set registry ${NPM_REGISTRY_URL}`);

            console.log(`✅ npm registry已设置为: ${NPM_REGISTRY_URL}`);
            return oldNpmRegistry; // 返回旧的registry,以便后续可以恢复
    } catch (error) {
            if (error.stdout) {
                    console.error(`❌ 设置npm registry stdout输出流: ${error.stdout}`);
            }
            if (error.stderr) {
                    console.error(`❌ 设置npm registry stderr出错: ${error.stderr}`);
            }
            console.error(`❌ 设置npm registry中发生错误: ${error.message}`);
            throw error; // 抛出错误以供调用者处理
    }
}

2.检测是否已经登录npm

之前的文章中我们已经介绍过,未登录npm是无法发包的,但是自动化的脚本对于这种敏感信息引导用户手动登录比较安全,这里我们通过npm whoami命令判断用户是否登录,如果未登录,引导用户登录

/**
 * 确保用户已登录npm
 * @returns {Promise<string>} 返回已登录用户的名称或抛出错误
 */
async function ensureNpmLoggedIn() {
        try {
                const {stdout} = await exec('npm whoami');
                console.log(`✅ 检测到您已作为${stdout.trim()}登录到npm`);
                return stdout.trim();
        } catch (error) {
                console.error('❌ 您似乎还没有登录到npm。请登录后继续。');
                const answers = await inquirer.prompt([
                        {
                                type: 'input',
                                name: 'username',
                                message: '请输入您的npm用户名:',
                        },
                        {
                                type: 'password',
                                name: 'password',
                                message: '请输入您的npm密码:',
                        },
                        {
                                type: 'input',
                                name: 'email',
                                message: '请输入您的npm邮箱地址:',
                        },
                ]);

                // 以下操作依赖于能够自动化的输入命令到npm login(在这个假设下编写)
                // 实际操作中这可能需要特殊处理,例如通过node-pty实现自动输入
                const {stdout: loginStdout} = await exec(
                        `echo "${answers.username}\n${answers.password}\n${answers.email}\n" | npm login`,
                );
                console.log('✅ 登录输出流loginStdout', loginStdout);
                return answers.username;
        }
}

3.发布到npm

使用npm publish命令发布。

/**
 * 异步地发布到npm
 * @returns {Promise<void>}
 */
async function publishToNpm() {
	console.log('🚀🚀🚀 正在发布到 npm...');

	try {
		// 这里可以添加对newVersion的使用,例如修改package.json中的版本号
		// 如果newVersion参数确实需要被用于发布特定版本,这里应该加入对版本号处理的逻辑
		// 例如使用 npm version 命令来更新项目的版本号

		const {stdout, stderr} = await exec('npm publish');

		if (stderr) {
			console.log(`✅ 发布输出流stderr: ${stderr}`);
		}

		console.log(`🎉🎉🎉 npm包发布成功: ${stdout}`);
	} catch (error) {
		console.error(`❌ 发布失败: ${error.message}`);
		throw error; // 抛出错误以供调用方处理
	}
}

4.恢复npm

根据之前保存的旧的npm源,还原

/**
 * 恢复npm的registry为旧的URL
 * @returns {Promise<void>}
 */
async function restoreNpmRegistry(oldNpmRegistry) {
    if (oldNpmRegistry) {
        try {
                await exec(`npm config set registry ${oldNpmRegistry}`);
                console.log(`✅ npm registry已恢复为: ${oldNpmRegistry}`);
        } catch (error) {
                if (error.stdout) {
                        console.error(`✅ 恢复npm registry输出流: ${error.stdout}`);
                }
                if (error.stderr) {
                        console.error(`❌ 恢复npm registry出错: ${error.stderr}`);
                }
                console.error(`❌ 恢复npm registry中发生错误: ${error.message}`);
                throw error; // 抛出错误以供调用方处理
        }
    } else {
        console.error(`❌ 未找到旧的npm registry,无法恢复。`);
        throw new Error(`❌ 未找到旧的npm registry,无法恢复。`);
    }
}

完整代码&代码仓库

组件库仓库切换到rollup_comp分支即是完整的项目,脚本已上传

const {execSync} = require('child_process');
const util = require('util');
// 改为异步:包裹返回promise

const fs = require('fs');
const path = require('path');
const inquirer = require('inquirer');
const exec = util.promisify(require('child_process').exec);

const projectRootPath = path.join(__dirname, '..');
const packageJsonPath = path.join(projectRootPath, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const packageName = packageJson.name;

/**
 * 解析版本号
 * @param {*} version
 * @returns
 */
function parseVersion(version) {
	const [semver, preReleaseTag = ''] = version.split('-');
	const [major, minor, patch] = semver.split('.').map(Number);
	const [preReleaseLabel, preReleaseVersion] = preReleaseTag.split('.');
	return {
		major,
		minor,
		patch,
		preReleaseLabel,
		preReleaseVersion: preReleaseVersion ? parseInt(preReleaseVersion, 10) : 0,
	};
}

/**
 * 检测是否是预发布版本
 * @param {*} version
 */
function isPreRelease(version) {
	return /-/.test(version);
}

/**
 * 获取预发布版本号
 * @param {*} currentVersion
 * @param {*} type
 */
function getPreReleaseVersion(currentVersion, type) {
	let {major, minor, patch, preReleaseLabel, preReleaseVersion} =
		currentVersion;
	switch (type) {
		case 'prepatch':
			patch += 1;
			return `${major}.${minor}.${patch}-0`;
		case 'preminor':
			minor += 1;
			return `${major}.${minor}.0-0`;
		case 'premajor':
			major += 1;
			return `${major}.0.0-0`;
		case 'prerelease':
			if (isPreRelease(`${major}.${minor}.${patch}`)) {
				preReleaseVersion = preReleaseVersion || 0;
				return `${major}.${minor}.${patch}-${preReleaseLabel || 'beta'}.${preReleaseVersion + 1}`;
			} else {
				return `${major}.${minor}.${patch}-beta.0`;
			}
		default:
			throw new Error(`❌ 不支持的预发布版本类型: ${type}`);
	}
}

/**
 * 获取最新版本号
 * @returns Promise
 */
async function getLatestVersion() {
	try {
		const {stdout} = await exec(`npm show ${packageName} version`);
		const latestVersion = stdout.trim().replace(/^v/, ''); // 删除可能存在的前导 v
		return latestVersion;
	} catch (error) {
		console.error(`❌ 获取最新版本失败: ${error.message}`);
		throw error; // 抛出错误,以便可以在调用此函数的地方捕获并处理
	}
}

/**
 * 更新版本号
 * @param {*} newVersion
 */
function updateVersion(newVersion) {
	packageJson.version = newVersion;
	fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
	console.log(`✅ 版本号已更新为 ${newVersion}`);
}

/**
 * 确保用户已登录npm
 * @returns {Promise<string>} 返回已登录用户的名称或抛出错误
 */
async function ensureNpmLoggedIn() {
	try {
		const {stdout} = await exec('npm whoami');
		console.log(`✅ 检测到您已作为${stdout.trim()}登录到npm`);
		return stdout.trim();
	} catch (error) {
		console.error('❌ 您似乎还没有登录到npm。请登录后继续。');
		const answers = await inquirer.prompt([
			{
				type: 'input',
				name: 'username',
				message: '请输入您的npm用户名:',
			},
			{
				type: 'password',
				name: 'password',
				message: '请输入您的npm密码:',
			},
			{
				type: 'input',
				name: 'email',
				message: '请输入您的npm邮箱地址:',
			},
		]);

		// 以下操作依赖于能够自动化的输入命令到npm login(在这个假设下编写)
		// 实际操作中这可能需要特殊处理,例如通过node-pty实现自动输入
		const {stdout: loginStdout} = await exec(
			`echo "${answers.username}\n${answers.password}\n${answers.email}\n" | npm login`,
		);
		console.log('✅ 登录输出流loginStdout', loginStdout);
		return answers.username;
	}
}

/**
 * 异步地发布到npm
 * @returns {Promise<void>}
 */
async function publishToNpm() {
	console.log('🚀🚀🚀 正在发布到 npm...');

	try {
		// 这里可以添加对newVersion的使用,例如修改package.json中的版本号
		// 如果newVersion参数确实需要被用于发布特定版本,这里应该加入对版本号处理的逻辑
		// 例如使用 npm version 命令来更新项目的版本号

		const {stdout, stderr} = await exec('npm publish');

		if (stderr) {
			console.log(`✅ 发布输出流stderr: ${stderr}`);
		}

		console.log(`🎉🎉🎉 npm包发布成功: ${stdout}`);
	} catch (error) {
		console.error(`❌ 发布失败: ${error.message}`);
		throw error; // 抛出错误以供调用方处理
	}
}

/**
 * 标记tag
 * @param {*} newVersion
 */
function gitOperations(newVersion) {
	try {
		process.chdir(projectRootPath); // Change the current working directory to project root

		// 获取当前分支名称
		const branchName = execSync('git rev-parse --abbrev-ref HEAD')
			.toString()
			.trim();

		// 检查是否有设置 upstream(远程跟踪分支)
		let setUpstream = false;
		try {
			execSync(`git rev-parse --abbrev-ref --symbolic-full-name @{u}`);
		} catch (error) {
			// 如果没有设置 upstream,为远程的同名分支设置 upstream
			const remoteBranchExists = execSync(
				`git ls-remote --heads origin ${branchName}`,
			)
				.toString()
				.trim();
			if (remoteBranchExists) {
				execSync(`git branch --set-upstream-to=origin/${branchName}`);
			} else {
				console.error(
					`❌ 远程分支 'origin/${branchName}' 不存在,无法设置 upstream。`,
				);
				return;
			}
			setUpstream = true;
		}

		execSync(`git add .`, {stdio: 'inherit'});
		execSync(`git commit -m "chore(release): ${newVersion}"`, {
			stdio: 'inherit',
		});
		execSync(`git tag v${newVersion}`, {stdio: 'inherit'});

		// 推送改动到远程分支
		execSync(`git push`, {stdio: 'inherit'});
		if (setUpstream) {
			// 如果之前没有 upstream,并且我们为其设置了 upstream,现在也推送它
			execSync(`git push --set-upstream origin ${branchName}`, {
				stdio: 'inherit',
			});
		}
		// 推送tag到远程
		execSync(`git push origin v${newVersion}`, {stdio: 'inherit'});

		console.log(`✅ Git tag v${newVersion} 已标记`);
	} catch (error) {
		console.error(`❌ Git 操作失败: ${error.message}`);
	}
}

/**
 * 设置npm的registry到指定的URL,并返回旧的registry
 * @returns {Promise<string>} 当成功时返回旧的registry URL
 */
async function setNpmRegistry() {
	try {
		const {stdout: getRegistryStdout} = await exec(`npm config get registry`);
		const oldNpmRegistry = getRegistryStdout.trim();

		const NPM_REGISTRY_URL = 'https://registry.npmjs.org/';
		await exec(`npm config set registry ${NPM_REGISTRY_URL}`);

		console.log(`✅ npm registry已设置为: ${NPM_REGISTRY_URL}`);
		return oldNpmRegistry; // 返回旧的registry,以便后续可以恢复
	} catch (error) {
		if (error.stdout) {
			console.error(`❌ 设置npm registry stdout输出流: ${error.stdout}`);
		}
		if (error.stderr) {
			console.error(`❌ 设置npm registry stderr出错: ${error.stderr}`);
		}
		console.error(`❌ 设置npm registry中发生错误: ${error.message}`);
		throw error; // 抛出错误以供调用者处理
	}
}

/**
 * 恢复npm的registry为旧的URL
 * @returns {Promise<void>}
 */
async function restoreNpmRegistry(oldNpmRegistry) {
	if (oldNpmRegistry) {
		try {
			await exec(`npm config set registry ${oldNpmRegistry}`);
			console.log(`✅ npm registry已恢复为: ${oldNpmRegistry}`);
		} catch (error) {
			if (error.stdout) {
				console.error(`✅ 恢复npm registry输出流: ${error.stdout}`);
			}
			if (error.stderr) {
				console.error(`❌ 恢复npm registry出错: ${error.stderr}`);
			}
			console.error(`❌ 恢复npm registry中发生错误: ${error.message}`);
			throw error; // 抛出错误以供调用方处理
		}
	} else {
		console.error(`❌ 未找到旧的npm registry,无法恢复。`);
		throw new Error(`❌ 未找到旧的npm registry,无法恢复。`);
	}
}

/**
 * 命令行显示逻辑
 * @param {*} latestVersion
 */
async function displayOptions(latestVersion) {
	console.log('✅ 发包脚本启动【自动更新版本号、自动发布到npm】');
	console.log('!!! 使用前请确保仓库内已经是可发布状态');
	const currentVersion = parseVersion(latestVersion);
	const choices = [
		{
			name: `Major【大版本】 (${parseInt(currentVersion.major) + 1}.0.0)`,
			value: 'major',
		},
		{
			name: `Minor【小版本】 (${currentVersion.major}.${parseInt(currentVersion.minor) + 1}.0)`,
			value: 'minor',
		},
		{
			name: `Patch【修订版本】 (${currentVersion.major}.${currentVersion.minor}.${parseInt(currentVersion.patch) + 1})`,
			value: 'patch',
		},
		{name: `Prepatch【预发修订版本】`, value: 'prepatch'},
		{name: `Preminor【预发小版本】`, value: 'preminor'},
		{name: `Premajor【预发大版本】`, value: 'premajor'},
		{name: `Prerelease【预发版】`, value: 'prerelease'},
		{name: `Specific version【指定版本】`, value: 'specific'},
	];

	inquirer
		.prompt([
			{
				type: 'list',
				name: 'releaseType',
				message: '请选择版本号的更新类型:',
				choices: choices,
			},
			{
				type: 'input',
				name: 'specificVersion',
				message: '输入具体的版本号:',
				when: (answers) => answers.releaseType === 'specific',
				validate: (input) =>
					/\d+\.\d+\.\d+(-\w+\.\d+)?/.test(input) ||
					'版本号必须符合语义化版本控制规范。',
			},
		])
		.then(async (answers) => {
			let newVersion = '';
			// 指定版本号
			if (answers.releaseType === 'specific') {
				newVersion = answers.specificVersion;
			} else if (['major', 'minor', 'patch'].includes(answers.releaseType)) {
				// 非预发版本
				currentVersion[answers.releaseType]++;
				newVersion = `${currentVersion.major}.${currentVersion.minor}.${currentVersion.patch}`;
			} else {
				// 预发布版本
				newVersion = getPreReleaseVersion(currentVersion, answers.releaseType);
			}
			// 更新版本号
			updateVersion(newVersion);
			// git增加tag并提交
			gitOperations(newVersion);
			// 设置npm源
			const oldRegistryUrl = await setNpmRegistry();
			// 检测是否已经登录npm
			await ensureNpmLoggedIn();
			// 发布到npm
			await publishToNpm();
			// 恢复npm源
			await restoreNpmRegistry(oldRegistryUrl);
		});
}

/**
 * 主函数入口
 */
async function main() {
	try {
		const latestVersion = await getLatestVersion();
		await displayOptions(latestVersion);
	} catch (error) {
		console.error('❌ 发生错误:', error);
	}
}

main();

🍎 推荐阅读

面试手写系列

react实现原理系列

其他

🍋 写在最后

如果您看到这里了,并且觉得这篇文章对您有所帮助,希望您能够点赞👍和收藏⭐支持一下作者🙇🙇🙇,感谢🍺🍺!如果文中有任何不准确之处,也欢迎您指正,共同进步。感谢您的阅读,期待您的点赞👍和收藏⭐!

感兴趣的同学可以关注下我的公众号ObjectX前端实验室

🌟 少走弯路 | ObjectX前端实验室 🛠️「精选资源|实战经验|技术洞见」