ARTICLE DETAIL

资讯详情

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

Backstage @backstage/cli-common 公共 API 全面解析:路径解析、子进程运行与安全边界

Backstage @backstage/cli-common 公共 API 全面解析:路径解析、子进程运行与安全边界 Backstage backstage/cli-common 公共 API 全面解析路径解析、子进程运行与安全边界【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage本文以仓库 packages/cli-common/report.api.mdAPI Extractor 生成的公开 API 报告为骨架结合 packages/cli-common 下的 TypeScript 源码与 Jest 测试系统讲解backstage/cli-common这个被backstage/cli、后端与create-app共同依赖的内部工具包它如何在 monorepo 中定位包路径与仓库根目录、如何安全运行子进程并统一处理退出码以及这些 API 的实际调用约定、边界条件与设计意图。读完本文你可以直接在自己的 Backstage 插件或 CLI 工具中正确使用targetPaths、findOwnPaths、run/runOutput/runCheck与isChildPath这套基础设施并理解其背后的实现原理。一、包定位Backstage 内部的“公共工具层”backstage/cli-common在 packages/cli-common/package.json 中的描述是“Common functionality used by cli, backend, and create-app”即它服务于 Backstage 仓库内三个最重要的消费方backstage/cli开发者命令行工具后端构建相关代码backstage/create-app脚手架初始化工具。这是一个典型的“内部包”包自带的 packages/cli-common/README.md 明确说明不要直接安装本包而应通过依赖backstage/cli或backstage/create-app间接获得能力。其 exports 通过 src/index.ts 对外暴露两个入口主入口.与测试工具入口./testUtils见 packages/cli-common/package.json。当前包版本为0.3.0许可证为 Apache-2.0运行依赖仅backstage/errors提供CustomErrorBase与cross-spawn跨平台 spawn 实现。该包的全部公共面由 packages/cli-common/report.api.md 固化为 API 契约1 个常量、3 个函数不含已废弃的findPaths、4 个函数式 API、2 个类型 2 个接口 1 个异常类功能高度收敛为两条主线路径解析与子进程运行。下面分别展开。二、公共 API 总览从 report.api.md 可见包对外暴露的全部符号如下类别符号说明废弃状态常量BACKSTAGE_JSONBackstage 配置文件backstage.json的文件名字符串常量否函数findOwnPaths(searchDir)基于调用代码所在包解析路径返回OwnPaths否函数findPaths(searchDir)旧版“双项目”路径解析返回Paths是deprecated函数isChildPath(base, path)判断path是否为base自身或其子路径否函数run(args, options?)启动子进程并返回可等待退出的句柄否函数runOutput(args, options?)运行命令并返回 stdout 字符串否函数runCheck(args)运行命令返回“是否以 0 退出”的布尔值否类型OwnPaths/TargetPaths两类路径解析结果否类型Paths旧版路径集合own target是deprecated类型ResolveFunc(...paths: string[]) string的路径拼接函数签名否类型RunOnOutput(data: Buffer) void的输出回调签名否接口RunOptions运行选项继承SpawnOptions否接口RunChildProcess扩展ChildProcess增加waitForExit()否类ExitCodeError子进程非零退出时的错误类型携带code否值得注意的公开承诺report.api.md中标注public的符号即稳定契约而findPaths与Paths被显式标记为deprecated——这正是理解该包演进方向的钥匙旧的“own/target 双路径”模型已被拆解为“基于 cwd 的targetPaths 基于包目录的findOwnPaths”两个更清晰的模型见 src/paths.ts 与 src/paths.ts 的 JSDoc 提示Use {link targetPaths} and {link findOwnPaths} instead。三、路径解析 API 详解3.1 统一路径拼接签名ResolveFuncResolveFunc是贯穿整个路径体系的基础类型export type ResolveFunc (...paths: string[]) string;它接受任意数量的路径片段最终基于某个根目录完成拼接内部实现为node:path的resolve。无论OwnPaths还是TargetPaths都会提供两个ResolveFuncresolve相对包/项目目录与resolveRoot相对 monorepo 根目录从而让调用方免于手工拼接__dirname或处理..层级。3.2targetPaths跟随 cwd 的单例targetPaths是包导出的一个惰性初始化单例定义于 src/paths.tsexport const targetPaths: TargetPaths new TargetPathsImpl();它始终以process.cwd()为基准因此“你在哪个目录下执行命令它就解析哪个项目”。其关键行为源码可证惰性解析dir在首次访问dir属性时才通过fs.realpathSync(cwd)计算src/paths.ts避免导入模块即产生副作用cwd 感知缓存dir与rootDir均带缓存但一旦检测到process.cwd()发生变化会立即失效并重新解析src/paths.ts所以 CLI 在切换工作目录后无需重新加载模块Windows 归一化realpathSync结果中的盘符会被统一转为大写replace(/^[a-z]:/, ...)保证跨平台行为一致根目录回退rootDir通过findRootPath向上查找第一个带workspaces字段的package.json若找不到例如在非 monorepo 的独立项目里运行则回退为dir本身src/paths.ts。注释明确说明这是为了“只让真正需要 monorepo 的命令在不在 monorepo 中时崩溃”。典型用法在 CLI 命令中直接import { targetPaths } from backstage/cli-common然后targetPaths.resolve(src, index.ts)得到当前项目下的绝对路径。3.3findOwnPaths基于“调用方所在包”的路径与targetPaths相反findOwnPaths(searchDir)解析的是调用代码自己所在包的位置export function findOwnPaths(searchDir: string): OwnPaths { return OwnPathsImpl.find(searchDir); }其返回的OwnPaths结构与TargetPaths完全一致dir/rootDir/resolve/resolveRoot区别在于基准dir是包含package.json的包根目录rootDir是包含该包的 monorepo 根目录。实现要点向上探测包根OwnPathsImpl.findDir从searchDir逐级向上找第一个含package.json的目录循环上限 1000 次防死循环src/paths.ts两级缓存实例缓存#instanceCache按包根去重另有一个跨实例共享的dirCache分层缓存——把searchDir解析到包根的同时会缓存路径上每一个中间目录使同一包内不同子目录的调用共享解析结果src/paths.ts根目录判定rootDir由findOwnRootDir计算它要求包内存在src目录否则视为“在 Backstage 仓库之外使用”并抛错随后用findRootPath找第一个声明了workspaces的package.jsonsrc/paths.ts。典型用法在包内代码例如自定义 CLI 插件里调用findOwnPaths(__dirname)即可获得当前包的根路径进而读取包内静态资源。3.4 根目录查找算法findRootPath无论是targetPaths.rootDir还是findOwnPaths的根解析最终都收敛到内部函数findRootPath(searchDir, filterFunc)src/paths.ts从searchDir开始检查path/package.json是否存在若存在用filterFunc(pkgJsonPath)判定是否为根对 monorepo 而言即JSON.parse后存在非空的workspaces字段否则沿dirname逐级上溯直至文件系统根newPath path或达到 1000 次迭代上限抛Iteration limit reached错误。测试 packages/cli-common/src/paths.test.ts 验证了该函数的三条路径找到根、找不到返回undefined、跳过最近一层后找到更上层根。同时测试还覆盖了workspaces为数组[packages/*]和对象{ packages: [...] }两种配置形态paths.test.ts。3.5 已废弃的findPaths为何被拆解findPaths(searchDir)返回旧的Paths类型同时携带own调用方包与 targetcwd 目标两套路径与四个 resolve 函数。源码显示它只是findOwnPaths与targetPaths的组合转发src/paths.tsexport function findPaths(searchDir: string): Paths { const own findOwnPaths(searchDir); return { get ownDir() { return own.dir; }, get ownRoot() { return own.rootDir; }, get targetDir() { return targetPaths.dir; }, get targetRoot() { return targetPaths.rootDir; }, resolveOwn: own.resolve, resolveOwnRoot: own.resolveRoot, resolveTarget: targetPaths.resolve, resolveTargetRoot: targetPaths.resolveRoot, }; }由于它隐含“own 与 target 同处一个 monorepo”的假设在独立项目中语义含糊因此被标记deprecated。新代码一律使用targetPaths跟 cwd与findOwnPaths跟包二选一这也是阅读本 API 报告时最重要的迁移提示。3.6 安全边界isChildPathisChildPath(base, path)判断path是否等于base或是其子路径是 CLI 处理用户输入路径时的安全检查工具src/isChildPath.ts。其实现有三个层次真实路径解析内部函数resolveRealPath先用realpathSync解析符号链接若目标不存在ENOENT会递归处理“悬空符号链接链”或沿父目录向上找到最近的真实路径后在其上重建不存在的部分src/isChildPath.ts相对性判定对两个真实路径求relative结果为空表示同一目录视为子路径返回true结果以..开头表示逃出base返回falsesrc/isChildPath.ts跨平台判定relative结果若为绝对路径在 Windows 上意味着两个路径位于不同盘符如C:与D:同样返回falsesrc/isChildPath.ts。测试 packages/cli-common/src/isChildPath.test.ts 使用jest.setMock(path, posix/win32)分别验证 POSIX 与 Windows 语义例如/x是/x/y/z的父路径true/x不是/x y的父路径false前缀边界Windows 下C:/x与c:/x大小写不敏感地视为同一路径true而C:/与D:/跨盘必然为 falseisChildPath.test.ts。3.7 测试辅助overrideTargetPaths通过backstage/cli-common/testUtils子路径src/testUtils.ts暴露overrideTargetPaths(dirOrOptions)允许测试中把targetPaths单例指向任意目录const override overrideTargetPaths({ dir: /tmp/fake-project, rootDir: /tmp/fake-monorepo, }); // ... 测试逻辑 ... override.restore();实现上它调用内部函数setTargetPathsOverride替换单例内部状态src/paths.ts。文档注释特别说明每个 Jest worker 的模块状态是干净的因此只有当同一测试文件内需要多次切换 override 时才必须显式调用restore()。这与 paths.test.ts 中通过jest.spyOn(process, cwd)模拟 cwd 的做法互为补充。四、子进程运行 API 详解4.1run启动子进程并管理生命周期run(args, options?)是包内最底层的进程 APIsrc/run.tsexport function run(args: string[], options: RunOptions {}): RunChildProcess { if (args.length 0) { throw new Error(run requires at least one argument); } const [name, ...cmdArgs] args; // ... const child spawn(name, cmdArgs, { ...spawnOptions, stdio, env }) as RunChildProcess; // ... }关键实现事实空参数即抛错run([])直接抛出run requires at least one argument测试 run.test.ts 覆盖跨平台 spawn底层使用cross-spawn而非node:child_process.spawn规避 Windows 上.cmd/.bat文件无法直接 spawn 的兼容性问题默认强制颜色子进程环境变量被合并为{ ...process.env, FORCE_COLOR: true, ...options.env }src/run.ts保证 CLI 输出在管道场景下仍保留 ANSI 颜色测试 run.test.ts 验证子进程内FORCE_COLOR truestdio 自适应默认[inherit, inherit, inherit]一旦提供onStdout/onStderr回调对应通道自动切换为pipe并挂接data事件src/run.ts信号联动waitForExit()等待期间注册SIGINT/SIGTERM监听收到信号且子进程尚未退出时调用child.kill()结束后清理全部监听器不泄漏src/run.ts测试 run.test.ts 验证 SIGINT/SIGTERM 触发 kill、进程已退出后不再误杀waitForExit 幂等内部共享同一个waitPromise重复调用与并发调用都只注册一次监听并返回同一结果src/run.ts测试 run.test.ts 覆盖“进程已退出再次调用”与“三次并发调用”两种场景。RunOptions的类型定义为OmitSpawnOptions, env { env?: PartialNodeJS.ProcessEnv; onStdout?; onStderr?; stdio? }src/run.ts即完整继承node:child_process的SpawnOptions能力如cwd、shell、timeout等并额外提供输出回调。4.2ExitCodeError统一非零退出语义ExitCodeError extends CustomErrorBase继承自backstage/errors持有只读属性code与可选的command上下文src/errors.tsexport class ExitCodeError extends CustomErrorBase { readonly code: number; constructor(code: number, command?: string) { super(command ? Command ${command} exited with code ${code} : Child exited with code ${code}); this.code code; } }当子进程以非零码退出时waitForExit()即 reject 一个ExitCodeError消息形如Command node --eval process.exit(1) exited with code 1测试 run.test.ts。统一异常类型让上层如backstage/cli的各命令可以用instanceof ExitCodeError精确捕获“命令执行失败”并读取退出码而不会与其他错误混淆。4.3runOutput以字符串形式取回 stdoutrunOutput(args, options?)是run的封装运行完成后返回trim 过的 stdout 字符串src/run.ts。其实现值得注意的错误处理设计内部用Buffer分块收集 stdout/stderr即使是大段输出也不会因一次性字符串拼接而破坏多字节字符失败时保留现场若命令非零退出捕获到的错误对象会被动态附加stdout与stderr字符串属性后再抛出src/run.ts调用方可在 catch 中直接读取err.stdout/err.stderr用于诊断仍然支持透传自定义onStdout/onStderr回调先收集、再转发便于在等待结果的同时做流式日志。对应测试验证了正常返回并 trim、失败时错误携带 stdout 与 stderr 内容run.test.ts。4.4runCheck只关心“成不成”runCheck(args)是最轻量的探测型 APIsrc/run.tsexport async function runCheck(args: string[]): Promiseboolean { try { await run(args, { stdio: ignore }).waitForExit(); return true; } catch { return false; } }它用stdio: ignore彻底丢弃子进程输出不会向终端泄漏 stdout/stderr测试 run.test.ts 专门验证了这一点随后把“退出码为 0”映射为true任何失败非零退出、命令不存在都映射为false。非常适合“探测某工具是否可用”“检查某命令是否通过”等存在性/就绪性判断。五、一个完整的使用组合示例综合以上 API一个典型的 CLI 场景可以这样组合示意代码可直接在 Backstage 仓库内基于该包编写import { targetPaths, findOwnPaths, runOutput, runCheck, isChildPath, BACKSTAGE_JSON, } from backstage/cli-common; // 1. 读取当前项目cwd的 backstage 配置文件路径 const configPath targetPaths.resolve(BACKSTAGE_JSON); // 2. 定位“我自己”所在包的静态资源目录 const own findOwnPaths(__dirname); const assetPath own.resolve(assets, logo.svg); // 3. 安全校验用户传入的路径不得逃出目标目录 if (!isChildPath(targetPaths.dir, userProvidedPath)) { throw new Error(Path escapes the project: ${userProvidedPath}); } // 4. 运行命令并拿到 stdout const version await runOutput([node, --version]); console.log(Node:, version); // 5. 探测依赖是否就绪 if (await runCheck([git, --version])) { console.log(git is available); }其中BACKSTAGE_JSON常量的语义在 src/paths.ts 有明确 JSDocThe name of the backstages config file即backstage.json文件名常量被create-app等工具用于写入/校验 Backstage 版本标识。六、设计与使用要点总结两条路径主线按场景选择跟当前工作目录走用targetPaths跟调用方包走用findOwnPaths(__dirname)不要再使用已废弃的findPaths。路径解析有明确的 monorepo 前提rootDir依赖package.json中的workspaces字段findOwnPaths额外要求包内存在src目录。在独立项目中这些 API 会抛错或回退到dir使用时需注意运行环境。进程运行三件套分工清晰run提供底层句柄与完整生命周期控制信号转发、stdio 接管runOutput适合需要结果字符串的命令runCheck适合忽略输出、只判成败的探测。错误语义统一所有非零退出都收敛为ExitCodeError含code与commandrunOutput还会把 stdout/stderr 附加到错误对象上便于上层统一处理与日志。安全边界内置isChildPath处理了符号链接、悬空链接、Windows 盘符等边界情况是处理用户路径输入的推荐防线测试覆盖isChildPath.test.ts、paths.test.ts、run.test.ts可作为行为契约的权威参考。backstage/cli-common虽然体量小源码仅 5 个 TS 文件却是 Backstage CLI、后端与脚手架工具共用的“地基”它用一致的路径语义统一了 monorepo 内的定位方式用统一的子进程封装与异常类型保证了所有命令的错误可观测性。阅读其 API 报告并对照源码是理解整个 Backstage 工具链运行方式的极佳切入点。【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表