fnm无缝切换项目的pnpm和node脚本化实践

大家好,我是Cn。今天早上很有幸我读到了一篇文章,然后根据作者的建议,我就写了一个脚本进行自动化处理。可以进行通用化处理的一个方式,可以让掘金的小伙伴们应用到各自的项目中就可以了。

背景

来自 water大佬写的一篇掘金文章# 你需要知道的 Node 版本管理工具 fnm------一次彻底的前端工程环境升级,我觉得对于团队项目使用的pnpm和node版本管理进行统一化管理是挺有效的。为此,我想了一下处理方式,就感觉能把整个判断流程串起来,实现自动化"傻瓜式"处理,我感觉还是挺好用的。为此,我研究了一下,写了一个"类似"自动化的脚本。

一、脚本实现步骤

js 复制代码
/** 
脚本的执行顺序是:
检查并更新 package.json 中的 packageManager
检查并创建 .nvmrc 文件
检查并安装 fnm(如果未安装)
检查并更新 .zshrc 中的 fnm 配置
检查并安装 Node.js 18.17.0(如果未安装)
检查并启用 corepack
检查并更新 .gitignore 文件
*/
// 执行所有检查
function main() {
  console.log("🚀 Running post-install checks...");
  try {
    checkAndUpdatePackageManager();
    checkAndCreateNvmrc();
    checkAndInstallFnm();
    checkAndUpdateZshrc();
    checkAndInstallNodeVersion();
    checkAndEnableCorepack();
    checkAndUpdateGitignore();
    console.log("✨ All checks completed successfully!");
  } catch (error) {
    console.error("❌ Error during post-install checks:", error);
    process.exit(1);
  }
}

下面我就按照顺序的处理给大家一一解答每一步的处理方式。

前提 - 项目需要require相关模块进行处理

ini 复制代码
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const os = require("os");

第一步:检查并更新 package.json 中的 packageManager

因为在Node 16.10+内置了corepack,可以直接用其对pnpm进行管理和激活。他会根据package.jsonpackageManager字段,对此进行管理和激活,为此第一步,我们就要在package.json中配置该字段

js 复制代码
// 检查并更新 package.json 中的 packageManager
function checkAndUpdatePackageManager() {
  const packageJsonPath = path.join(process.cwd(), "package.json");
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
  const { pnpm } = getVersionConfig();

  if (!packageJson.packageManager) {
    packageJson.packageManager = `pnpm@${pnpm}`;
    fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
    console.log(`✅ Added packageManager to package.json with pnpm@${pnpm}`);
  } else {
    console.log("✅ packageManager already exists in package.json");
  }
}

通过获取目录下的package.json文件,读取其内容并且解析,判断有没有已经写入了packageManager字段,如果没写入,我们就帮其填入项目想要设置和维护的pnpm版本,那这里就会通过getVersionConfig进行获取。

getVersionConfig - 获取用户设定的pnpm和node版本
js 复制代码
// 默认版本配置
const DEFAULT_VERSIONS = {
  pnpm: "8.6.12",
  node: "18.17.0",
};

// 版本配置文件路径
const VERSION_CONFIG_PATH = path.join(process.cwd(), "RN_CONFIG_VERSION.json");

// 读取或创建版本配置
function getVersionConfig() {
  try {
    let config = {};

    // 如果配置文件存在,读取它
    if (fs.existsSync(VERSION_CONFIG_PATH)) {
      config = JSON.parse(fs.readFileSync(VERSION_CONFIG_PATH, "utf8"));
    }

    // 检查并填充缺失的版本配置
    let hasChanges = false;
    if (!config.pnpm) {
      config.pnpm = DEFAULT_VERSIONS.pnpm;
      hasChanges = true;
    }
    if (!config.node) {
      config.node = DEFAULT_VERSIONS.node;
      hasChanges = true;
    }

    // 如果有更改,写入配置文件
    if (hasChanges) {
      fs.writeFileSync(VERSION_CONFIG_PATH, JSON.stringify(config, null, 2));
      console.log("✅ Updated version configuration file");
    }

    return config;
  } catch (error) {
    console.error("❌ Error handling version configuration:", error.message);
    return DEFAULT_VERSIONS;
  }
}

