目录

搭建node脚手架一

搭建node脚手架(一)

核心代码(init.ts)

🧩 代码流程解析

项目初始化配置

const cwd = options.cwd || process.cwd(); // 获取当前工作目录
const isTest = process.env.NODE_ENV === 'test'; // 检测是否为测试环境
const checkVersionUpdate = options.checkVersionUpdate || false; // 版本检查开关
const disableNpmInstall = options.disableNpmInstall || false; // 禁用依赖安装开关
const pkgPath = path.resolve(cwd, 'package.json');
let pkg: PKG = fs.readJSONSync(pkgPath);

🔹 功能:获取项目路径、运行环境设置,并解析package.json文件

工具版本检查(可选)

if (!isTest && checkVersionUpdate) {
  await update(false);
}

🔹 当非测试环境且开启版本检查时,自动检测CLI工具更新

交互式配置收集

  • 默认启用ESLint
  • 选择ESLint类型(JS/TS + React/Vue)
  • 交互式确认是否启用Stylelint、Markdownlint、Prettier

🔹 通过inquirer.prompt实现用户交互配置

依赖处理

pkg = await conflictResolve(cwd, options.rewriteConfig);
spawn.sync(npm, ['i', '-D', PKG_NAME], { stdio: 'inherit', cwd });

🔹 解决依赖冲突后,安装当前工具的开发依赖

脚本命令配置

pkg.scripts[`${PKG_NAME}-scan`] = `${PKG_NAME} scan`;
pkg.scripts[`${PKG_NAME}-fix`] = `${PKG_NAME} fix`;

🔹 在package.json中添加扫描和修复命令

Git Hooks配置

pkg.husky.hooks['pre-commit'] = `${PKG_NAME} commit-file-scan`;
pkg.husky.hooks['commit-msg'] = `${PKG_NAME} commit-msg-scan`;

🔹 配置提交前和提交信息时的自动化检查

配置文件生成

generateTemplate(cwd, config);

🔹 根据用户选择生成对应的配置文件模板

完成提示

log.success(`${PKG_NAME} 初始化完成 :D`);

⚙️ 典型应用场景

执行 my-cli init 命令即可实现:
✅ 全套Lint工具配置
✅ 依赖自动安装
✅ package.json脚本自动配置
✅ Git hooks自动化配置
✅ 配置文件模板生成

流程图

https://i-blog.csdnimg.cn/direct/5768464ddd984d009c8d1b0f5b0aad24.png

import path from 'path';
import fs from 'fs-extra';
import inquirer from 'inquirer';
import spawn from 'cross-spawn';
import update from './update';
import npmType from '../utils/npm-type';
import log from '../utils/log';
import conflictResolve from '../utils/conflict-resolve';
import generateTemplate from '../utils/generate-template';
import { PROJECT_TYPES, PKG_NAME } from '../utils/constants';
import type { InitOptions, PKG } from '../types';

let step = 0;

/**
 * 选择项目语言和框架
 */
const chooseEslintType = async (): Promise<string> => {
  const { type } = await inquirer.prompt({
    type: 'list',
    name: 'type',
    message: `Step ${++step}. 请选择项目的语言(JS/TS)和框架(React/Vue)类型:`,
    choices: PROJECT_TYPES,
  });

  return type;
};

/**
 * 选择是否启用 stylelint
 * @param defaultValue
 */
const chooseEnableStylelint = async (defaultValue: boolean): Promise<boolean> => {
  const { enable } = await inquirer.prompt({
    type: 'confirm',
    name: 'enable',
    message: `Step ${++step}. 是否需要使用 stylelint(若没有样式文件则不需要):`,
    default: defaultValue,
  });

  return enable;
};

/**
 * 选择是否启用 markdownlint
 */
const chooseEnableMarkdownLint = async (): Promise<boolean> => {
  const { enable } = await inquirer.prompt({
    type: 'confirm',
    name: 'enable',
    message: `Step ${++step}. 是否需要使用 markdownlint(若没有 Markdown 文件则不需要):`,
    default: true,
  });

  return enable;
};

/**
 * 选择是否启用 prettier
 */
const chooseEnablePrettier = async (): Promise<boolean> => {
  const { enable } = await inquirer.prompt({
    type: 'confirm',
    name: 'enable',
    message: `Step ${++step}. 是否需要使用 Prettier 格式化代码:`,
    default: true,
  });

  return enable;
};

