发布与扩展(Publish & Extend)
发布脚手架到 npm,实现版本管理、用户自定义模板、配置文件系统、Mock 服务器等扩展功能,以及测试和 CI/CD 流程。
📋 目录
1. 发布到 npm
1.1 发布前准备
package.json 配置
{
"name": "my-cli",
"version": "1.0.0",
"description": "前端脚手架工具",
"main": "dist/index.js",
"bin": {
"my-cli": "./bin/cli.js"
},
"files": [
"bin",
"dist",
"templates",
"README.md"
],
"keywords": [
"cli",
"scaffold",
"frontend",
"react",
"vue",
"template"
],
"author": "Your Name",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/your-username/my-cli.git"
},
"bugs": {
"url": "https://github.com/your-username/my-cli/issues"
},
"homepage": "https://github.com/your-username/my-cli#readme",
"engines": {
"node": ">=14.0.0"
}
}.npmignore 文件
# 开发文件
src/
tsconfig.json
*.ts
!*.d.ts
# 测试文件
test/
*.test.ts
*.spec.ts
# 配置文件
.eslintrc.json
.prettierrc.json
.gitignore
# 临时文件
.temp/
node_modules/
dist/*.map
1.2 构建和发布流程
# 1. 构建项目
npm run build
# 2. 运行测试
npm test
# 3. 检查文件
npm pack --dry-run
# 4. 登录 npm
npm login
# 5. 发布
npm publish
# 6. 发布到特定 tag(如 beta)
npm publish --tag beta1.3 发布脚本
{
"scripts": {
"prepublishOnly": "npm run build && npm test",
"version": "npm run build && git add -A dist",
"postversion": "git push && git push --tags"
}
}1.4 使用示例
# 全局安装
npm install -g my-cli
# 使用
my-cli create my-app
# 更新
npm update -g my-cli2. 版本管理策略
2.1 语义化版本
遵循 Semantic Versioning 规范:
- 主版本号(MAJOR):不兼容的 API 修改
- 次版本号(MINOR):向下兼容的功能性新增
- 修订号(PATCH):向下兼容的问题修正
2.2 版本更新命令
# 更新修订号(1.0.0 -> 1.0.1)
npm version patch
# 更新次版本号(1.0.0 -> 1.1.0)
npm version minor
# 更新主版本号(1.0.0 -> 2.0.0)
npm version major
# 预发布版本
npm version prerelease --preid=beta2.3 版本管理脚本
// scripts/version.ts
import { execSync } from 'child_process';
import fs from 'fs-extra';
import path from 'path';
async function updateVersion(type: 'patch' | 'minor' | 'major') {
// 读取当前版本
const packageJsonPath = path.join(process.cwd(), 'package.json');
const packageJson = await fs.readJson(packageJsonPath);
const [major, minor, patch] = packageJson.version.split('.').map(Number);
// 计算新版本
let newVersion: string;
switch (type) {
case 'major':
newVersion = `${major + 1}.0.0`;
break;
case 'minor':
newVersion = `${major}.${minor + 1}.0`;
break;
case 'patch':
newVersion = `${major}.${minor}.${patch + 1}`;
break;
}
// 更新版本
packageJson.version = newVersion;
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
// 构建
execSync('npm run build', { stdio: 'inherit' });
// 提交
execSync(`git add -A`, { stdio: 'inherit' });
execSync(`git commit -m "chore: bump version to ${newVersion}"`, {
stdio: 'inherit'
});
execSync(`git tag v${newVersion}`, { stdio: 'inherit' });
console.log(`Version updated to ${newVersion}`);
}
const type = process.argv[2] as 'patch' | 'minor' | 'major';
updateVersion(type);3. 用户自定义模板支持
3.1 模板注册命令
// src/commands/template.ts
import { Command } from 'commander';
import path from 'path';
import fs from 'fs-extra';
export function registerTemplateCommand(program: Command) {
const templateCommand = program
.command('template')
.description('管理自定义模板');
// 添加模板
templateCommand
.command('add')
.description('添加自定义模板')
.argument('<name>', '模板名称')
.argument('<path>', '模板路径')
.action(async (name, templatePath) => {
await addTemplate(name, templatePath);
});
// 列出模板
templateCommand
.command('list')
.description('列出所有自定义模板')
.action(async () => {
await listTemplates();
});
// 删除模板
templateCommand
.command('remove')
.description('删除自定义模板')
.argument('<name>', '模板名称')
.action(async (name) => {
await removeTemplate(name);
});
}
async function addTemplate(name: string, templatePath: string) {
const configDir = path.join(require('os').homedir(), '.my-cli', 'templates');
await fs.ensureDir(configDir);
const targetPath = path.join(configDir, name);
const sourcePath = path.resolve(templatePath);
if (!(await fs.pathExists(sourcePath))) {
throw new Error(`Template path not found: ${templatePath}`);
}
// 复制模板
await fs.copy(sourcePath, targetPath);
// 保存配置
const configPath = path.join(configDir, 'config.json');
let config: Record<string, string> = {};
if (await fs.pathExists(configPath)) {
config = await fs.readJson(configPath);
}
config[name] = targetPath;
await fs.writeJson(configPath, config, { spaces: 2 });
console.log(`Template "${name}" added successfully`);
}
async function listTemplates() {
const configDir = path.join(require('os').homedir(), '.my-cli', 'templates');
const configPath = path.join(configDir, 'config.json');
if (!(await fs.pathExists(configPath))) {
console.log('No custom templates found');
return;
}
const config = await fs.readJson(configPath);
console.log('\nCustom Templates:');
Object.keys(config).forEach((name) => {
console.log(` - ${name}: ${config[name]}`);
});
}
async function removeTemplate(name: string) {
const configDir = path.join(require('os').homedir(), '.my-cli', 'templates');
const configPath = path.join(configDir, 'config.json');
if (!(await fs.pathExists(configPath))) {
throw new Error('No templates found');
}
const config = await fs.readJson(configPath);
if (!config[name]) {
throw new Error(`Template "${name}" not found`);
}
// 删除模板目录
const templatePath = config[name];
if (await fs.pathExists(templatePath)) {
await fs.remove(templatePath);
}
// 更新配置
delete config[name];
await fs.writeJson(configPath, config, { spaces: 2 });
console.log(`Template "${name}" removed successfully`);
}3.2 使用自定义模板
// 在创建命令中支持自定义模板
const customTemplates = await loadCustomTemplates();
const allTemplates = [...builtinTemplates, ...customTemplates];
// 用户可以选择自定义模板
const { template } = await inquirer.prompt([
{
type: 'list',
name: 'template',
message: '选择模板:',
choices: allTemplates
}
]);4. 配置文件系统
4.1 配置文件格式
// .scaffoldrc.json 或 .my-clirc.json
{
"templates": {
"custom-react": "~/.my-cli/templates/custom-react",
"company-template": "github:company/templates/react"
},
"defaults": {
"packageManager": "pnpm",
"installDeps": true,
"initGit": true,
"features": ["eslint", "prettier"]
},
"plugins": [
"my-cli-eslint-plugin",
"my-cli-prettier-plugin"
]
}4.2 配置管理器
// src/utils/config.ts
import path from 'path';
import fs from 'fs-extra';
export interface ScaffoldConfig {
templates?: Record<string, string>;
defaults?: {
packageManager?: 'npm' | 'yarn' | 'pnpm';
installDeps?: boolean;
initGit?: boolean;
features?: string[];
};
plugins?: string[];
}
export class ConfigManager {
private configPath: string;
constructor() {
// 支持多个配置文件位置
const homeDir = require('os').homedir();
this.configPath = path.join(homeDir, '.my-cli', 'config.json');
}
// 加载配置
async loadConfig(): Promise<ScaffoldConfig> {
if (await fs.pathExists(this.configPath)) {
return await fs.readJson(this.configPath);
}
// 返回默认配置
return {
defaults: {
packageManager: 'npm',
installDeps: true,
initGit: true,
features: []
}
};
}
// 保存配置
async saveConfig(config: ScaffoldConfig): Promise<void> {
await fs.ensureDir(path.dirname(this.configPath));
await fs.writeJson(this.configPath, config, { spaces: 2 });
}
// 合并配置
mergeConfig(
base: ScaffoldConfig,
override: ScaffoldConfig
): ScaffoldConfig {
return {
templates: { ...base.templates, ...override.templates },
defaults: { ...base.defaults, ...override.defaults },
plugins: [...(base.plugins || []), ...(override.plugins || [])]
};
}
}4.3 使用配置
// 在创建命令中使用配置
const configManager = new ConfigManager();
const config = await configManager.loadConfig();
// 合并命令行参数和配置
const finalOptions = configManager.mergeConfig(
{
defaults: config.defaults || {}
},
options
);5. Mock 服务器集成
5.1 Mock 服务器插件
// plugins/mock-server-plugin/index.ts
import { Plugin } from '../../src/core/plugin';
import { HookRegistry } from '../../src/core/hooks';
import { LifecycleHooks } from '../../src/core/lifecycle';
import express from 'express';
import { createServer } from 'http';
const mockServerPlugin: Plugin = {
name: 'mock-server-plugin',
version: '1.0.0',
description: '添加 Mock 服务器',
install(registry: HookRegistry, options?: any) {
registry.register(
LifecycleHooks.AFTER_CREATE,
async (targetDir: string, createOptions: any) => {
if (createOptions.features?.includes('mock-server')) {
await addMockServer(targetDir, createOptions);
}
},
50
);
}
};
async function addMockServer(targetDir: string, options: any) {
// 创建 mock 目录
const mockDir = path.join(targetDir, 'mock');
await fs.ensureDir(mockDir);
// 创建 mock 服务器文件
const mockServerCode = `
import express from 'express';
const app = express();
app.use(express.json());
// Mock API
app.get('/api/users', (req, res) => {
res.json([
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' }
]);
});
app.listen(3001, () => {
console.log('Mock server running on http://localhost:3001');
});
`;
await fs.writeFile(
path.join(mockDir, 'server.js'),
mockServerCode
);
// 更新 package.json
const packageJsonPath = path.join(targetDir, 'package.json');
const packageJson = await fs.readJson(packageJsonPath);
packageJson.devDependencies = {
...packageJson.devDependencies,
express: '^4.18.0'
};
packageJson.scripts = {
...packageJson.scripts,
'mock:server': 'node mock/server.js'
};
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
}
export default mockServerPlugin;6. 运行时能力
6.1 开发服务器集成
// src/commands/dev.ts
import { Command } from 'commander';
import { execSync } from 'child_process';
export function registerDevCommand(program: Command) {
program
.command('dev')
.description('启动开发服务器')
.option('-p, --port <port>', '端口号', '3000')
.option('--mock', '启动 Mock 服务器')
.action(async (options) => {
if (options.mock) {
// 启动 Mock 服务器
execSync('npm run mock:server', {
stdio: 'inherit',
cwd: process.cwd()
});
}
// 启动开发服务器
execSync('npm run dev', {
stdio: 'inherit',
cwd: process.cwd(),
env: {
...process.env,
PORT: options.port
}
});
});
}6.2 中间件系统
// src/core/middleware.ts
export type Middleware = (
req: any,
res: any,
next: () => void
) => void | Promise<void>;
export class MiddlewareManager {
private middlewares: Middleware[] = [];
use(middleware: Middleware): void {
this.middlewares.push(middleware);
}
async execute(req: any, res: any): Promise<void> {
let index = 0;
const next = async () => {
if (index < this.middlewares.length) {
const middleware = this.middlewares[index++];
await middleware(req, res, next);
}
};
await next();
}
}7. 测试和 CI/CD
7.1 单元测试
// test/utils/file.test.ts
import { describe, it, expect } from '@jest/globals';
import { createProject } from '../../src/utils/file';
import fs from 'fs-extra';
import path from 'path';
describe('createProject', () => {
it('should create project directory', async () => {
const projectName = 'test-project';
const targetDir = path.join(process.cwd(), projectName);
// 清理
if (await fs.pathExists(targetDir)) {
await fs.remove(targetDir);
}
await createProject(projectName, {
template: 'basic-js'
});
expect(await fs.pathExists(targetDir)).toBe(true);
expect(await fs.pathExists(path.join(targetDir, 'package.json'))).toBe(
true
);
// 清理
await fs.remove(targetDir);
});
});7.2 CI/CD 配置
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm run build
- run: npm test
publish:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}📝 总结
核心功能
- ✅ 发布到 npm 的完整流程
- ✅ 版本管理策略
- ✅ 用户自定义模板支持
- ✅ 配置文件系统
- ✅ Mock 服务器集成
- ✅ 运行时能力
- ✅ 测试和 CI/CD
完整流程
- 开发 → 编写代码和测试
- 构建 → 编译 TypeScript
- 测试 → 运行单元测试
- 发布 → 发布到 npm
- 使用 → 用户安装和使用