turbo-monorepo中自定义脚本运行项目下的包

发布于:2025-07-20 ⋅ 阅读:(18) ⋅ 点赞:(0)

Turbo Run 工具

概述

turbo-run 是一个为微前端项目定制的交互式命令行工具,用于简化在多包环境中运行 Turbo 命令的过程。

功能特性

  • 🔍 智能包检测:自动扫描项目中的所有包,并识别包含指定脚本的包
  • 🎯 交互式选择:提供友好的交互界面,让用户选择要运行的具体包
  • 🚀 快速执行:直接执行选中包的对应脚本,无需手动输入复杂的过滤参数
  • 🎨 美观输出:彩色终端输出,提供清晰的执行状态反馈

使用方法

基本用法

# 运行开发环境
pnpm dev

# 这等同于运行
tsx scripts/turbo-run/index.ts dev

工作流程

  1. 命令解析:工具解析传入的命令参数(如 dev
  2. 包扫描:扫描项目中的所有 package.json 文件
  3. 脚本筛选:只显示包含指定脚本的包
  4. 交互选择
    • 如果只有一个包含该脚本,直接执行
    • 如果有多个包,显示选择菜单
  5. 命令执行:运行 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 文件

错误处理

工具包含完善的错误处理机制:

  • ✅ 参数验证:检查是否提供了有效的命令参数
  • ✅ 包检测:验证是否找到了包含指定脚本的包
  • ✅ 用户取消:优雅处理用户取消操作
  • ✅ 命令执行:捕获并报告命令执行错误

扩展说明

如果需要添加新的功能,可以:

  1. utils.ts 中添加新的工具函数
  2. run.ts 中扩展核心逻辑
  3. index.ts 中添加新的命令行选项

故障排除

常见问题

  1. 找不到包:确保包的 package.json 中包含要执行的脚本
  2. 权限错误:确保有足够的权限执行命令
  3. 依赖缺失:运行 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",
  }

网站公告

今日签到

点亮在社区的每一天
去签到