cross-env@10.1.0源码阅读

86 阅读6分钟

发布日期: 2025 年 9 月 30 日

cross-env 是一个 Node.js 工具,用于解决不同操作系统间环境变量设置方式不一致的问题,支持 Windows、Linux 和 macOS平台。

package.json

cross-env-10.1.0/package.json

{
  "name": "cross-env",
  "version": "0.0.0-semantically-released",
  "description": "Run scripts that set and use environment variables across platforms",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "bin": {
    "cross-env": "./dist/bin/cross-env.js",
    "cross-env-shell": "./dist/bin/cross-env-shell.js"
  },
  "engines": {
    "node": ">=20"
  },
  "scripts": {
    "build": "zshy",
    "dev": "zshy --watch",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "format": "prettier --write .",
    "format:check": "prettier --check .",
    "typecheck": "tsc --noEmit",
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:run": "vitest run",
    "test:coverage": "vitest run --coverage",
    "test:e2e": "node e2e/test-cross-env.js && node e2e/test-cross-env-shell.js && node e2e/test-default-values.js",
    "validate": "npm run build && npm run typecheck && npm run lint && npm run format:check && npm run test:run"
  },
  "files": [
    "dist"
  ],
  "keywords": [
    "cross-environment",
    "environment variable",
    "windows",
    "cross-platform"
  ],
  "author": "Kent C. Dodds <me@kentcdodds.com> (https://kentcdodds.com)",
  "license": "MIT",
  "dependencies": {
    "@epic-web/invariant": "^1.0.0",
    "cross-spawn": "^7.0.6"
  },
  "devDependencies": {
    "@epic-web/config": "^1.21.1",
    "@types/cross-spawn": "^6.0.6",
    "@types/node": "^24.1.0",
    "@vitest/coverage-v8": "^3.2.4",
    "@vitest/ui": "^3.2.4",
    "eslint": "^9.32.0",
    "prettier": "^3.6.2",
    "typescript": "^5.8.3",
    "vitest": "^3.2.4",
    "zshy": "^0.3.0"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/kentcdodds/cross-env.git"
  },
  "bugs": {
    "url": "https://github.com/kentcdodds/cross-env/issues"
  },
  "homepage": "https://github.com/kentcdodds/cross-env#readme",
  "zshy": {
    "cjs": false,
    "exports": {
      ".": "./src/index.ts",
      "./bin/cross-env": "./src/bin/cross-env.ts",
      "./bin/cross-env-shell": "./src/bin/cross-env-shell.ts"
    }
  },
  "prettier": "@epic-web/config/prettier",
  "module": "./dist/index.js",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    },
    "./bin/cross-env": {
      "types": "./dist/bin/cross-env.d.ts",
      "import": "./dist/bin/cross-env.js"
    },
    "./bin/cross-env-shell": {
      "types": "./dist/bin/cross-env-shell.d.ts",
      "import": "./dist/bin/cross-env-shell.js"
    }
  }
}

cross-env

cross-env-10.1.0/src/bin/cross-env.ts

#!/usr/bin/env node

import { crossEnv } from '../index.js'

// process.argv.slice(2) 用于获取命令行参数(排除掉 node 和脚本路径本身)
crossEnv(process.argv.slice(2))

cross-env-shell

cross-env-10.1.0/src/bin/cross-env-shell.ts

#!/usr/bin/env node

import { crossEnv } from '../index.js'

crossEnv(process.argv.slice(2), { shell: true })

crossEnv

cross-env-10.1.0/src/index.ts

import { spawn } from 'cross-spawn'

