搭建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自动化配置
✅ 配置文件模板生成
流程图
对应代码:
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.jsonpkg = 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”
开发模式启动脚本
顺序执行两个命令:
npm run copyfiles
- 将 src/config 目录下的配置文件复制到 lib 目录tsc -w
- 启动 TypeScript 编译器监听模式,实时将 .ts 文件编译至 lib 目录
用途:本地开发时同步监听文件变更并确保配置文件同步更新
🔹 2. “build”: “rm -rf lib && npm run copyfiles && tsc”
完整构建流程
执行步骤:
rm -rf lib
- 清除旧的 lib 目录(Linux/Mac 命令,Windows 需使用 rimraf)npm run copyfiles
- 复制 src/config 下的文件到 libtsc
- 编译 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”
测试流程
执行顺序:
npm run build
- 完整构建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:发布质量保障