lerna源码解析

147 阅读15分钟

内容基于lerna v8,node18+

核心目标:

  • 理解import-local实现原理
  • 理解require.resolve实现原理
  • 理解lerna实现原理

一、入口文件

lerna使用的是Nx 的高效构建工具链,找到packages/lerna/project.json

{
  // 项目名称为 lerna,表示这是 Lerna CLI 的核心包
  "name": "lerna",
  // 指向 Nx 的项目模式文件,确保配置符合 Nx 规范
  "$schema": "../../node_modules/nx/schemas/project-schema.json",
  // 源代码目录为 packages/lerna/src,即 Lerna 的核心代码存放位置。
  "sourceRoot": "packages/lerna/src",
  "projectType": "application",
  "tags": [],
  // 配置了多个构建目标(如 build、compile、lint、test),每个目标定义了如何执行特定任务。
  "targets": {
    "build": {
      // 依赖 compile 目标,确保先编译代码。
      "dependsOn": ["compile"],
      // 使用 Nx 的 nx:run-commands 执行器,执行自定义 shell 命令
      "executor": "nx:run-commands",
      "options": {
        // packages/lerna/dist 目录下执行命令。
        "cwd": "packages/lerna/dist",
        "parallel": false,
        // 运行 rm -rf package.json,删除生成目录中的 package.json(可能用于清理构建产物)。
        "commands": ["rm -rf package.json"]
      }
    },
    "compile": {
      // 使用 Nx 的 @nx/esbuild 插件进行编译。
      "executor": "@nx/esbuild:esbuild",
      "outputs": ["{options.outputPath}"],
      "options": {
        // 编译后的文件输出到 packages/lerna/dist---------------说明编译构建之后会生成dist目录
        "outputPath": "packages/lerna/dist",
        // 入口文件为 src/index.ts,这是 Lerna 的核心入口点。
        "main": "packages/lerna/src/index.ts",
        // 使用 tsconfig.lib.json 配置 TypeScript 编译选项
        "tsConfig": "packages/lerna/tsconfig.lib.json",
        "assets": [
          // 将 src/cli.js 复制到输出目录,确保 CLI 入口文件存在。
          {
            "input": "packages/lerna/src",
            "glob": "cli.js",
            "output": "."
          }
        ],
        "thirdParty": false,
        "platform": "node",
        "format": ["cjs"],
        // 列出所有需要编译的命令模块(如 publish、version 等),确保每个命令的代码都被处理。
        "additionalEntryPoints": [
        ],
        "esbuildOptions": {
          // 排除一些依赖项(如 @lerna/create、nx/*),避免打包时包含这些模块。
          "external": ["*package.json", "@lerna/create", "@lerna/legacy-package-management", "nx/*", "@nx/*"],
          "outExtension": {
            // 输出为 .js 文件(CommonJS 格式)
            ".js": ".js"
          },
          "logOverride": {
            // 抑制某些 Esbuild 的警告(如 CommonJS 变量在 ESM 中的使用)
            "commonjs-variable-in-esm": "silent"
          }
        }
      }
    },
    "lint": {
      // 使用 ESLint 进行代码检查。
      "executor": "@nx/eslint:lint",
      "outputs": ["{options.outputFile}"]
    },
    "test": {
      "executor": "@nx/jest:jest",
      "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
      "options": {
        // 使用 Jest 进行单元测试,配置文件为 jest.config.ts。
        "jestConfig": "packages/lerna/jest.config.ts"
      }
    }
  }
}

通过 compilebuild 目标将 TypeScript 代码编译为可执行的 CLI 工具,并通过 linttest 保障代码质量。

这个配置文件大致看出构建流程