function crossEnv(
  args: string[],
  options: CrossEnvOptions = {},
): ProcessResult | null {

  // 1、解析命令行参数
  // envSetters:需要设置的环境变量键值对(如 { NODE_ENV: 'production' })
  // command:要执行的命令(如 node 或 webpack)
  // commandArgs:命令的参数(如 ['server.js'])
  const [envSetters, command, commandArgs] = parseCommand(args)

  // 2、构建环境变量对象
  // 基于当前进程的环境变量(process.env)
  // 合并 envSetters 中的自定义环境变量(会经过格式处理)
  // 保证跨平台兼容性(如保留 Windows 的 APPDATA 变量)
  const env = getEnvVars(envSetters)

  // 3、执行命令(当命令存在时)
  if (command) {
    // 配置子进程启动选项
    const spawnOptions: SpawnOptions = {
      stdio: 'inherit',   // 子进程共享父进程的输入输出流(控制台)
      shell: options.shell,  // 是否通过 shell 执行命令(可选)
      env,  // 使用上面构建的环境变量对象
    }

    // 启动子进程执行命令
    // 使用 cross-spawn 的 spawn 函数(跨平台版本的 child_process.spawn)启动子进程
    const proc = spawn(
      // run `path.normalize` for command(on windows)
      // 处理命令名称(如 Windows 路径规范化)
      commandConvert(command, env, true),
      // by default normalize is `false`, so not run for cmd args
       // 处理命令参数
      commandArgs.map((arg) => commandConvert(arg, env)),
      spawnOptions,
    )

    // 4、信号处理(进程间通信)
    // 当父进程收到终止信号时,将信号传递给子进程
    // 确保父进程收到终止信号(如用户按 Ctrl+C)时,子进程能同步终止,避免僵尸进程
    process.on('SIGTERM', () => proc.kill('SIGTERM'))
    process.on('SIGINT', () => proc.kill('SIGINT'))
    process.on('SIGBREAK', () => proc.kill('SIGBREAK'))
    process.on('SIGHUP', () => proc.kill('SIGHUP'))

    // 5、处理子进程退出
    proc.on('exit', (code: number | null, signal?: string) => {
      let crossEnvExitCode = code
      // 如果退出码为 null(通常是被信号终止),设置默认退出码
      // 处理信号终止的情况(如 SIGINT 通常是用户主动中断,返回 0 表示正常退出)
      if (crossEnvExitCode === null) {
        crossEnvExitCode = signal === 'SIGINT' ? 0 : 1
      }
      // 父进程使用子进程的退出码退出
      process.exit(crossEnvExitCode)
    })

    return proc
  }

  return null
}
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "test-crossenv": "cross-env NODE_ENV=test node test-crossenv.js"
},

执行 npm run test-crossenv

parseCommand

cross-env-10.1.0/src/index.ts

function parseCommand(
  args: string[],
): [Record<string, string>, string | null, string[]] {

  // 存储环境变量
  const envSetters: Record<string, string> = {}
  // 存储命令名称
  let command: string | null = null
  // 存储命令参数
  let commandArgs: string[] = []

  // 遍历处理参数
  for (let i = 0; i < args.length; i++) {
    const arg = args[i]
    if (!arg) continue // 跳过空参数
    const match = envSetterRegex.exec(arg)

    // 解析环境变量
    if (match && match[1]) {
      let value: string
      if (typeof match[3] !== 'undefined') {
        value = match[3]
      } else if (typeof match[4] === 'undefined') {
        value = match[5] || ''
      } else {
        value = match[4]
      }

      envSetters[match[1]] = value

      // 解析命令和命令参数
    } else {
      // No more env setters, the rest of the line must be the command and args
      const cStart = args
        .slice(i)
        .map((a) => {
          const re = /\\\\|(\\)?'|([\\])(?=[$"\\])/g
          // Eliminate all matches except for "\'" => "'"
          return a.replace(re, (m) => {
            if (m === '\\\\') return '\\'
            if (m === "\\'") return "'"
            return ''
          })
        })
      const parsedCommand = cStart[0]
      invariant(parsedCommand, 'Command is required') // 确保命令存在
      command = parsedCommand
      commandArgs = cStart.slice(1).filter(Boolean) // 过滤空参数
      // 退出循环,后续参数已处理
      break
    }
  }

  return [envSetters, command, commandArgs]
}
const envSetterRegex = /(\w+)=('(.*)'|"(.*)"|(.*))/
const re = /\\\\|(\\)?'|([\\])(?=[$"\\])/g