export default async (options: InitOptions) => {
  const cwd = options.cwd || process.cwd();
  const isTest = process.env.NODE_ENV === 'test';
  const checkVersionUpdate = options.checkVersionUpdate || false;
  const disableNpmInstall = options.disableNpmInstall || false;
  const config: Record<string, any> = {};
  const pkgPath = path.resolve(cwd, 'package.json');
  let pkg: PKG = fs.readJSONSync(pkgPath);

  // 版本检查
  if (!isTest && checkVersionUpdate) {
    await update(false);
  }

  // 初始化 `enableESLint`,默认为 true,无需让用户选择
  if (typeof options.enableESLint === 'boolean') {
    config.enableESLint = options.enableESLint;
  } else {
    config.enableESLint = true;
  }

  // 初始化 `eslintType`
  if (options.eslintType && PROJECT_TYPES.find((choice) => choice.value === options.eslintType)) {
    config.eslintType = options.eslintType;
  } else {
    config.eslintType = await chooseEslintType();
  }

  // 初始化 `enableStylelint`
  if (typeof options.enableStylelint === 'boolean') {
    config.enableStylelint = options.enableStylelint;
  } else {
    config.enableStylelint = await chooseEnableStylelint(!/node/.test(config.eslintType));
  }

  // 初始化 `enableMarkdownlint`
  if (typeof options.enableMarkdownlint === 'boolean') {
    config.enableMarkdownlint = options.enableMarkdownlint;
  } else {
    config.enableMarkdownlint = await chooseEnableMarkdownLint();
  }

  // 初始化 `enablePrettier`
  if (typeof options.enablePrettier === 'boolean') {
    config.enablePrettier = options.enablePrettier;
  } else {
    config.enablePrettier = await chooseEnablePrettier();
  }

  if (!isTest) {
    log.info(`Step ${++step}. 检查并处理项目中可能存在的依赖和配置冲突`);
    pkg = await conflictResolve(cwd, options.rewriteConfig);
    log.success(`Step ${step}. 已完成项目依赖和配置冲突检查处理 :D`);

    if (!disableNpmInstall) {
      log.info(`Step ${++step}. 安装依赖`);
      const npm = await npmType;
      spawn.sync(npm, ['i', '-D', PKG_NAME], { stdio: 'inherit', cwd });
      log.success(`Step ${step}. 安装依赖成功 :D`);
    }
  }

  // 更新 pkg.json
  pkg = fs.readJSONSync(pkgPath);
  // 在 `package.json` 中写入 `scripts`
  if (!pkg.scripts) {
    pkg.scripts = {};
  }
  if (!pkg.scripts[`${PKG_NAME}-scan`]) {
    pkg.scripts[`${PKG_NAME}-scan`] = `${PKG_NAME} scan`;
  }
  if (!pkg.scripts[`${PKG_NAME}-fix`]) {
    pkg.scripts[`${PKG_NAME}-fix`] = `${PKG_NAME} fix`;
  }

  // 配置 commit 卡点
  log.info(`Step ${++step}. 配置 git commit 卡点`);
  if (!pkg.husky) pkg.husky = {};
  if (!pkg.husky.hooks) pkg.husky.hooks = {};
  pkg.husky.hooks['pre-commit'] = `${PKG_NAME} commit-file-scan`;
  pkg.husky.hooks['commit-msg'] = `${PKG_NAME} commit-msg-scan`;
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
  log.success(`Step ${step}. 配置 git commit 卡点成功 :D`);

  log.info(`Step ${++step}. 写入配置文件`);
  generateTemplate(cwd, config);
  log.success(`Step ${step}. 写入配置文件成功 :D`);

  // 完成信息
  const logs = [`${PKG_NAME} 初始化完成 :D`].join('\r\n');
  log.success(logs);
};

检查和处理包的版本更新(update.ts)

完整流程

版本检查 → 提示展示 → 执行更新

  • 主逻辑封装:
// 依赖引入
import { execSync } from 'child_process';
import ora from 'ora';
import log from '../utils/log';
import npmType from '../utils/npm-type';
import { PKG_NAME, PKG_VERSION } from '../utils/constants';

/**
 * 检查最新版本
 * @returns Promise<string|null> 返回最新版本号或null
 */
const checkLatestVersion = async (): Promise<string | null> => {
  const npm = await npmType;
  const latestVersion = execSync(`${npm} view ${PKG_NAME} version`)
    .toString('utf-8')
    .trim();

  if (PKG_VERSION === latestVersion) return null;

  const [currentVersions, latestVersions] = [
    PKG_VERSION,
    latestVersion
  ].map(v => v.split('.').map(Number));

  for (let i = 0; i < currentVersions.length; i++) {
    if (currentVersions[i] > latestVersions[i]) return null;
    if (currentVersions[i] < latestVersions[i]) return latestVersion;
  }
};

