搭建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自动化配置
✅ 配置文件模板生成
流程图
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”
开发模式启动脚本
顺序执行两个命令:
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:发布质量保障