getEnvVars

cross-env-10.1.0/src/index.ts

function getEnvVars(
  envSetters: Record<string, string>
): NodeJS.ProcessEnv {
  
  // 初始化环境变量对象
  const envVars = { ...process.env }

  // 特殊处理 Windows 系统的 APPDATA 变量
  // APPDATA 是 Windows 系统中存储应用程序数据的目录路径环境变量
  // 通常路径为 C:\Users\<用户名>\AppData\Roaming)
  if (process.env.APPDATA) {
    envVars.APPDATA = process.env.APPDATA
  }

  // 合并并处理自定义环境变量
  Object.keys(envSetters).forEach((varName) => {
    const value = envSetters[varName]
    if (value !== undefined) {
      envVars[varName] = varValueConvert(value, varName)
    }
  })
  return envVars
}

varValueConvert

cross-env-10.1.0/src/variable.ts

function varValueConvert(
	originalValue: string,
	originalName: string,
): string {
	return resolveEnvVars(replaceListDelimiters(originalValue, originalName))
}

replaceListDelimiters

cross-env-10.1.0/src/variable.ts

function replaceListDelimiters(varValue: string, varName = ''): string {

  // 1、确定目标分隔符
  // Windows 系统的路径列表分隔符是 ;(例如 PATH=C:\a;C:\b)
  // 类 Unix 系统(Linux、macOS 等)的路径列表分隔符是 :(例如 PATH=/usr/bin:/bin)
  const targetSeparator = isWindows() ? ';' : ':'

  // pathLikeEnvVarWhitelist 是一个白名单集合(包含 PATH、NODE_PATH 环境变量名)
  if (!pathLikeEnvVarWhitelist.has(varName)) {
    return varValue
  }

  // 匹配一个或多个反斜杠(\\*)后面紧跟一个冒号(:)
  // (\\*): 捕获组,匹配0个或多个反斜杠
  // 在JavaScript字符串中需要用两个反斜杠表示一个实际的反斜杠
  return varValue.replace(
    /(\\*):/g, 
    // 替换回调
    // match: 完整的匹配结果(反斜杠序列加冒号)
    // backslashes: 捕获组中匹配的反斜杠部分
    (match, backslashes) => {
      if (backslashes.length % 2) {
        // 反斜杠数量为奇数:表示分隔符被转义,移除一个反斜杠
        return match.substring(1)
      }
      // 反斜杠数量为偶数:表示是普通分隔符,替换为目标分隔符
      return backslashes + targetSeparator
    })
}
const pathLikeEnvVarWhitelist = new Set(['PATH', 'NODE_PATH'])

resolveEnvVars

cross-env-10.1.0/src/variable.ts

function resolveEnvVars(varValue: string): string {

  const envUnixRegex = /(\\*)(\$(\w+)|\${(\w+)})/g

  return varValue.replace(
    envUnixRegex,
    // 替换回调函数
    // escapeChars:环境变量引用前的所有反斜杠(捕获组 1)
    // varNameWithDollarSign:完整的环境变量引用(如 $VAR 或 ${VAR})
    // varName:$VAR 格式中的变量名(捕获组 3)
    // altVarName:${VAR} 格式中的变量名(捕获组 4)
    (_, escapeChars, varNameWithDollarSign, varName, altVarName) => {
      // 奇数个反斜杠
      // 当反斜杠数量为奇数时,表示这个环境变量引用被转义了,应该保留原始格式(不替换为实际值)
      if (escapeChars.length % 2 === 1) {
        return varNameWithDollarSign
      }
      // 偶数个反斜杠
      // 反斜杠数量为偶数时,表示是正常的环境变量引用
      // 保留一半的反斜杠(因为偶数个反斜杠是成对的转义)
      // 拼接上环境变量的实际值(从 process.env 获取,不存在则用空字符串)
      return (
        escapeChars.substring(0, escapeChars.length / 2) +
        (process.env[varName || altVarName] || '')
      )
    },
  )
}

