发布与扩展(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 beta

1.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-cli

2. 版本管理策略

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=beta

2.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 }}

📝 总结

核心功能

  1. ✅ 发布到 npm 的完整流程
  2. ✅ 版本管理策略
  3. ✅ 用户自定义模板支持
  4. ✅ 配置文件系统
  5. ✅ Mock 服务器集成
  6. ✅ 运行时能力
  7. ✅ 测试和 CI/CD

完整流程

  1. 开发 → 编写代码和测试
  2. 构建 → 编译 TypeScript
  3. 测试 → 运行单元测试
  4. 发布 → 发布到 npm
  5. 使用 → 用户安装和使用

脚手架 前端工程化 npm发布 CD 扩展能力