这里,我们是利用JSON配置化的形式进行处理,我们是会去读取目录中是否RN_CONFIG_VERSION.json这个文件,如果没有该文件,我们会帮其创建这个文件出来;如果有,就读取该文件,观察其有没有配置pnpmnode字段,如果没有,我们就会自动赋予其默认的pnpm和node版本,由于小弟的项目中用的是[email protected][email protected],所以就默认用这两个版本,更新后最终写入到RN_CONFIG_VERSION.json文件中。所以如果用户自定义自己的版本,只需要创建该文件,然后配置项目自定义的脚本,再执行脚本,即可实现。

第二步:检查并创建 .nvmrc 文件

js 复制代码
function checkAndCreateNvmrc() {
  const nvmrcPath = path.join(process.cwd(), ".nvmrc");
  const { node } = getVersionConfig();

  if (!fs.existsSync(nvmrcPath)) {
    fs.writeFileSync(nvmrcPath, node);
    console.log(`✅ Created .nvmrc file with Node version ${node}`);
  } else {
    console.log("✅ .nvmrc file already exists");
  }
}

这里就是利用fnm的特性,通过定义.nvmrc文件,指定对应的node版本,进行维护,这个函数的实现也比较简单,就是判断一下项目目录有没有.nvmrc文件,如果有,就不需要处理;如果没有,就创建该文件,并且写入对应的node版本

第三步:检查并安装fnm

js 复制代码
// 检查命令是否已安装
function isCommandInstalled(command) {
  try {
    execSync(`which ${command}`, { stdio: "ignore" });
    return true;
  } catch (error) {
    return false;
  }
}

// 检查并安装 fnm
function checkAndInstallFnm() {
  try {
    if (!isCommandInstalled("fnm")) {
      console.log("📦 Installing fnm...");
      // 检测系统架构
      const arch = process.arch;
      if (arch === 'arm64') {
        // ARM 架构 (M1/M2/M3) 使用 curl 安装
        execSync('curl -fsSL https://fnm.vercel.app/install | bash', { stdio: 'inherit' });
      } else {
        // x86 架构使用 brew 安装
        execSync("brew install fnm", { stdio: "inherit" });
      }
      console.log("✅ Fnm installed successfully");
    } else {
      console.log("✅ Fnm is already installed");
    }
  } catch (error) {
    console.error("❌ Error with fnm:", error.message);
    console.log(
      "⚠️  Skipping fnm setup. You may need to manually install fnm."
    );
    console.log("   For ARM Mac (M1/M2/M3), run:");
    console.log("   curl -fsSL https://fnm.vercel.app/install | bash");
    console.log("   For Intel Mac, run:");
    console.log("   brew install fnm");
  }
}

这里的操作,主要是检查当前电脑中是否安装fnm,如果没有安装,我们通过Homebrew进行安装,因为原本大佬提供的安装方式是curl -fsSL https://fnm.vercel.app/install | bash进行安装,但这个需要🪜方式进行处理才能安装成功,所以我就退而求其次选择了Homebrew

第四步:检查并更新~/.zshrc中关于fnm的配置