/**
 * 版本检查与更新入口
 * @param install 是否自动安装更新,默认true
 */
export default async (install = true) => {
  const spinner = ora(`[${PKG_NAME}] 版本检查中...`).start();

  try {
    const npm = await npmType;
    const latestVersion = await checkLatestVersion();
    spinner.stop();

    if (!latestVersion && install) {
      log.info(`当前没有可用的更新`);
      return;
    }

    if (latestVersion && install) {
      const updateSpinner = ora(`[${PKG_NAME}] 升级至 ${latestVersion}...`).start();
      execSync(`${npm} i -g ${PKG_NAME}`);
      updateSpinner.stop();
    } else if (latestVersion) {
      log.warn(
        `发现新版本 ${latestVersion} (当前 ${PKG_VERSION})\n` +
        `升级命令: ${npm} install -g ${PKG_NAME}@latest\n`
      );
    }
  } catch (e) {
    spinner.stop();
    log.error(e);
  }
};

功能概览

版本检测机制

  • 通过执行 npm view <pkg-name> version 命令获取远程最新版本号
  • 自动比对本地 PKG_VERSION,若发现更新则返回新版本号,否则返回 null

智能更新选项

  • 当 install=true 时:自动执行 npm i -g <pkg-name> 升级到最新版
  • 当 install=false 时:仅显示更新提示和升级命令
  • 当前为最新版本时显示"没有可用更新"提示

交互体验优化

  • 集成 ora 实现命令行动态加载指示器(检查/更新状态)
  • 采用统一日志工具管理 info/warn/error 输出

核心函数

checkLatestVersion()

  • 自动识别当前包管理器(npm/yarn/pnpm)
  • 执行版本查询并返回远程最新版本
  • 实现语义化版本号比较(Major.Minor.Patch)

package.json 配置讲解

对应 package.json 配置:

"scripts": {
  "dev": "npm run copyfiles && tsc -w",
  "build": "rm -rf lib && npm run copyfiles && tsc",
  "copyfiles": "copyfiles -a -u 1 \"src/config/**\" lib",
  "test": "npm run build && jest",
  "coverage": "nyc jest --silent --forceExit",
  "prepublishOnly": "npm run test"
}

🔹 1. “dev”: “npm run copyfiles && tsc -w”

开发模式启动脚本

顺序执行两个命令:

  1. npm run copyfiles - 将 src/config 目录下的配置文件复制到 lib 目录
  2. tsc -w - 启动 TypeScript 编译器监听模式,实时将 .ts 文件编译至 lib 目录

用途:本地开发时同步监听文件变更并确保配置文件同步更新

🔹 2. “build”: “rm -rf lib && npm run copyfiles && tsc”

完整构建流程

执行步骤:

  1. rm -rf lib - 清除旧的 lib 目录(Linux/Mac 命令,Windows 需使用 rimraf)
  2. npm run copyfiles - 复制 src/config 下的文件到 lib
  3. tsc - 编译 TypeScript 源码到 lib 目录

用途:生成干净的 lib 目录,包含编译后的 JS 和配置文件

🔹 3. “copyfiles”: “copyfiles -a -u 1 “src/config/**” lib”

配置文件复制脚本

参数说明:

  • -a:保留文件属性(时间戳、权限等)
  • -u 1:移除路径第一层(src/),确保文件复制到 lib/config/ 而非 lib/src/config/

用途:解决 TypeScript 不处理非 .ts 文件(如 JSON 等)的问题

🔹 4. “test”: “npm run build && jest”

测试流程

执行顺序:

  1. npm run build - 完整构建
  2. jest - 运行单元测试

用途:确保测试基于最新编译代码

🔹 5. “coverage”: “nyc jest –silent –forceExit”

测试覆盖率检查

参数说明:

  • nyc:基于 Istanbul 的覆盖率工具
  • --silent:抑制 console.log 输出
  • --forceExit:测试完成后强制退出进程

用途:生成代码覆盖率报告

🔹 6. “prepublishOnly”: “npm run test”

npm 发布前钩子

用途:确保只有通过测试的代码才能发布到 npm

🔑 脚本总结

这套脚本构建了一个完整的 TypeScript + Jest 开发流程:

  • dev:开发实时编译
  • build:完整构建
  • copyfiles:辅助文件处理
  • test:构建+测试
  • coverage:覆盖率检查
  • prepublishOnly:发布质量保障