ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

pnpm scriptShell 配置深度解析:pnpm-workspace.yaml 相对路径解析修复与底层实现

pnpm scriptShell 配置深度解析:pnpm-workspace.yaml 相对路径解析修复与底层实现 pnpm scriptShell 配置深度解析pnpm-workspace.yaml 相对路径解析修复与底层实现【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm导读scriptShell是 pnpm 用于指定生命周期脚本如install、build、devPreinstall等执行所用 shell 的配置项。本篇文章以 pnpm 仓库中 .changeset/curly-shells-resolve.md 所记录的行为变更为主线讲解scriptShell的配置方式、相对路径解析规则、裸命令名查找行为并结合仓库中 TypeScript 侧pnpm 本体与 Rust 侧pacquet的实现源码与测试用例还原该配置从读取、解析到执行 shell 的完整链路。读完本文你将掌握scriptShell的正确配置姿势、相对路径在 monorepo 嵌套包场景下的解析语义以及 Windows 平台下的安全限制。变更背景相对scriptShell路径的解析问题curly-shells-resolve这一变更记录patch 级别影响pnpm/config.reader、pacquet、pnpm三个包修复了一个与 issue #14422 相关的行为问题pnpm-workspace.yaml中相对路径形式的scriptShell现在相对于**工作区根目录workspace root**解析因此从嵌套的工作区子包中运行的脚本能够正确找到该 shell而裸命令名例如bash仍然通过PATH环境变量查找。在此之前如果在 monorepo 中为scriptShell配置了一个相对路径例如./.tools/my-shell从某个嵌套子包目录启动脚本时进程的当前工作目录cwd是子包目录相对路径很可能指向不存在的文件导致生命周期脚本无法启动。修复后相对路径统一以 workspace 根目录为基准进行解析消除了对脚本从哪里触发的依赖。该变更同时影响三个包的原因在于pnpm/config.reader是 pnpm 读取 workspace 清单的 TypeScript 实现pnpm是整体包而pacquet是仓库中独立的 Rust 实现其配置层同样需要保持相同的解析语义。scriptShell 的作用与适用场景scriptShell决定 pnpm 以哪个程序来执行包的scripts字段中的命令。它作用于项目自身的生命周期脚本如pnpm:devPreinstall、postinstall、prepare、build等依赖包的构建脚本dependency build scriptspnpm run系列命令执行的任意 script。从 lifecycle 脚本的端到端测试可以看到测试通过一个探针 shellprobe shell来验证配置scriptShell后pnpm:devPreinstall、postinstall以及依赖包的 preinstall/postinstall 都会在该 shell 下执行——探针把每次收到的脚本写入日志文件然后转交给真正的/bin/sh运行从而确认哪些调用点遵守了该配置。在 Windows 与 Linux 交叉开发的团队中这一配置常用于强制使用特定 shell例如统一使用 bash 执行脚本或为项目定制包装 shell在脚本执行前后注入环境变量、记录调用等。在 pnpm-workspace.yaml 中配置 scriptShell在 pnpm 10 中大部分安装相关配置从.npmrc迁移到了pnpm-workspace.yamlcamelCase 键名scriptShell也在其中。一个最小示例# pnpm-workspace.yaml packages: - packages/* - apps/* scriptShell: /usr/bin/bash配置值支持三种形态配置值含义绝对路径如/usr/bin/bash、C:\tools\bash.exe直接使用该程序相对路径如./.tools/my-shell.sh、.tools/sh相对于 workspace 根目录解析本次变更的核心裸命令名如bash、zsh通过PATH查找三态语义继承、覆盖与清除在 Rust 侧配置结构 workspace_yaml/settings.rs 中script_shell被建模为三态字段/// Tri-state scriptShell from pnpm-workspace.yaml. pnpm reads /// workspace settings into an object and assigns each present key /// onto the merged config, so an explicit scriptShell: null /// clears a value inherited from global config.yaml, while an /// absent key inherits. The extra Option layer preserves that /// distinction ... #[serde(default, deserialize_with deserialize_double_option)] pub script_shell: OptionOptionString,这里的双层Option语义非常关键键缺失外层None不改变任何已有配置继承全局config.yaml即~/.config/pnpm/config.yaml或默认值显式赋值内层Some(value)覆盖继承值显式置空scriptShell: null内层None清除从全局配置文件继承的值回退到平台默认 shell。对应的单元测试位于 workspace_yaml/tests/runtime.rs它验证了从 YAML 解析scriptShell: /usr/bin/bash后能正确写入Configlet yaml r scriptShell: /usr/bin/bash nodeOptions: --max-old-space-size4096 ; let settings: WorkspaceSettings serde_saphyr::from_str(yaml).unwrap(); assert_eq!(settings.script_shell, Some(Some(/usr/bin/bash.to_string()))); let mut config Config::new(); settings.apply_to(mut config, Path::new(/irrelevant)); assert_eq!(config.script_shell.as_deref(), Some(/usr/bin/bash));核心变更相对路径如何解析TypeScript 侧实现pnpm 本体读取 workspace 清单的逻辑位于 config/reader/src/getOptionsFromRootManifest.ts。关键代码在resolveScriptShell函数第 119-123 行function resolveScriptShell (manifestDir: string, scriptShell: string): string { if (path.isAbsolute(scriptShell) || (!scriptShell.includes(/) !scriptShell.includes(\\))) { return scriptShell } return path.join(manifestDir, scriptShell) }解析规则的完整逻辑绝对路径path.isAbsolute(scriptShell)为真时原样返回不做任何变换裸命令名路径中既不包含/也不包含\如bash、zsh、cmd说明是打算从PATH查找的可执行命令同样原样返回——这正是 changeset 中bare command name such asbashis still looked up onPATH所对应的行为相对路径如./.tools/my-shell或.tools/my-shell与manifestDir即 workspace 根目录拼接为绝对路径。调用时机在第 81-83 行if (manifestDir ! null settings.scriptShell ! null) { settings.scriptShell resolveScriptShell(manifestDir, settings.scriptShell) }注意这里manifestDir是 workspace 清单所在目录workspace 根因此无论后续从哪个嵌套子包触发脚本最终拿到的都是一个以 workspace 根为基准的绝对路径不会再受 cwd 影响。顺带一提同一文件中patchedDependencies的相对路径也采用了类似策略path.join(manifestDir, patchFile)说明workspace 内的路径型配置以 workspace 根为基准解析是该代码库的一贯约定。Rust 侧pacquet的解析与应用仓库中的 Rust 实现pacquet同样需要遵守一致的语义。在 workspace_yaml/apply.rs 中可以看到大多数路径型设置storeDir、lockfileDir、globalVirtualStoreDir、cacheDir等在apply_to中会通过resolve(base_dir, v)锚定到 workspace 根目录但文档注释明确说明scriptShell是个例外Path-valued settings are resolved againstbase_dirif relative — anchored at the workspace root where the yaml was found, matching pnpm.scriptShellis the exception; see [Self::resolve_script_shell].scriptShell走的是apply_process_settings直接覆盖到Configpub(super) fn apply_process_settings(mut self, config: mut Config) { ... overlay(mut config.scripts_prepend_node_path, self.scripts_prepend_node_path.take()); overlay(mut config.script_shell, self.script_shell.take()); overlay(mut config.node_options, self.node_options.take()); ... }即 Rust 侧只负责把值原样搬进配置路径解析与执行逻辑则在 executor 层完成。select_shellshell 的最终选择真正把scriptShell变成可执行程序的是 executor/src/shell.rs 中的select_shell函数。其选择逻辑为pub fn select_shell( script_shell: OptionPath, is_windows: bool, ) - ResultSelectedShell, ScriptShellError { // Windows 上拒绝 .bat/.cmd安全原因见下文 if is_windows let Some(p) script_shell is_windows_batch_file(p) { return Err(ScriptShellError::BatchFileOnWindows { ... }); } // 配置了 scriptShell使用它并附带 -c 参数 if let Some(p) script_shell { return Ok(SelectedShell { program: p.to_path_buf(), args: vec![OsString::from(-c)], windows_verbatim_args: false, }); } // Windows 默认cmd /d /s /cComSpec 或 cmd if is_windows { let comspec env::var_os(ComSpec)...; return Ok(SelectedShell { program: comspec, args: vec![/d, /s, /c], ... }); } // POSIX 默认sh -c Ok(SelectedShell { program: PathBuf::from(sh), args: vec![-c], ... }) }总结为下表场景程序参数配置了scriptShell任意平台配置值绝对路径或解析后的路径-cWindows 且未配置%ComSpec%回退cmd/d /s /cPOSIX 且未配置sh-cSelectedShell还带一个windows_verbatim_args标志Windows 上默认cmd走原样参数对应 Node 的windowsVerbatimArguments行为而自定义 shell 与 POSIX 路径则为false。对应的单元测试在 executor/src/shell/tests.rs 中完整覆盖了上述分支包括自定义 shell 在双平台都生效#[test] fn custom_script_shell_wins_on_both_platforms() { let custom Path::new(/usr/local/bin/bash); for is_windows in [false, true] { let shell select_shell(Some(custom), is_windows).expect(select_shell); assert_eq!( shell, SelectedShell { program: custom.to_path_buf(), args: vec![os(-c)], windows_verbatim_args: false, }, ); } }选择时机在触碰文件系统之前在 executor/src/lifecycle.rs 的run_lifecycle_hook中shell 的挑选发生在构建生命周期环境与 PATH 之后、真正 spawn 之前并特意放在最前面// Pick the shell up front so a misconfigured scriptShell fails // before we touch the filesystem (TMPDIR etc. already created // above — thats a minor leak, but the env is built before the // shell pick anyway). The pick also runs when the emulator will // take over below, because pnpm rejects a .bat / .cmd // scriptShell regardless of shellEmulator. let shell select_shell(opts.execution.shell, cfg!(windows)) .map_err(|source| LifecycleScriptError::ScriptShell { ... })?;两个值得注意的点尽早失败如果scriptShell配置非法例如 Windows 上指向.bat会立刻报错而不是等到文件系统副作用发生后与 shellEmulator 的交互即使启用了shellEmulatorRust 内置的 shell 模拟器shell 选择仍然执行——因为对.bat/.cmd的拒绝是无条件生效的。同样的选择逻辑也出现在 executor/src/run_script.rs覆盖pnpm run执行脚本的路径。Windows 安全限制为何拒绝 .bat / .cmdselect_shell在 Windows 上对以.bat/.cmd大小写不敏感结尾的scriptShell直接报错错误类型为ScriptShellError::BatchFileOnWindows。其错误信息与原因注释如下/// Setting scriptShell to a .bat or .cmd file on Windows is /// blocked because Node refuses to spawn batch files without /// shell: true, and re-escaping arguments for a shell-wrapped /// batch invocation is unsafe (cf. CVE-2024-27980 / CVE-2024-24576).背景是 Node 在未开启shell: true时无法直接 spawn 批处理文件而如果用 shell 包裹批处理再重转义参数又会引入命令注入风险对应 CVE-2024-27980 / CVE-2024-24576 这类批处理参数转义漏洞。因此实现选择直接拒绝并提示用户改用.exeCannot spawn .bat or .cmd as a script shell. The pnpm-workspace.yaml scriptShell option was configured to a .bat or .cmd file. These cannot be used as a script shell reliably. Please unset the scriptShell option, or configure it to a .exe instead.注意该检查只在 Windows 上生效——测试 batch_file_script_shell_allowed_on_posix 验证了 POSIX 平台会放行名为weird.cmd的路径反正也执行不了但这不属于该守卫的职责范围。配置变更的增量处理在 CLI 的 config-deps 处理逻辑 cli/src/config_deps/hooks.rs 中还处理了scriptShell被删除的增量场景如果外部如 pnpmfile 钩子把scriptShell键从配置中删除即使整个 delta 对象为空也要把config.script_shell清为None确保删除操作真正生效而非被忽略。实战建议与注意事项基于上述实现细节给出几条可落地的使用建议monorepo 中优先使用相对路径且不依赖 cwd本次修复后scriptShell: ./.tools/my-shell.sh会相对 workspace 根解析从任意子包运行脚本行为一致。但要注意这类相对路径解析只发生在 pnpm-workspace.yaml 的 workspace 配置层如果通过环境变量或全局 config.yaml 提供相对路径请确认其基准目录语义再使用。裸命令名依赖 PATH希望跟随用户环境动态选择 shell 时直接写scriptShell: bash或zsh即可pnpm 会在PATH中查找并跳过相对路径解析。跨平台脚本建议配置绝对路径或命令名Windows 上scriptShell不能指向.bat/.cmd请指向.exe如 Git Bash 的bash.exe同时在 Windows 上配置自定义 shell 时其参数统一为-c与 POSIX 保持一致。利用三态语义管理全局与项目配置在~/.config/pnpm/config.yaml中统一设置团队默认 shell在特定项目的pnpm-workspace.yaml中用scriptShell: null显式回退到平台默认。调试技巧参考端到端测试 script_shell.rs 的做法用一个探针 shell记录输入后转交真实 shell即可确认 pnpm 的每个脚本入口是否都遵守了你的scriptShell配置。小结scriptShell的路径解析看似小事却直接影响 monorepo 中所有生命周期脚本的执行环境。本次curly-shells-resolve变更统一了相对路径以 workspace 根为基准、裸命令名走 PATH的语义并在 TypeScriptpnpm/config.reader与 Rustpacquet两条实现线上保持一致配合 Windows 平台对批处理文件的安全拒绝、与 shellEmulator 的交互设计以及完备的单元与端到端测试构成了一个清晰、可验证的配置行为闭环。理解这条链路能帮助你在复杂工作区中可靠地定制脚本执行环境也能在排查脚本为什么没跑起来时快速定位到配置层的问题。【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表