js 复制代码
// 检查并更新 .zshrc 中的 fnm 配置
function checkAndUpdateZshrc() {
  try {
    const zshrcPath = path.join(os.homedir(), ".zshrc");
    const fnmConfig = 'eval "$(fnm env --use-on-cd)"';

    // 检查文件是否存在,如果不存在则创建
    if (!fs.existsSync(zshrcPath)) {
      fs.writeFileSync(zshrcPath, fnmConfig + "\n");
      console.log("✅ Created .zshrc and added fnm configuration");
      return;
    }

    // 读取文件内容
    const content = fs.readFileSync(zshrcPath, "utf8");

    // 检查是否已包含 fnm 配置
    if (content.includes(fnmConfig)) {
      console.log("✅ Fnm configuration already exists in .zshrc");
    } else {
      // 在文件末尾添加配置
      fs.appendFileSync(zshrcPath, "\n" + fnmConfig + "\n");
      console.log("✅ Added fnm configuration to .zshrc");
    }
  } catch (error) {
    console.error("❌ Error updating .zshrc:", error.message);
    console.log(
      "⚠️  Skipping .zshrc update. You may need to manually add fnm configuration."
    );
    console.log("   Add this line to your ~/.zshrc:");
    console.log('   eval "$(fnm env --use-on-cd)"');
  }
}

这里主要操作是配置环境变量,首先,我们会对.zshrc文件进行判断是否存在,不存在的话,我们帮其创建该文件并且写入fnm的环境变量配置;如果存在的话,我们判断内容中是否已经配置了fnm的环境变量,如果配置了就不需要处理,如果没有,我们就会在文件末尾追加该配置。

第五步:利用fnm检查并安装 Node.js 版本

js 复制代码
// 检查并安装 Node.js 版本
function checkAndInstallNodeVersion() {
  try {
    const { node } = getVersionConfig();

    // 获取已安装的 Node.js 版本列表
    const installedVersions = execSync("fnm list", { encoding: "utf8" });

    // 检查是否已安装指定版本
    if (!installedVersions.includes(node)) {
      console.log(`📦 Installing Node.js ${node}...`);
      execSync(`fnm install ${node}`, { stdio: "inherit" });
      console.log(`✅ Node.js ${node} installed successfully`);
    } else {
      console.log(`✅ Node.js ${node} is already installed`);
    }
  } catch (error) {
    console.error("❌ Error installing Node.js version:", error.message);
    console.log(
      "⚠️  Skipping Node.js installation. You may need to manually install it."
    );
    console.log("   You can do this by running:");
    console.log("   fnm install <version>");
  }
}

这里首先会检查用户自定义的node版本是否用fnm已经安装了,如果没有,就会通过fnm进行install;如果安装了,就不需要处理

第六步:检查并启用corepack

js 复制代码
// 检查并启用 corepack
function checkAndEnableCorepack() {
  try {
    // 检查 corepack 是否已安装
    if (!isCommandInstalled("corepack")) {
      console.log("📦 Installing corepack...");
      execSync("npm install -g corepack", { stdio: "inherit" });
    } else {
      console.log("✅ Corepack is already installed");
    }

    // 尝试启用 corepack
    console.log("🔄 Enabling corepack...");
    execSync("corepack enable", { stdio: "inherit" });
    console.log("✅ Corepack enabled successfully");
  } catch (error) {
    console.error("❌ Error with corepack:", error.message);
    console.log(
      "⚠️  Skipping corepack setup. You may need to manually install and enable corepack."
    );
    console.log("   You can do this by running:");
    console.log("   npm install -g corepack");
    console.log("   corepack enable");
  }
}

同样,首先我们检查一下当前corepack是否已经安装了,一般node版本是16.10+以上是内置了。这里同样判断当前使用已经安装了corepack,如果没有,我们会进行全局安装下,安装完毕后,我们就会执行corepack enable进行激活管理。

第七步:检查并更新 .gitignore 文件【这一步看项目需要,如果觉得不想提交上去维护,就ignore就好】