编译阶段( compile

  • 将 TypeScript 代码编译为 CommonJS 格式。
  • 处理所有命令模块(如 publishversion),确保每个命令的代码被正确打包。
  • 复制 cli.js 到输出目录,作为 CLI 入口文件。

构建阶段( build

  • 清理生成目录中的 package.json(可能用于避免重复文件)。

测试阶段( test

  • 使用 Jest 运行测试用例,确保代码质量。

上面推测可以知道lerna的入口文件packages/lerna/src/index.ts,但是脚手架开发主命令入口

可见,在执行lerna主命令的时候,入口文件为packages/lerna/src/cli.js

二、packages/lerna/src/cli.js解读

npx

  • npx 会优先查找当前项目中的 node_modules/.bin 目录,然后是全局安装的包。
  • 如果找不到,它会从 npm 仓库下载一个临时版本并运行。
const importLocal = require("import-local");

if (importLocal(__filename)) {
  // 输出本地版本提示
  function minimalInfoLog(prefix, message) {
    // 省略日志输出的实现细节
  }

  minimalInfoLog("cli", "using local version of lerna");
} else {
  // @ts-ignore
  require(".")(process.argv.slice(2));
}

代码的执行流程

  • 本地分支( importLocal(__filename) 返回 true) :
  • 入口脚本 cli.js 是本地安装的 node_modules/lerna/cli.js
    • 代码仅输出日志 using local version of lerna,没有其他操作。
  • 全局分支( importLocal 返回false ) :
  • 调用 require(".")(process.argv.slice(2)),即加载全局 lerna 的核心逻辑。

本地分支入口脚本的职责

  • 入口脚本 cli.js 的核心职责是 版本检测提示本地版本
  • 核心逻辑的触发由 Node.js 的模块加载机制自动完成,无需显式调用。

本地版本的核心逻辑触发机制

本地cli.js是本地安装的模块的一部分

  • 当用户运行 npx lerna 时,Node 直接执行本地 node_modules/lerna/cli.js
  • 该脚本属于本地 lerna 模块,其核心逻辑 已包含在本地模块中
  • 核心逻辑的触发由 Node 的模块加载规则 自动完成,而非入口脚本显式调用。

具体实现细节

本地模块的加载点

根据 Node 的模块加载规则,当用户运行 npx lerna 时:

Node 执行 node_modules/lerna/cli.js

importLocal 检测到本地版本,输出日志。

后续逻辑由 lerna 模块的 main 字段指定的入口点自动触发(如 index.js)。

核心逻辑通过模块导出的函数自动执行,无需入口脚本显式调用

路径解析与模块查找

⚠️ 从 Node.js v13.2 版本开始,默认支持 ES6 模块。在使用模块语法之前,要先正确声明。可以通过文件扩展名或项目配置来区分模块类型。

基于文件扩展名

  • .mjs 文件:Node.js 会将其解释为 ES6 模块,支持使用 importexport

  • .cjs 文件:Node.js 会将其解释为 CommonJS 模块,只能使用requiremodule.exports

基于项目配置

在项目的 package.json 文件中,指定 type 字段为 module

{

"type": "module"

}

一旦设置,该目录下的 .js 文件都会被解释为 ES6 模块,支持使用 importexport

现创建本地验证代码目录

mkdir ~/Desktop/lerna-pro
cd ~/Desktop/lerna-pro
npm init
// 先全局安装lerna
npm install lerna -g

编辑器打开该代码目录,手动按照以下结构创建文件

lerna-pro/
├── packages/
│   ├── core  
│   	├── lib  
│         ├── index.js
│       └── package.json
├── package.json
  • 静态字符串

以下模版字符串只支持在common.js模块中使用

cd ~/Desktop/lerna-pro/core/lib
node index.js
#!/usr/bin/env node 
console.log(__filename, '__filename')
// 当前文件的路径
// /Users/gene/Desktop/lerna-pro/packages/core/lib/index.js __filename

console.log(__dirname, '__dirname')
// 当前文件所在的目录
// /Users/gene/Desktop/lerna-pro/packages/core/lib __dirname

console.log(process.cwd(), 'process.cwd()');
// 当前执行的文件所在的目录-----注意这点很重要
// /Users/gene/Desktop/lerna-pro/packages/core/lib

console.log(process, 'process');

console.log(process, 'process')可以看到一些关键的信息

  • fileURLToPath()

fileURLToPath 是 Node.js 中用于将 文件 URL 转换为本地文件系统的文件路径。Node.js 的 ES Modules 中,import.meta.url 返回的是文件的 URL(以 file:// 开头),而许多文件操作需要本地路径(如 /Users/me/file.js)。

以下代码是es模块标准,修改lerna-pro/package.json文件,添加一个属性:

"type": "module"

cd ~/Desktop/lerna-pro/core/lib
node index.js
#!/usr/bin/env node

import { fileURLToPath } from 'url';      // ES Modules
import path from 'path';


// es模块中的本地文件路径
console.log(import.meta.url)
// file:///Users/gene/Desktop/lerna-pro/packages/core/lib/index.js

// 获取当前文件的绝对路径
const __filename = fileURLToPath(import.meta.url);
console.log(__filename, '__filename')
// /Users/gene/Desktop/lerna-pro/packages/core/lib/index.js __filename

// 获取当前文件所在目录的路径
const __dirname = path.dirname(__filename);
console.log(__dirname, '__dirname')
// /Users/gene/Desktop/lerna-pro/packages/core/lib __dirname

// 基于当前路径生成新的路径
const aliaPath = fileURLToPath(new URL('./src', import.meta.url));
console.log(aliaPath, 'aliaPath');
// /Users/gene/Desktop/lerna-pro/packages/core/lib/src aliaPath

其他用法:构建工具中配置路径别名

// 配置 '@' 别名指向 'src' 目录
// 根据当前文件的 URL,解析相对路径为绝对 URL
const path = fileURLToPath(new URL('./src', import.meta.url));
const alias = { '@': path };
  • pkg-dir

pkg-dir 是一个用于查找 Node.js 项目或 npm 包根目录的工具。它通过从指定目录向上查找,直到找到包含 package.json 文件的目录,从而确定项目的根目录。

默认优先从process.cwd()开始。

以下代码是commomJs模块标准,修改lerna-pro/package.json文件,去掉属性:"type": "module"

cd ~/Desktop/lerna-pro
npm i pkg-dir@4.2.0
cd ~/Desktop/lerna-pro/core/lib
node index.js
#!/usr/bin/env node 
const path = require('path');
const pkgDir = require('pkg-dir');
const globalDir = pkgDir.sync(path.dirname(__filename));
console.log(globalDir,'globalDir')
// /Users/gene/Desktop/lerna-pro globalDir

根据当前的项目结构可以知道pkg-dir的查找过程按照以下顺序

/Users/gene/Desktop/lerna-pro/packages/core/lib
/Users/gene/Desktop/lerna-pro/packages/core
/Users/gene/Desktop/lerna-pro/packages
/Users/gene/Desktop/lerna-pro // 停止查找

最终发现/Users/gene/Desktop/lerna-pro 下存在package.json,说明该目录为包的根目录,并返回路径。

  • path.relative()

path.relative() 是 Node.js 中 path 模块的一个方法,用于计算两个绝对路径之间的相对路径。

const relativePath = path.relative(from, to)

  • 参数
    • from:起始路径(字符串类型)。
    • to:目标路径(字符串类型)。
  • 返回值:从 fromto 的相对路径(字符串类型)。

⚠️ 注意事项

  • 参数必须为字符串

非字符串参数(如 nullundefined)会导致错误。确保传入字符串路径

  • 建议使用绝对路径

如果传入相对路径,它们会基于当前工作目录解析。为避免歧义,建议先用 path.resolve() 转为绝对路径。

// 举例说明
const path = require('path');

// POSIX 系统
console.log(path.relative('/a/b/c', '/a/b/d')); // 输出: '../d'

// Windows 系统
console.log(path.relative('C:\a\b', 'C:\a\d')); // 输出: '..\d'

// 相同路径,返回''
console.log(path.relative('/a/b', '/a/b')); // 输出: ''
  • resolveCwd.silent()

用于在 Node.js 中静默解析当前工作目录下的模块路径。与普通的 resolveCwd() 不同,resolveCwd.silent 在模块解析失败时不会抛出错误,而是返回 null,适合静默场景。静默失败的特性,可以避免因路径错误导致的进程中断,提升代码的健壮性。

  1. 路径基准
    解析始终基于 当前工作目录(即 process.cwd() )即终端所在的目录,而非当前文件所在目录。

若需基于文件目录解析,可使用 require.resolve 结合相对路径。

  1. 性能影响
    频繁调用可能影响性能(涉及文件系统查询),建议缓存结果。
  2. 模块缓存
    解析路径后,若通过 require() 加载模块,依然受 Node.js 模块缓存机制影响。
cd ~/Desktop/lerna-pro
npm install resolve-cwd@3.0.0
node packages/core/lib/index.js
#!/usr/bin/env node
const resolveCwd = require('resolve-cwd');

// 尝试解析插件路径: /Users/gene/Desktop/lerna-pro/packages/core/lib/cli.js
const targetFilePath = '/Users/gene/Desktop/lerna-pro/packages/core/lib/cli.js'
const pluginPath = resolveCwd.silent(targetFilePath);

// 如果不存在则返回null
if (pluginPath) {
  console.log('找到插件,路径:', pluginPath);
} else {
  console.log('未找到插件,跳过执行');
}

三、import-local实现原理

import-local是一个用于优先加载本地安装的模块版本的工具,适用于 CLI 工具的开发场景。其核心逻辑是通过路径解析和模块目录查找,判断当前环境是否存在本地安装的同名模块,并决定是否使用本地版本。

其实现基于 Node.js 的模块解析机制和文件系统路径操作。

  • 现全局安装lerna,lerna-pro本地不安装
npm install lerna -g
// 随意终端目录执行
lerna -v

以下是lerna v8import-local的源代码,观察打印信息

'use strict';
const path = require('path');
const {fileURLToPath} = require('url');
const resolveCwd = require('resolve-cwd');
const pkgDir = require('pkg-dir');
console.log('执行全局')

module.exports = filename => {
  // 入口文件地址
	const normalizedFilename = filename.startsWith('file://') ? fileURLToPath(filename) : filename;
	// /Users/gene/.nvm/versions/node/v18.20.7/lib/node_modules/lerna/dist/cli.js

  // 根目录(入口文件所在的包)
	const globalDir = pkgDir.sync(path.dirname(normalizedFilename));
	// /Users/gene/.nvm/versions/node/v18.20.7/lib/node_modules/lerna

  // 相对路径(入口文件相对于根目录)
	const relativePath = path.relative(globalDir, normalizedFilename);
	// dist/cli.js

  // 根目录下package.json
	const pkg = require(path.join(globalDir, 'package.json'));
  console.log(pkg.name, 'packageName')
  // lerna
  
	console.log(path.join(pkg.name, relativePath), 'path.join(pkg.name, relativePath)')
  // lerna/dist/cli.js

  // 工作目录
	console.log(process.cwd(), 'process.cwd()');
  // /Users/gene/Desktop/lerna-pro 
	

  // 核心关键--静默查找--在工作目录下查找
  // 即在/Users/gene/Desktop/lerna-pro 下查找 lerna/dist/cli.js,有返回完整路径否则返回undifined
	const localFile = resolveCwd.silent(path.join(pkg.name, relativePath));
	// 返回undifined, 因为本地目录没有安装

  // 工作目录下的node_modules 路径
	const localNodeModules = path.join(process.cwd(), 'node_modules');
	// /Users/gene/Desktop/lerna-pro/node_modules
  
              
	const filenameInLocalNodeModules = !path.relative(localNodeModules, normalizedFilename).startsWith('..') &&
		
  	path.parse(localNodeModules).root === path.parse(normalizedFilename).root;

	console.log(filenameInLocalNodeModules,'filenameInLocalNodeModules')
  // 返回 false,因为本地工作目录下没有安装lerna的情况下,localNodeModules-> normalizedFilename
  // /Users/gene/Desktop/lerna-pro/node_modules ->
  // /Users/gene/.nvm/versions/node/v18.20.7/lib/node_modules/lerna/dist/cli.js
  // 一定是往上层找的,返回的相对路径一定是以../ 开头


	console.log(path.relative(localFile, normalizedFilename),'path.relative(localFile, normalizedFilename)')
	return !filenameInLocalNodeModules && localFile && path.relative(localFile, normalizedFilename) !== '' && require(localFile);
};

⚠️观察信息


console.log(path.parse(localNodeModules), 'path.parse(localNodeModules).)

console.log(path.parse(normalizedFilename), 'path.parse(normalizedFilename)')

以下代码为lerna入口文件cli.js

#!/usr/bin/env node
"use strict";

const importLocal = require("import-local");

// 全局安装lerna,lerna-pro本地不安装的情况下可以得出结论
// importLocal(__filename) == false
// 因此会加载全局安装的 /Users/gene/.nvm/versions/node/v18.20.7/lib/node_modules/lerna/dist/index.js

if (importLocal(__filename)) {
// ...
} else {
  // /Users/gene/.nvm/versions/node/v18.20.7/lib/node_modules/lerna/dist/index.js
  require(".")(process.argv.slice(2));
}
  • 现全局安装lerna@7,lerna-pro本地安装lerna@8
// 任意终端目录
npm uninstall lerna -g
npm install lerna@7 -g
cd ~/Desktop/learn-pro
npm install lerna@8
lerna -v

现在分别在全局和本地所对应的的入口文件cli.js以及import-local代码中加上打印信息进对比分析,得出执行流程


全局安装的lerna入口文件和import-local

"use strict";

const importLocal = require("import-local");
console.log('开始执行的是全局安装的cli.js')

if (importLocal(__filename)) {
  require("npmlog").info("cli", "using local version of lerna");
} else {
  require(".")(process.argv.slice(2));
}
'use strict';
const path = require('path');
const {fileURLToPath} = require('url');
const resolveCwd = require('resolve-cwd');
const pkgDir = require('pkg-dir');

module.exports = filename => {
	const normalizedFilename = filename.startsWith('file://') ? fileURLToPath(filename) : filename;
	const globalDir = pkgDir.sync(path.dirname(normalizedFilename));
	const relativePath = path.relative(globalDir, normalizedFilename);
	const pkg = require(path.join(globalDir, 'package.json'));
	const localFile = resolveCwd.silent(path.join(pkg.name, relativePath));

	const localNodeModules = path.join(process.cwd(), 'node_modules');

	console.log(localFile, 'localFile')
	console.log(localNodeModules, 'localNodeModules')
	console.log(normalizedFilename, 'normalizedFilename')
	console.log(path.relative(localNodeModules, normalizedFilename), 'path.relative(localNodeModules, normalizedFilename)')
	console.log(path.parse(localNodeModules), 'path.parse(localNodeModules)')
	console.log(path.parse(normalizedFilename), 'path.parse(normalizedFilename)')

	const filenameInLocalNodeModules = !path.relative(localNodeModules, normalizedFilename).startsWith('..') &&
		path.parse(localNodeModules).root === path.parse(normalizedFilename).root;
		console.log('全局安装import-local逻辑结束=====================')


	return !filenameInLocalNodeModules && localFile && path.relative(localFile, normalizedFilename) !== '' && require(localFile);
};

本地安装的lerna入口文件和import-local

#!/usr/bin/env node

"use strict";

const importLocal = require("import-local");
console.log('开始执行的是工作目录本地安装的cli.js')

if (importLocal(__filename)) {
  function minimalInfoLog(prefix, message) {
    const stream = process.stderr;
    const green = "\x1b[32m";
    const magenta = "\x1b[35m";
    const reset = "\x1b[0m";
    const useColor = stream.isTTY;

    let output = "";

    if (useColor) {
      output += green; // Info level color
    }
    output += "info";
    if (useColor) {
      output += reset;
    }

    if (prefix) {
      output += " ";
      if (useColor) {
        output += magenta; // Prefix color
      }
      output += prefix;
      if (useColor) {
        output += reset;
      }
    }

    output += " " + message;

    stream.write(output + "\n");
  }

  minimalInfoLog("cli", "using local version of lerna");
} else {
  // @ts-ignore
  require(".")(process.argv.slice(2));
}
'use strict';
const path = require('path');
const {fileURLToPath} = require('url');
const resolveCwd = require('resolve-cwd');
const pkgDir = require('pkg-dir');

module.exports = filename => {
	const normalizedFilename = filename.startsWith('file://') ? fileURLToPath(filename) : filename;
	const globalDir = pkgDir.sync(path.dirname(normalizedFilename));
	const relativePath = path.relative(globalDir, normalizedFilename);
	const pkg = require(path.join(globalDir, 'package.json'));
	const localFile = resolveCwd.silent(path.join(pkg.name, relativePath));
	const localNodeModules = path.join(process.cwd(), 'node_modules');

	console.log(localFile, 'localFile')
	console.log(localNodeModules, 'localNodeModules')
	console.log(normalizedFilename, 'normalizedFilename')
	console.log(path.relative(localNodeModules, normalizedFilename), 'path.relative(localNodeModules, normalizedFilename)')
	console.log(path.parse(localNodeModules), 'path.parse(localNodeModules)')
	console.log(path.parse(normalizedFilename), 'path.parse(normalizedFilename)')

	const filenameInLocalNodeModules = !path.relative(localNodeModules, normalizedFilename).startsWith('..') &&
		path.parse(localNodeModules).root === path.parse(normalizedFilename).root;
		console.log('工作目录本地安装import-local结束==============' )
	return !filenameInLocalNodeModules && localFile && path.relative(localFile, normalizedFilename) !== '' && require(localFile);
};

在工作目录下执行lerna init

gene@gene-mini lerna-pro % lerna init
开始执行的是全局安装的cli.js
/Users/gene/Desktop/lerna-pro/node_modules/lerna/dist/cli.js localFile
/Users/gene/Desktop/lerna-pro/node_modules localNodeModules
/Users/gene/.nvm/versions/node/v18.20.7/lib/node_modules/lerna/dist/cli.js normalizedFilename
../../../.nvm/versions/node/v18.20.7/lib/node_modules/lerna/dist/cli.js path.relative(localNodeModules, normalizedFilename)
{
  root: '/',
  dir: '/Users/gene/Desktop/lerna-pro',
  base: 'node_modules',
  ext: '',
  name: 'node_modules'
} path.parse(localNodeModules)
{
  root: '/',
  dir: '/Users/gene/.nvm/versions/node/v18.20.7/lib/node_modules/lerna/dist',
  base: 'cli.js',
  ext: '.js',
  name: 'cli'
} path.parse(normalizedFilename)
全局安装import-local逻辑结束=====================
开始执行的是工作目录本地安装的cli.js
/Users/gene/Desktop/lerna-pro/node_modules/lerna/dist/cli.js localFile
/Users/gene/Desktop/lerna-pro/node_modules localNodeModules
/Users/gene/Desktop/lerna-pro/node_modules/lerna/dist/cli.js normalizedFilename
lerna/dist/cli.js path.relative(localNodeModules, normalizedFilename)
{
  root: '/',
  dir: '/Users/gene/Desktop/lerna-pro',
  base: 'node_modules',
  ext: '',
  name: 'node_modules'
} path.parse(localNodeModules)
{
  root: '/',
  dir: '/Users/gene/Desktop/lerna-pro/node_modules/lerna/dist',
  base: 'cli.js',
  ext: '.js',
  name: 'cli'
} path.parse(normalizedFilename)
工作目录本地安装import-local结束==================
info cli using local version of lerna
lerna notice cli v8.2.2
lerna ERR! Lerna has already been initialized for this repo.
lerna ERR! If you are looking to ensure that your config is up to date with the latest and greatest, run `lerna repair` instead
gene@gene-mini lerna-pro % 

从执行的结果来看,可以分成三部份

分析第一部分:

执行是全局模块下的import-local的逻辑

'use strict';
const path = require('path');
const {fileURLToPath} = require('url');
const resolveCwd = require('resolve-cwd');
const pkgDir = require('pkg-dir');

module.exports = filename => {
  // 全局和本地同时安装了lerna的情况下,执行lerna init
  // 进入的全局的cli.js-> 全局的import-local
  // normalizedFilename的值可以看出
	const normalizedFilename = filename.startsWith('file://') ? fileURLToPath(filename) : filename;
  // /Users/gene/.nvm/versions/node/v18.20.7/lib/node_modules/lerna/dist/cli.js

	const globalDir = pkgDir.sync(path.dirname(normalizedFilename));
  // /Users/gene/.nvm/versions/node/v18.20.7/lib/node_modules/lerna
  
	const relativePath = path.relative(globalDir, normalizedFilename);
  // dist/cli.js
  
	const pkg = require(path.join(globalDir, 'package.json'));
  // /Users/gene/.nvm/versions/node/v18.20.7/lib/node_modules/lerna/package.json

  // path.join(pkg.name, relativePath)
  // lerna/dist/cli.js
  // 在工作目录下/Users/gene/Desktop/lerna-pro开始逐级往上找这个文件lerna/dist/cli.js,找到返回否则静默返回undefined
  // 因为本地有安装,所以可以找到
	const localFile = resolveCwd.silent(path.join(pkg.name, relativePath));
  // /Users/gene/Desktop/lerna-pro/node_modules/lerna/dist/cli.js

	const localNodeModules = path.join(process.cwd(), 'node_modules');
  // /Users/gene/Desktop/lerna-pro/node_modules

	console.log(localFile, 'localFile')
	console.log(localNodeModules, 'localNodeModules')
	console.log(normalizedFilename, 'normalizedFilename')


  // 取/Users/gene/Desktop/lerna-pro/node_modules->
  // /Users/gene/.nvm/versions/node/v18.20.7/lib/node_modules/lerna/dist/cli.js
  // 的相对路径一定是往上层找,所以结果一定是以..开头的
	console.log(path.relative(localNodeModules, normalizedFilename), 'path.relative(localNodeModules, normalizedFilename)')
	// ../../../.nvm/versions/node/v18.20.7/lib/node_modules/lerna/dist/cli.js
  
  
  console.log(path.parse(localNodeModules), 'path.parse(localNodeModules)')
  // 兼容不同系统的根目录
  
	console.log(path.parse(normalizedFilename), 'path.parse(normalizedFilename)')

	const filenameInLocalNodeModules = !path.relative(localNodeModules, normalizedFilename).startsWith('..') &&
		path.parse(localNodeModules).root === path.parse(normalizedFilename).root;
		console.log('全局安装import-local逻辑结束=====================')

  // 以上逻辑可以得出结论,入口文件/Users/gene/.nvm/versions/node/v18.20.7/lib/node_modules/lerna/dist/cli.js
  // 并不能在本地工作目录中被找到且本地的入口文件是存在的
  // 且这两者路径是不同的
  // 加载本地的入口文件/Users/gene/Desktop/lerna-pro/node_modules/lerna/dist/cli.js
  // 后续逻辑为第二部分的运行结果
	return !filenameInLocalNodeModules && localFile && path.relative(localFile, normalizedFilename) !== '' && require(localFile);
};

分析第二部分:

执行本地工作目录下的import-local的逻辑

'use strict';
const path = require('path');
const {fileURLToPath} = require('url');
const resolveCwd = require('resolve-cwd');
const pkgDir = require('pkg-dir');

module.exports = filename => {
	const normalizedFilename = filename.startsWith('file://') ? fileURLToPath(filename) : filename;
  // /Users/gene/Desktop/lerna-pro/node_modules/lerna/dist/cli.js
  
	const globalDir = pkgDir.sync(path.dirname(normalizedFilename));
  // /Users/gene/Desktop/lerna-pro/node_modules/lerna
  
	const relativePath = path.relative(globalDir, normalizedFilename);
  // dist/cli.js
  
	const pkg = require(path.join(globalDir, 'package.json'));
  // /Users/gene/Desktop/lerna-pro/node_modules/lerna/package.json

  // path.join(pkg.name, relativePath)
  // lerna/dist/cli.js
  // 在工作目录下/Users/gene/Desktop/lerna-pro开始逐级往上找这个文件lerna/dist/cli.js,找到返回否则静默返回undefined
  // 因为本地有安装,所以可以找到
	const localFile = resolveCwd.silent(path.join(pkg.name, relativePath));
  // /Users/gene/Desktop/lerna-pro/node_modules/lerna/dist/cli.js
  
	const localNodeModules = path.join(process.cwd(), 'node_modules');
  // /Users/gene/Desktop/lerna-pro/node_modules
  
	console.log(localFile, 'localFile')
	console.log(localNodeModules, 'localNodeModules')
	console.log(normalizedFilename, 'normalizedFilename')


  // 取/Users/gene/Desktop/lerna-pro/node_modules->
  // /Users/gene/Desktop/lerna-pro/node_modules/lerna/dist/cli.js
  // 的相对路径一定是往下层找,所以结果一定不是以..开头的
	console.log(path.relative(localNodeModules, normalizedFilename), 'path.relative(localNodeModules, normalizedFilename)')
	// lerna/dist/cli.js

  // 兼容判断不同系统
  console.log(path.parse(localNodeModules), 'path.parse(localNodeModules)')
	console.log(path.parse(normalizedFilename), 'path.parse(normalizedFilename)')

	const filenameInLocalNodeModules = !path.relative(localNodeModules, normalizedFilename).startsWith('..') &&
		path.parse(localNodeModules).root === path.parse(normalizedFilename).root;
		console.log('工作目录本地安装import-local结束==============' )

  
  // 以上逻辑可以得出结论,加载的入口文件/Users/gene/Desktop/lerna-pro/node_modules/lerna/dist/cli.js
  // 在本地工作目录中被找到且本地的入口文件是存在的
  // 所以!filenameInLocalNodeModules 返回false
  // localFile 将不会被重复加载
	return !filenameInLocalNodeModules && localFile && path.relative(localFile, normalizedFilename) !== '' && require(localFile);
};

分析第三部分

由于第二部分逻辑执行完毕后,将进入本地入口文件的以下部分逻辑

#!/usr/bin/env node

"use strict";

const importLocal = require("import-local");

if (importLocal(__filename)) {

} else {
  // 第二部分执行完将进入以下逻辑
  require(".")(process.argv.slice(2));
}

从以上的调试信息可以大致总结import-local的实现原理

通过路径分析和模块查找机制,判断本地是否存在同名包,并优先使用本地版本。其执行流程为:

  1. 获取当前入口文件的地址
  2. 通过pkg-dir找到项目的根目录
  3. 计算根目录到入口文件的相对路径
  4. 通过resolve-cwd尝试解析本地模块的路径
  5. 验证路径有效性
  6. 返回本地模块或者全局模块

四、lerna的实现原理

  1. Monorepo 结构:将多个包集中管理,共享 Git 仓库和根配置。
  2. 软链机制:通过动态符号链接实现本地依赖的快速切换,替代 npm link
  3. Git 集成:利用 Git 的标签和提交历史检测变更,实现自动化版本控制。
  4. 依赖提升:通过工作区(Workspaces)减少依赖重复安装。
  5. 命令行工具:模块化设计,提供高效的批量操作(如 lerna runlerna publish)。