【源码共读】还在用npm link? 试试yalc

300 阅读3分钟

当我们在构建复杂项目时,往往会将部分功能拆分出一个独立的npm模块,以便其他项目复用。现在遇到一个这样的场景:我们构建了最新的组件库,希望在A项目中使用并查看效果,但是组件库还没发布,这时候该如何处理呢?
解决办法:

  • 直接在package.json中修改包路径
"dependencies": {
  "bar": "file:../foo/bar"
}
  • npm link
cd ~/projects/node-redis    # go into the package directory
npm link                    # creates global link
cd ~/projects/node-bloggy   # go into some other package directory.
npm link redis              # link-install the package
  • yalc

image.png
yalc 将npm link的步骤简化,并实现了包更新后,其他项目依赖项的更新。

简单使用

首先,我们来尝试下用法

# 在公用库 package1 中
yalc publish --push
# 在项目中引入
yalc add package1
# 在项目中移除
yalc remove package1

项目中会导入package1和生成相应的链接
image.png
image.png
用法非常简单,接下来我们来看这个过程是怎么实现的

源码分析

通过一个简单的例子,我们大概知道他的原理:

  • 这个库会将需要发布的包存入全局的.yalc中,当其他项目引入依赖时,将对应的包存放至当前项目的.yalc文件夹中,修改对应包的软连接。

接下来,我们来看下源码是怎么实现的
github.com/wclr/yalc?s…
跳转至package.json查看入口文件
image.png

// index.js
// 省略...

// 项目主目录
const userHome = homedir()

// 基本配置参数
export const values = {
  myNameIs: 'yalc',
  ignoreFileName: '.yalcignore',
  myNameIsCapitalized: 'Yalc',
  lockfileName: 'yalc.lock',
  yalcPackagesFolder: '.yalc',
  prescript: 'preyalc',
  postscript: 'postyalc',
  installationsFile: 'installations.json',
}

// 省略...

/* 
  Not using Node.Global because in this case 
  <reference types="mocha" /> is aded in built d.ts file  
*/
export const yalcGlobal: YalcGlobal = global as any

// 获取全局yalc的存储路径
export function getStoreMainDir(): string {
  if (yalcGlobal.yalcStoreMainDir) {
    return yalcGlobal.yalcStoreMainDir
  }
  if (process.platform === 'win32' && process.env.LOCALAPPDATA) {
    return join(process.env.LOCALAPPDATA, values.myNameIsCapitalized)
  }
  return join(userHome, '.' + values.myNameIs)
}
// 找到当前包的对应全局yalc的存储路径
export function getStorePackagesDir(): string {
  return join(getStoreMainDir(), 'packages')
}
// 获取当前包的存储路径
export const getPackageStoreDir = (packageName: string, version = '') =>
  join(getStorePackagesDir(), packageName, version)

export const execLoudOptions = { stdio: 'inherit' } as ExecSyncOptions

const signatureFileName = 'yalc.sig'

// 读取签名文件
export const readSignatureFile = (workingDir: string) => {
  const signatureFilePath = join(workingDir, signatureFileName)
  try {
    const fileData = fs.readFileSync(signatureFilePath, 'utf-8')
    return fileData
  } catch (e) {
    return ''
  }
}
// 读取yalcignore
export const readIgnoreFile = (workingDir: string) => {
  const filePath = join(workingDir, values.ignoreFileName)
  try {
    const fileData = fs.readFileSync(filePath, 'utf-8')
    return fileData
  } catch (e) {
    return ''
  }
}
// 写入签名文件 yalc.sig
export const writeSignatureFile = (workingDir: string, signature: string) => {
  const signatureFilePath = join(workingDir, signatureFileName)
  try {
    fs.writeFileSync(signatureFilePath, signature)
  } catch (e) {
    console.error('Could not write signature file')
    throw e
  }
}
  • 当我们执行yalc publish --push的时候,会执行哪些流程
  • 首先在源码中找到对应的命令
  • github.com/wclr/yalc/b…

image.png

.command({
  // 定义指令
  command: 'publish',
  // 指令描述
  describe: 'Publish package in yalc local repo',
  builder: () => {
    // 设置命令的默认选项
    return yargs
      .default('sig', false)
      .default('scripts', true)
      .default('dev-mod', true)
      .default('workspace-resolve', true)
      .default(rcArgs)
      .alias('script', 'scripts')

      .boolean(['push'].concat(publishFlags))
  },
  handler: (argv) => {
    // 推送到所有安装的地方
    return publishPackage(getPublishOptions(argv))
  },
})
export const publishPackage = async (options: PublishPackageOptions) => {
  const workingDir = options.workingDir

  // 检查包的清单文件
  const pkg = readPackageManifest(workingDir)
  if (!pkg) {
    return
  }

  // 获取包管理器
  const pm = getPackageManager(workingDir)

  // 执行脚本 eg. npm run build
  const runPmScript = (script: keyof PackageScripts) => {
    // 省略..
  }
  
  // 检查包的私有性
  if (pkg.private && !options.private) {
     // 省略..
  }
  
  // 执行预发布脚本
  const preScripts: (keyof PackageScripts)[] = [
    'prepublish',
    'prepare',
    'prepublishOnly',
    'prepack',
    'preyalcpublish',
  ]
  preScripts.forEach(runPmScript)
  
  // 拷贝包到store
  const copyRes = await copyPackageToStore(options)
  
  // 省略..

  // 执行发布后脚本
  const postScripts: (keyof PackageScripts)[] = [
    'postyalcpublish',
    'postpack',
    'publish',
    'postpublish',
  ]
  postScripts.forEach(runPmScript)

  // 省略...

  // 遍历每个安装目录,更新包
  if (options.push) {
    const installationsConfig = readInstallationsFile()
    const installationPaths = installationsConfig[pkg.name] || []
    const installationsToRemove: PackageInstallation[] = []
    for (const workingDir of installationPaths) {
      console.info(`Pushing ${pkg.name}@${pkg.version} in ${workingDir}`)
      const installationsToRemoveForPkg = await updatePackages([pkg.name], {
        replace: options.replace,
        workingDir,
        update: options.update,
        noInstallationsRemove: true,
      })
      installationsToRemove.push(...installationsToRemoveForPkg)
    }
    await removeInstallations(installationsToRemove)
  }
}

总结

通过学习这个库,我们了解到这个库的原理就是通过建立一个全局的公共库,在项目引用时,自动修改对应的包成软连接,从而实现了本地modules的使用。

  • 代码中的publishPackage函数会运行包管理器的预发布脚本和发布后脚本,以确保在发布过程中执行必要的脚本操作。
  • 使用了yargs库来解析命令行的参数。
  • 考虑了边界情况,~,^,*的版本号
  • overloadConsole 定义了多种颜色的命令行