js 复制代码
// 检查并更新 .gitignore 文件
function checkAndUpdateGitignore() {
  try {
    const gitignorePath = path.join(process.cwd(), ".gitignore");
    const nvmrcEntry = ".nvmrc";

    // 检查文件是否存在,如果不存在则创建
    if (!fs.existsSync(gitignorePath)) {
      fs.writeFileSync(gitignorePath, nvmrcEntry + "\n");
      console.log("✅ Created .gitignore and added .nvmrc");
      return;
    }

    // 读取文件内容
    const content = fs.readFileSync(gitignorePath, "utf8");

    // 检查是否已包含 .nvmrc
    if (content.includes(nvmrcEntry)) {
      console.log("✅ .nvmrc already exists in .gitignore");
    } else {
      // 在文件末尾添加 .nvmrc
      fs.appendFileSync(gitignorePath, "\n" + nvmrcEntry + "\n");
      console.log("✅ Added .nvmrc to .gitignore");
    }
  } catch (error) {
    console.error("❌ Error updating .gitignore:", error.message);
    console.log(
      "⚠️  Skipping .gitignore update. You may need to manually add .nvmrc to .gitignore."
    );
    console.log("   Add this line to your .gitignore:");
    console.log("   .nvmrc");
  }
}

这一步其实很简单理解啦,就是看是否维护.nvmrc到项目中,如果觉得没必要维护,就是ignore掉不提交上去。

以上就是整个脚本的实现原理和解析啦,大家可以根据我的脚本,可以给点建议和意见,帮我进一步迭代优化,也是没问题的,因为我是花了一个多小时秒出来的,所以可能很多东西不够严谨,还需多加维护吧哈哈,希望大家都多提一下宝贵意见啦~

二、应用脚本的步骤

这里我主要是考虑的是用户可以git clone拉到项目之后,他不需要去关心,直接执行完pnpm install之后,就会执行我的脚本进行处理。所以我把脚本放到了postinstall执行。如果觉得不好的话,大家可以根据自己的实际情况,进行脚本执行处理即可。

  1. 首先,在你的项目之中,创建一个 scripts 文件夹,然后把这个脚本放入到文件夹中
  2. 在 package.json 文件中,添加如下脚本
json 复制代码
"postinstall": "node ./scripts/postinstall.js && rm -rf v8-compile-cache-0",
  1. 如果你想指定对应 pnpm 和 node 版本,可以在项目根目录中创建 RN_CONFIG_VERSION.json 文件,然后写入该内容
json 复制代码
{
  "pnpm": "8.6.12",
  "node": "18.17.0"
}

这样其实就完成,当你执行 pnpm install 的时候,就会自动执行脚本

总结

我觉得现在在业务实践和前端工程化中,大家可以多点思考。让代码也更加智能化和自动化啦。比如实现这个脚本,我觉得对于项目维护有两个优点:

  1. 可以统一团队大家使用的pnpm和node版本。
  2. 并且便于后续的升级版本的管理。

这样也可以大大增强工程化维护的健壮性和可持续性啦~ 感谢你们能够读完这篇文章啦,希望他能够对大家有益啦~我是CnLiang,有建议的同学或小伙伴,可以给我发邮件[email protected]哦,我会及时进行关注和回复,也希望大家能多多支持关注,感谢🙏

相关推荐
魔都吴所谓1 分钟前
[前端]HTML模拟实现一个基于摄像头的手势识别交互页面
前端·html·交互
来自星星的猫教授3 分钟前
Vue 3.6前瞻:响应式性能革命与Vapor模式展望
前端·javascript·vue.js
涵信7 分钟前
第九节 高频代码题-实现Sleep函数(异步控制)
前端·javascript·typescript
Kusunoki_D21 分钟前
Python 实现 Web 静态服务器(HTTP 协议)
服务器·前端·python
爱学习的茄子31 分钟前
【前端实战】三分钟掌握原生JS电影搜索应用,从此告别框架依赖
前端·javascript·深度学习
林太白33 分钟前
Next.js超简洁完整篇
前端·后端·react.js
前端付豪33 分钟前
汇丰登录风控体系拆解:一次 FaceID 被模拟攻击的调查纪实
前端·后端·架构
天生我材必有用_吴用41 分钟前
Three.js开发必备:模型对象和材质详解
前端
万变不离其宗_841 分钟前
echarts使用笔记
前端·笔记·echarts
时光足迹44 分钟前
电子书阅读器之章节拆分
前端·javascript·react.js