Turbo Run 工具
概述
turbo-run
是一个为微前端项目定制的交互式命令行工具,用于简化在多包环境中运行 Turbo 命令的过程。
功能特性
- 🔍 智能包检测:自动扫描项目中的所有包,并识别包含指定脚本的包
- 🎯 交互式选择:提供友好的交互界面,让用户选择要运行的具体包
- 🚀 快速执行:直接执行选中包的对应脚本,无需手动输入复杂的过滤参数
- 🎨 美观输出:彩色终端输出,提供清晰的执行状态反馈
使用方法
基本用法
# 运行开发环境
pnpm dev
# 这等同于运行
tsx scripts/turbo-run/index.ts dev
工作流程
- 命令解析:工具解析传入的命令参数(如
dev
) - 包扫描:扫描项目中的所有
package.json
文件 - 脚本筛选:只显示包含指定脚本的包
- 交互选择:
- 如果只有一个包含该脚本,直接执行
- 如果有多个包,显示选择菜单
- 命令执行:运行
pnpm --filter=选中的包 run 命令
项目结构
scripts/turbo-run/
├── index.ts # 入口文件,处理命令行参数
├── run.ts # 核心逻辑,包扫描和执行
├── utils.ts # 工具函数,提供基础功能
└── README.md # 说明文档
文件说明
index.ts
- 命令行接口的入口点
- 使用
cac
库处理命令行参数 - 提供帮助信息和错误处理
run.ts
- 核心业务逻辑
- 包扫描和脚本检测
- 交互式用户选择
- 命令执行
utils.ts
- 基础工具函数
- 颜色输出工具
- 控制台日志工具
- 包信息获取
- 命令执行封装
依赖说明
@clack/prompts
: 提供美观的交互式命令行界面cac
: 命令行参数解析tsx
: TypeScript 文件直接执行glob
: 文件模式匹配,用于查找 package.json 文件
错误处理
工具包含完善的错误处理机制:
- ✅ 参数验证:检查是否提供了有效的命令参数
- ✅ 包检测:验证是否找到了包含指定脚本的包
- ✅ 用户取消:优雅处理用户取消操作
- ✅ 命令执行:捕获并报告命令执行错误
扩展说明
如果需要添加新的功能,可以:
- 在
utils.ts
中添加新的工具函数 - 在
run.ts
中扩展核心逻辑 - 在
index.ts
中添加新的命令行选项
故障排除
常见问题
- 找不到包:确保包的
package.json
中包含要执行的脚本 - 权限错误:确保有足够的权限执行命令
- 依赖缺失:运行
pnpm install
安装所有依赖
调试模式
可以通过修改 utils.ts
中的日志级别来获取更多调试信息。
运行结果
utils.ts
/**
* Turbo Run 工具函数
*
* 这个文件提供了 turbo-run 所需的基本工具函数,
* 替代了原本的 @vben/node-utils 依赖
*/
import { execSync } from 'child_process';
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { glob } from 'glob';
/**
* 颜色工具 - 用于在终端中显示彩色文本
*/
export const colors = {
red: (text: string) => `\x1b[31m${text}\x1b[0m`,
green: (text: string) => `\x1b[32m${text}\x1b[0m`,
yellow: (text: string) => `\x1b[33m${text}\x1b[0m`,
blue: (text: string) => `\x1b[34m${text}\x1b[0m`,
magenta: (text: string) => `\x1b[35m${text}\x1b[0m`,
cyan: (text: string) => `\x1b[36m${text}\x1b[0m`,
white: (text: string) => `\x1b[37m${text}\x1b[0m`,
};
/**
* 控制台日志工具 - 提供格式化的日志输出
*/
export const consola = {
info: (message: any) => console.log(`ℹ ${message}`),
success: (message: any) => console.log(`✅ ${colors.green(message)}`),
warn: (message: any) => console.warn(`⚠️ ${colors.yellow(message)}`),
error: (message: any) => console.error(`❌ ${colors.red(message)}`),
log: (message: any) => console.log(message),
};
/**
* 包信息接口
*/
export interface Package {
packageJson: {
name: string;
scripts?: Record<string, string>;
[key: string]: any;
};
dir: string;
}
/**
* 执行命令的选项
*/
export interface ExecOptions {
stdio?: 'inherit' | 'pipe' | 'ignore';
cwd?: string;
}
/**
* 执行命令函数
* @param command 要执行的命令
* @param options 执行选项
*/
export function execaCommand(command: string, options: ExecOptions = {}) {
try {
consola.info(`执行命令: ${colors.cyan(command)}`);
// 在 Windows 上使用 cmd 执行命令
const isWindows = process.platform === 'win32';
const execCommand = isWindows ? `cmd /c ${command}` : command;
execSync(execCommand, {
stdio: options.stdio || 'inherit',
cwd: options.cwd || process.cwd(),
encoding: 'utf8',
});
} catch (error) {
consola.error(`命令执行失败: ${command}`);
consola.error(error);
process.exit(1);
}
}
/**
* 获取项目中的所有包
* @returns 包含所有包信息的对象
*/
export async function getPackages(): Promise<{ packages: Package[] }> {
const packages: Package[] = [];
const rootDir = process.cwd();
try {
// 查找所有的 package.json 文件
const packageJsonFiles = await glob('**/package.json', {
cwd: rootDir,
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'],
});
for (const packageJsonFile of packageJsonFiles) {
const packageJsonPath = join(rootDir, packageJsonFile);
const packageDir = join(rootDir, packageJsonFile.replace('/package.json', ''));
if (existsSync(packageJsonPath)) {
try {
const packageJsonContent = readFileSync(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonContent);
// 只包含有 scripts 的包
if (packageJson.scripts && Object.keys(packageJson.scripts).length > 0) {
packages.push({
packageJson,
dir: packageDir,
});
}
} catch (error) {
consola.warn(`无法解析 package.json: ${packageJsonPath}`);
}
}
}
consola.info(`找到 ${packages.length} 个包`);
return { packages };
} catch (error) {
consola.error('获取包信息失败');
consola.error(error);
return { packages: [] };
}
}
run.ts
/**
* Turbo Run 核心运行逻辑
*
* 这个文件包含了 turbo-run 工具的核心功能实现:
* 1. 扫描项目中的所有包
* 2. 筛选出包含指定命令的包
* 3. 提供交互式选择界面
* 4. 执行选中包的对应命令
*/
import { execaCommand, getPackages } from './utils';
import { cancel, isCancel, select } from '@clack/prompts';
import { join } from 'path';
/**
* 运行选项接口
*/
interface RunOptions {
command?: string; // 要执行的命令名称,如 'dev', 'build' 等
}
/**
* 主要的运行函数
* @param options 运行选项
*/
export async function run(options: RunOptions) {
const { command } = options;
// 验证命令参数
if (!command) {
console.error('Please enter the command to run');
process.exit(1);
}
// 获取项目中的所有包信息
const { packages } = await getPackages();
// 获取项目根目录
const rootDir = process.cwd();
const packagesDir = join(rootDir, 'packages');
// 筛选出 packages 目录下的包
const packagesOnly = packages.filter((pkg) => {
return pkg.dir.startsWith(packagesDir);
});
// 从 packages 目录下的包中筛选出包含指定命令的包
// 只有在 package.json 的 scripts 中定义了对应命令的包才会被显示
const selectPkgs = packagesOnly.filter((pkg) => {
return (pkg?.packageJson as Record<string, any>)?.scripts?.[command];
});
let selectPkg: string | symbol;
// 如果有多个包含该命令的包,提供选择界面
if (selectPkgs.length > 1) {
selectPkg = await select<string>({
message: `Select the app you need to run [${command}]:`,
options: selectPkgs.map((item) => ({
label: item?.packageJson.name, // 显示包名
value: item?.packageJson.name, // 选择值
})),
});
// 处理用户取消操作
if (isCancel(selectPkg) || !selectPkg) {
cancel('👋 Has cancelled');
process.exit(0);
}
} else {
// 如果只有一个包,直接选择
selectPkg = selectPkgs[0]?.packageJson?.name ?? '';
}
// 验证是否找到了有效的包
if (!selectPkg) {
console.error('No app found');
process.exit(1);
}
// 执行对应的 pnpm 命令
// 使用 --filter 参数指定要运行的包
execaCommand(`pnpm --filter=${selectPkg} run ${command}`, {
stdio: 'inherit', // 继承父进程的标准输入输出,以便看到命令执行结果
});
}
/**
* 过滤app包的函数(已注释)
* 这个函数原本用于筛选特定的应用包,现在暂时不使用
*
* @param root 项目根目录
* @param packages 所有包的列表
*/
// async function findApps(root: string, packages: Package[]) {
// // 筛选 apps 目录内的包
// const appPackages = packages.filter((pkg) => {
// const viteConfigExists = fs.existsSync(join(pkg.dir, 'vite.config.mts'));
// return pkg.dir.startsWith(join(root, 'apps')) && viteConfigExists;
// });
// return appPackages;
// }
index.ts
import { colors, consola } from './utils';
import { cac } from 'cac';
import { run } from './run';
try {
const turboRun = cac('turbo-run');
turboRun
.command('[script]')
.usage(`Run turbo interactively.`)
.action(async (command: string) => {
run({ command });
});
// Invalid command
turboRun.on('command:*', () => {
consola.error(colors.red('Invalid command!'));
process.exit(1);
});
turboRun.usage('turbo-run');
turboRun.help();
turboRun.parse();
} catch (error) {
consola.error(error);
process.exit(1);
}
package.json
"scripts": {
"dev": "tsx scripts/turbo-run/index.ts dev",
"test": "tsx scripts/turbo-run/index.ts test",
}