commandConvert

cross-env-10.1.0/src/command.ts

function commandConvert(
  command: string,
  env: NodeJS.ProcessEnv,
  normalize = false,
): string {
  // 1、非 Windows 系统直接返回
  if (!isWindows()) {
    return command
  }

  // 2、定义正则
  // 匹配简单变量引用: $var 或 ${var}
  const simpleEnvRegex = /\$(\w+)|\${(\w+)}/g
  // 匹配带默认值的 Bash 参数扩展: ${var:-default}
  const defaultValueRegex = /\$\{(\w+):-([^}]+)\}/g

  let convertedCmd = command

  // First, handle bash parameter expansion with default values
  // 3、处理带默认值的变量引用
  convertedCmd = convertedCmd.replace(
    defaultValueRegex,
    // 替换回调函数
    // match:整个匹配的字符串(如 ${PORT:-3000})
    // varName:正则捕获组 1 的值,即环境变量名(如 PORT)
    // defaultValue:正则捕获组 2 的值,即默认值(如 3000)
    (match, varName, defaultValue) => {
      // 优先用环境变量值,否则用默认值
      const value = env[varName] || defaultValue
      return value
    },
  )

  // 4、处理简单变量引用
  convertedCmd = convertedCmd.replace(
    simpleEnvRegex, 
    // 替换回调函数
    // match:整个匹配的字符串(如 $PATH 或 ${HOME})
    // $1:正则第一个捕获组的值,对应 $VAR 格式中的变量名(如 PATH)
    // $2:正则第二个捕获组的值,对应 ${VAR} 格式中的变量名(如 HOME)
    (match, $1, $2) => {
      // 从捕获组获取变量名($1 对应 $VAR,$2 对应 ${VAR})
      const varName = $1 || $2
      // 如果环境变量存在,返回 Windows 风格的 %VAR%,否则返回空字符串
      return env[varName] ? `%${varName}%` : ''
    })

  // 5、路径规范化(可选)
  return normalize === true ? path.normalize(convertedCmd) : convertedCmd
}

isWindows

function isWindows(): boolean {
  return (
    // 条件1:检测原生 Windows 系统
    process.platform === 'win32' ||
    // 条件2:检测 Windows 兼容环境(msys/cygwin)
    /^(msys|cygwin)$/.test(process.env.OSTYPE || '')
  )
}

process.platform:是 Node.js 内置的进程属性,用于返回当前操作系统的平台标识,不同系统对应固定值:

  1. Windows 系统(包括 Windows 10/11、Windows Server 等)返回 'win32'(注意:即使是 64 位 Windows,也返回 'win32',这是历史兼容设计);
  2. macOS 系统返回 'darwin'
  3. Linux 系统返回 'linux'

process.env.OSTYPE:是环境变量中存储的 “操作系统类型” 标识,常见于 Unix-like 环境或 Windows 兼容层(如 msys、cygwin):

  1. msys/cygwin:是 Windows 系统上的两款类 Unix 兼容层工具(可模拟 Linux/macOS 的命令行环境,如 Git Bash 基于 msys),它们会将 OSTYPE 设为 'msys''cygwin'
  2. 原生 Linux/macOS 中,OSTYPE 通常为 'linux-gnu''darwin',不会匹配此正则。

cross-env 适用场景

  1. 简单命令执行,不需要 shell 特性。
  2. 需要高性能执行的场景(不经过 shell 可以略微提高性能)。
  3. 安全敏感场景(避免 shell 注入风险)。

cross-env-shell 适用场景

  1. 需要使用 shell 特性(如管道 |、重定向 >、命令组合 && 等)。
  2. 需要执行复杂的 shell 脚本。
  3. 需要变量替换、通配符等 shell 功能。