🧑💻 写在开头
点赞 + 收藏 === 学会🤣🤣🤣
在日常工作中,我们经常为会遇到需要创建新项目的需求,为了统计代码风格,项目配置,提升效率,我们可以创建一个cli工具,帮助我们实现这样的功能。你也可以搭建一个自己用,毕竟省下来的时间都是自己的
🥑 你能学到什么?
希望在你阅读本篇文章之后,不会觉得浪费了时间。如果你跟着读下来,你将会学到:
- cli工具的基本搭建流程
- 如何通过模板引擎实现可选依赖
- 模板系统如何设计
- 如何根据模板引擎生成所需项目
- 熟悉一个组件库的基本结构
- 熟悉一个类型库的基本结构
- 熟悉一个cli项目的基本结构
后续你可以在此项目ObjectX-CLI的基础上,扩展项目支持的技术栈,和项目类型,以此了解各种项目的搭建流程
实现效果
🍎 系列文章
本系列是一个从0到1的实现过程,如果您有耐心跟着实现,您可以实现一个完整的react18 + ts5 + webpack5 + 代码质量&代码风格检测&自动修复 + storybook8 + rollup + git action实现的一个完整的组件库模板项目。如果您不打算自己配置,也可以直接clone组件库仓库切换到rollup_comp分支即是完整的项目,当前实现已经足够个人使用,后续我们会新增webpack5优化、按需加载组件、实现一些常见的组件封装:包括但不限于拖拽排序、瀑布流、穿梭框、弹窗等
- 【前端工程化】项目搭建篇-项目初始化&prettier、eslint、stylelint、lint-staged、husky
- 【前端工程化】项目搭建篇-配置changelog、webpack5打包
- 【前端工程化】项目搭建篇-引入react、ts、babel解析es6+、配置css module
- 【前端工程化】组件库搭建篇-引入storybook、rollup打包组件、本地测试组件库
- 【前端工程化】包管理器篇-三大包管理器、npm工程管理、npm发布流程
- 【前端工程化】自动化篇-Github Action基本使用、自动部署组件库文档、github3D指标统计
- 【前端工程化】自动化篇-手写脚本一键自动tag、发包、引导登录npm
- 【前端工程化】monorepo篇-rush管理monorepo实践
- 【前端工程化】monorepo篇-monorepo多包发布脚本实现
项目概述
ObjectX-CLI 是一个现代化的前端项目脚手架工具,支持快速创建以下三种类型的项目:
- 组件库项目 (component-lib):基于 React + TypeScript + Vite
- 工具包项目 (tool-lib):通用 JavaScript/TypeScript 工具库
- 类型包项目 (types-lib):纯 TypeScript 类型定义包
核心特性
- 🚀 零配置启动:开箱即用,一键创建项目
- 📦 模板化设计:基于 EJS 模板引擎,支持灵活配置
- 🎨 多样式方案:支持 Less、Tailwind CSS、CSS Modules
- 📚 文档集成:可选 Storybook 组件文档
- 🧩 Monorepo 支持:使用 pnpm workspace
- ⚡ 现代化打包:Rollup + TypeScript
核心架构设计
整体架构图
1objectx-cli/ 2├── bin/ # CLI 入口 3│ └── index.js # 可执行文件入口 4├── src/ # 源代码 5│ ├── index.ts # 主程序入口 6│ ├── commands/ # 命令实现 7│ │ └── create.ts # create 命令 8│ └── utils/ # 工具函数 9│ ├── generate.ts # 项目生成逻辑 10│ └── validate.ts # 验证逻辑 11├── templates/ # 项目模板 12│ ├── component-lib/ # 组件库模板 13│ ├── tool-lib/ # 工具库模板 14│ └── types-lib/ # 类型库模板 15├── lib/ # 编译输出(发布到 npm) 16├── package.json # 包配置 17├── rollup.config.js # 打包配置 18└── tsconfig.json # TypeScript 配置 19
核心流程图
1用户执行命令 2 ↓ 3bin/index.js(可执行入口) 4 ↓ 5src/index.ts(初始化 Commander) 6 ↓ 7src/commands/create.ts(处理 create 命令) 8 ↓ 9├─ validate.ts(验证项目名) 10├─ inquirer 交互式问答 11└─ generate.ts(生成项目文件) 12 ↓ 13├─ 读取模板文件 14├─ EJS 模板渲染 15├─ 条件文件过滤 16└─ 生成 package.json 17 ↓ 18Git 初始化 19 ↓ 20完成提示 21
技术栈分析
1. CLI 框架层
Commander.js - 命令行框架
1// src/index.ts 2import { Command } from 'commander'; 3 4const cli = new Command(); 5 6cli 7 .name('objectx-cli') 8 .description('前端项目脚手架工具') 9 .version(pkg.version); 10 11cli 12 .command('create <project-name>') 13 .description('创建一个新的项目') 14 .option('-t, --template <template>', '指定项目模板') 15 .action(create); 16
作用:
- 定义 CLI 命令和参数
- 自动生成
--help和--version - 参数解析和验证
2. 交互层
@inquirer/prompts - 交互式问答
1// src/commands/create.ts 2import * as inquirer from '@inquirer/prompts'; 3 4// 确认覆盖 5const overwriteResult = await inquirer.confirm({ 6 message: `目标目录 ${chalk.cyan(projectName)} 已存在。是否覆盖?`, 7 default: false 8}); 9 10// 单选 11const projectType = await inquirer.select({ 12 message: '请选择项目类型:', 13 choices: [ 14 { value: 'component-lib', name: '组件库项目' }, 15 { value: 'tool-lib', name: '工具包项目' }, 16 { value: 'types-lib', name: '类型包项目' } 17 ] 18}); 19 20// 多选 21const styles = await inquirer.checkbox({ 22 message: '选择样式解决方案 (可多选):', 23 choices: [ 24 { value: 'less', name: 'Less', checked: true }, 25 { value: 'tailwind', name: 'Tailwind CSS' }, 26 { value: 'css-modules', name: 'CSS Modules', checked: true } 27 ] 28}); 29
3. 用户体验层
Chalk - 终端颜色
1import chalk from 'chalk'; 2 3console.log(`${chalk.bgBlue('OBJECTX CLI ')} 🚀 创建新项目...`); 4console.log(chalk.green('✔') + ' 项目创建成功!'); 5console.log(chalk.red('✖') + ' 项目名称不能为空'); 6console.log([` cd ${chalk.cyan(projectName)}`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.cd.md)); 7
Ora - 加载动画
1import ora from 'ora'; 2 3const spinner = ora('正在生成项目文件...').start(); 4await generateProject(targetDir, projectOptions); 5spinner.succeed('项目文件生成完成'); 6 7// 失败情况 8spinner.fail('项目文件生成失败'); 9
4. 文件处理层
fs-extra - 增强的文件系统
1import fs from 'fs-extra'; 2 3// 确保目录存在 4await fs.ensureDir(targetDir); 5 6// 删除目录 7await fs.remove(targetDir); 8 9// 输出文件(自动创建父目录) 10await fs.outputFile(targetPath, content); 11 12// 检查文件是否存在 13if (fs.existsSync(targetDir)) { ... } 14
fast-glob - 文件搜索
1import glob from 'fast-glob'; 2 3// 读取所有模板文件(包括隐藏文件) 4const templateFiles = await glob('**/*', { 5 cwd: templateDir, 6 dot: true, // 包括 .gitignore 等 7 ignore: ['**/node_modules/**', '**/.git/**'] 8}); 9
5. 模板引擎层【核心】
EJS - 模板渲染
1import ejs from 'ejs'; 2 3// 模板数据 4const templateData = { 5 projectName: 'my-component', 6 hasLess: true, 7 hasTailwind: false, 8 year: 2024 9}; 10 11// 渲染模板 12const content = await fs.readFile(sourcePath, 'utf8'); 13const renderedContent = ejs.render(content, templateData); 14
EJS 模板示例:
1// Button.tsx.ejs 2<% if (hasCssModules) { %> 3import styles from './Button.module.css'; 4<% } else if (hasLess) { %> 5import './Button.less'; 6<% } else if (hasTailwind) { %> 7// 使用Tailwind类名 8<% } %> 9 10export const Button: React.FC<ButtonProps> = ({ children }) => { 11 <% if (hasCssModules) { %> 12 return <button className={styles.button}>{children}</button>; 13 <% } else if (hasTailwind) { %> 14 return <button className="bg-blue-500 text-white">{children}</button>; 15 <% } %> 16}; 17
6. 子进程管理
execa - 执行外部命令
1import { execa } from 'execa'; 2 3// 初始化 Git 4await execa('git', ['init'], { cwd: targetDir }); 5
实现原理深度解析【核心】
1. 可执行入口实现
package.json 配置
1{ 2 "name": "objectx-cli", 3 "type": "module", 4 "bin": { 5 "objectx-cli": "./bin/index.js" 6 }, 7 "files": ["bin", "lib", "templates"] 8} 9
bin字段:指定命令名称和执行文件type: "module":使用 ES Modulefiles字段:指定发布到 npm 的文件
bin/index.js
1#!/usr/bin/env node 2 3import { createRequire } from 'module'; 4const require = createRequire(import.meta.url); 5const pkg = require('../package.json'); 6 7import '../lib/index.js'; 8
关键点:
#!/usr/bin/env node:Shebang,告诉系统用 Node.js 执行createRequire:在 ESM 中使用require加载 JSON- 引入编译后的
lib/index.js(不是源码src/index.ts)
2. 项目名称验证
1// src/utils/validate.ts 2 3export function validateProjectName(projectName: string): void { 4 // 1. 空值检查 5 if (!projectName) { 6 console.log(chalk.red('✖') + ' 项目名称不能为空'); 7 process.exit(1); 8 } 9 10 // 2. 空格检查 11 if (projectName.trim() !== projectName) { 12 console.log(chalk.red('✖') + ' 项目名称不能以空格开头或结尾'); 13 process.exit(1); 14 } 15 16 // 3. npm 包名验证 17 const npmNameValidation = validateNpmPackageName(projectName); 18 if (!npmNameValidation.validForNewPackages) { 19 npmNameValidation.errors.forEach(err => { 20 console.log(' ' + chalk.red('✖') + ' ' + err); 21 }); 22 process.exit(1); 23 } 24 25 // 4. 保留关键字检查 26 const RESERVED_KEYWORDS = ['node_modules', 'favicon.ico', '.git']; 27 if (RESERVED_KEYWORDS.includes(projectName.toLowerCase())) { 28 console.log(chalk.red('✖') + ` 项目名称不能使用保留关键字`); 29 process.exit(1); 30 } 31} 32 33function validateNpmPackageName(name: string) { 34 const errors: string[] = []; 35 36 if (name.length > 214) { 37 errors.push('名称不能超过214个字符'); 38 } 39 40 if (name.match(/^[._]/)) { 41 errors.push('名称不能以 . 或 _ 开头'); 42 } 43 44 if (name.match(/[/\\]/)) { 45 errors.push('名称不能包含斜杠'); 46 } 47 48 return { 49 validForNewPackages: errors.length === 0, 50 errors 51 }; 52} 53
3. 目录覆盖处理
1// src/commands/create.ts 2 3const targetDir = path.join(process.cwd(), projectName); 4 5// 检查目录是否已存在 6if (fs.existsSync(targetDir)) { 7 const overwriteResult = await inquirer.confirm({ 8 message: `目标目录 ${chalk.cyan(projectName)} 已存在。是否覆盖?`, 9 default: false 10 }); 11 12 if (!overwriteResult) { 13 console.log(chalk.red('✖') + ' 操作取消'); 14 return; 15 } 16 17 const spinner = ora(`正在删除 ${chalk.cyan(targetDir)}...`).start(); 18 await fs.remove(targetDir); 19 spinner.succeed(`已删除 ${chalk.cyan(targetDir)}`); 20} 21 22await fs.ensureDir(targetDir); 23
安全性考虑:
- 先询问用户确认
- 显示加载动画,提升体验
- 使用
fs.remove完全删除旧目录
模板系统设计【核心】
1. 模板目录结构
1templates/ 2├── component-lib/ # 组件库模板 3│ ├── _gitignore.ejs # .gitignore(以 _ 开头) 4│ ├── demo/ # 示例应用(无 Storybook 时使用) 5│ │ ├── App.tsx.ejs 6│ │ ├── index.html.ejs 7│ │ └── main.tsx.ejs 8│ ├── pnpm-workspace.yaml.ejs 9│ ├── README.md.ejs 10│ ├── src/ 11│ │ ├── components/ 12│ │ │ ├── Button/ 13│ │ │ │ ├── Button.tsx.ejs 14│ │ │ │ ├── Button.module.css.ejs 15│ │ │ │ ├── Button.module.less.ejs 16│ │ │ │ └── Button.stories.tsx.ejs 17│ │ │ └── Card/ 18│ │ │ ├── Card.tsx.ejs 19│ │ │ └── Card.stories.tsx.ejs 20│ │ ├── index.ts 21│ │ ├── styles/ 22│ │ │ └── tailwind.css.ejs 23│ │ └── types/ 24│ │ └── css.d.ts 25│ ├── tailwind.config.js.ejs 26│ ├── tsconfig.json.ejs 27│ └── vite.config.ts.ejs 28│ 29├── tool-lib/ # 工具库模板 30│ ├── README.md.ejs 31│ ├── src/ 32│ │ ├── index.ts 33│ │ └── utils/ 34│ │ └── string.ts 35│ ├── tsconfig.json.ejs 36│ └── vite.config.ts.ejs 37│ 38└── types-lib/ # 类型库模板 39 ├── README.md.ejs 40 ├── src/ 41 │ ├── index.ts 42 │ └── types/ 43 │ ├── api.ts 44 │ └── common.ts 45 ├── tests/ 46 │ └── api.test-d.ts 47 └── tsconfig.json.ejs 48
2. 特殊文件命名约定
隐藏文件处理
1// src/utils/generate.ts 2 3function getTargetPath(file: string, targetDir: string, options: ProjectOptions): string { 4 let filename = file; 5 6 // 处理点文件(如 .gitignore) 7 // 模板中命名为 _gitignore.ejs,生成时转为 .gitignore 8 if (filename.startsWith('_')) { 9 filename = [`.${filename.slice(1)}`](https://xplanc.org/primers/document/zh/02.Python/EX.%E5%86%85%E5%BB%BA%E5%87%BD%E6%95%B0/EX.slice.md); 10 } 11 12 return path.join(targetDir, filename); 13} 14
为什么这样做?
- npm 发布时会忽略
.gitignore文件 - 使用
_gitignore绕过这个限制 - 生成项目时再重命名为
.gitignore
3. 条件文件生成
1function getTargetPath(file: string, targetDir: string, options: ProjectOptions): string { 2 // ...文件名处理... 3 4 // 如果选择了Storybook,跳过demo目录 5 if (filename.startsWith('demo') && options.needDocs) { 6 return ''; // 返回空字符串表示跳过 7 } 8 9 // 根据样式选择过滤文件 10 if (filename.endsWith('.less') && !options.styles.includes('less')) { 11 return ''; 12 } 13 14 if (filename.includes('tailwind') && !options.styles.includes('tailwind')) { 15 return ''; 16 } 17 18 // Card组件只在选择了tailwind时生成 19 if (filename.includes('components/Card') && !options.styles.includes('tailwind')) { 20 return ''; 21 } 22 23 return path.join(targetDir, filename); 24} 25
设计思想:
- 根据用户选择动态生成文件
- 避免生成无用文件
- 保持项目清爽
文件生成机制【核心】
1. 核心生成流程
1// src/utils/generate.ts 2 3export async function generateProject( 4 targetDir: string, 5 options: ProjectOptions 6): Promise<void> { 7 8 // 1. 定位模板目录 9 const templateDir = path.resolve( 10 __dirname, 11 '../../templates', 12 options.projectType 13 ); 14 15 // 2. 确保模板目录存在 16 if (!fs.existsSync(templateDir)) { 17 throw new Error(`找不到模板目录:${templateDir}`); 18 } 19 20 // 3. 读取所有模板文件 21 const templateFiles = await glob('**/*', { 22 cwd: templateDir, 23 dot: true, 24 ignore: ['**/node_modules/**', '**/.git/**'] 25 }); 26 27 // 4. 准备模板数据 28 const templateData: TemplateData = { 29 projectName: options.projectName, 30 needDocs: options.needDocs, 31 hasLess: options.styles.includes('less'), 32 hasTailwind: options.styles.includes('tailwind'), 33 hasCssModules: options.styles.includes('css-modules'), 34 year: new Date().getFullYear(), 35 packageManager: options.packageManager 36 }; 37 38 // 5. 遍历并处理每个文件 39 for (const file of templateFiles) { 40 const sourcePath = path.join(templateDir, file); 41 const targetPath = getTargetPath(file, targetDir, options); 42 43 // 跳过不需要的文件 44 if (targetPath === '') continue; 45 46 // 处理目录 47 if (fs.statSync(sourcePath).isDirectory()) { 48 await fs.ensureDir(targetPath); 49 continue; 50 } 51 52 // 读取文件内容 53 const content = await fs.readFile(sourcePath, 'utf8'); 54 55 // 6. EJS 模板渲染 56 if (file.endsWith('.ejs')) { 57 const renderedContent = ejs.render(content, templateData); 58 // 去掉 .ejs 后缀 59 await fs.outputFile( 60 targetPath.replace(/\.ejs$/, ''), 61 renderedContent 62 ); 63 } else { 64 // 非模板文件直接复制 65 await fs.outputFile(targetPath, content); 66 } 67 } 68 69 // 7. 生成 package.json 70 await generatePackageJson(targetDir, options); 71} 72
2. package.json 动态生成
1async function generatePackageJson( 2 targetDir: string, 3 options: ProjectOptions 4): Promise<void> { 5 6 // 基础配置 7 const packageJson: any = { 8 name: options.projectName, 9 version: '0.1.0', 10 private: false, 11 scripts: { 12 dev: 'vite', 13 build: 'vite build && tsc --emitDeclarationOnly', 14 lint: 'eslint src --ext .ts,.tsx', 15 test: 'jest' 16 }, 17 files: ['dist'], 18 devDependencies: { 19 "husky": "^9.0.7", 20 "typescript": "^5.2.2", 21 "eslint": "^8.52.0", 22 "terser": "^5.24.0" 23 } 24 }; 25 26 // 根据项目类型定制 27 switch (options.projectType) { 28 case 'component-lib': 29 // 组件库配置 30 packageJson.type = 'module'; 31 packageJson.main = './dist/index.js'; 32 packageJson.module = './dist/index.js'; 33 packageJson.types = './dist/index.d.ts'; 34 packageJson.exports = { 35 '.': { 36 import: './dist/index.js', 37 require: './dist/index.cjs' 38 } 39 }; 40 41 packageJson.peerDependencies = { 42 react: "^18.0.0", 43 "react-dom": "^18.0.0" 44 }; 45 46 packageJson.devDependencies = { 47 ...packageJson.devDependencies, 48 "vite": "^5.0.0", 49 "@vitejs/plugin-react": "^4.2.0", 50 "@types/react": "^18.2.0", 51 "@types/react-dom": "^18.2.0", 52 "react": "^18.2.0", 53 "react-dom": "^18.2.0", 54 "autoprefixer": "^10.4.16", 55 "postcss": "^8.4.31" 56 }; 57 58 // Storybook 支持 59 if (options.needDocs) { 60 packageJson.scripts.dev = 'storybook dev -p 6006'; 61 packageJson.scripts.storybook = 'storybook dev -p 6006'; 62 packageJson.scripts['build-storybook'] = 'storybook build'; 63 64 packageJson.devDependencies = { 65 ...packageJson.devDependencies, 66 "@storybook/addon-essentials": "^7.5.3", 67 "@storybook/react": "^7.5.3", 68 "@storybook/react-vite": "^7.5.3", 69 "storybook": "^7.5.3" 70 }; 71 } else { 72 // 使用 demo 作为开发环境 73 packageJson.scripts.dev = 'vite demo --open'; 74 } 75 76 // 样式依赖 77 if (options.styles.includes('less')) { 78 packageJson.devDependencies.less = "^4.2.0"; 79 } 80 81 if (options.styles.includes('tailwind')) { 82 packageJson.devDependencies.tailwindcss = "^3.3.5"; 83 } 84 break; 85 86 case 'tool-lib': 87 // 工具库配置 88 packageJson.type = 'module'; 89 packageJson.main = './dist/index.js'; 90 packageJson.module = './dist/index.js'; 91 packageJson.types = './dist/index.d.ts'; 92 93 packageJson.scripts.dev = 'vite build --mode watch'; 94 packageJson.scripts.docs = 'typedoc --out docs src/index.ts'; 95 96 packageJson.devDependencies = { 97 ...packageJson.devDependencies, 98 "vite": "^5.0.0", 99 "typedoc": "^0.25.3" 100 }; 101 break; 102 103 case 'types-lib': 104 // 类型库配置 105 packageJson.scripts.build = 'tsc --emitDeclarationOnly'; 106 packageJson.scripts.dev = 'tsc --emitDeclarationOnly --watch'; 107 packageJson.scripts['test:types'] = 'tsc --noEmit'; 108 109 packageJson.main = './dist/index.d.ts'; 110 packageJson.types = './dist/index.d.ts'; 111 112 packageJson.devDependencies = { 113 "husky": "^9.0.7", 114 "typescript": "^5.2.2", 115 "eslint": "^8.52.0", 116 "tsd": "^0.30.0", 117 "@types/node": "^20.8.10" 118 }; 119 break; 120 } 121 122 // 写入文件 123 await fs.writeFile( 124 path.join(targetDir, 'package.json'), 125 JSON.stringify(packageJson, null, 2) 126 ); 127} 128
亮点:
- 动态配置:根据项目类型和用户选择生成不同的依赖和脚本
- 双模式支持:同时支持 ESM 和 CommonJS
- 按需加载:只添加用户选择的功能的依赖
打包与发布
1. Rollup 打包配置
1// rollup.config.js 2 3import resolve from '@rollup/plugin-node-resolve'; 4import commonjs from '@rollup/plugin-commonjs'; 5import typescript from '@rollup/plugin-typescript'; 6import json from '@rollup/plugin-json'; 7import terser from '@rollup/plugin-terser'; 8 9export default { 10 // 入口文件 11 input: 'src/index.ts', 12 13 // 输出配置 14 output: [{ 15 dir: 'lib', // 输出目录 16 format: 'esm', // ES Module 格式 17 sourcemap: true, // 生成 source map 18 preserveModules: true, // 保留模块结构 19 entryFileNames: '[name].js' 20 }], 21 22 // 外部依赖(不打包) 23 external: [ 24 'fs', 'path', 'os', 'util', 'child_process', // Node.js 内置模块 25 'commander', 'chalk', 'ora', // CLI 依赖 26 '@inquirer/prompts', 'fs-extra', 'ejs', // 工具库 27 'fast-glob', 'execa', 'semver' 28 ], 29 30 // 插件 31 plugins: [ 32 resolve({ 33 extensions: ['.ts', '.js'] 34 }), 35 commonjs(), 36 json(), 37 typescript({ 38 tsconfig: './tsconfig.json', 39 outputToFilesystem: true 40 }), 41 terser() // 压缩代码 42 ] 43}; 44
为什么使用 Rollup?
- Tree Shaking:更好的死代码消除
- 模块保留:
preserveModules: true保持源码结构 - 更小的包体积:相比 Webpack 更适合库的打包
2. TypeScript 配置
1{ 2 "compilerOptions": { 3 "target": "ES2020", 4 "module": "NodeNext", // Node.js ESM 支持 5 "moduleResolution": "NodeNext", 6 "esModuleInterop": true, 7 "strict": true, 8 "declaration": true, // 生成 .d.ts 9 "resolveJsonModule": true, 10 "skipLibCheck": true, 11 "outDir": "lib", // 输出到 lib 目录 12 "baseUrl": ".", 13 "paths": { 14 "@/*": ["src/*"] 15 } 16 }, 17 "include": ["src/**/*", "bin/**/*"], 18 "exclude": ["node_modules", "src/templates/**/*"] 19} 20
3. npm 发布配置
package.json 关键字段
1{ 2 "name": "objectx-cli", 3 "version": "0.1.0", 4 "type": "module", 5 6 "bin": { 7 "objectx-cli": "./bin/index.js" 8 }, 9 10 "files": [ 11 "bin", 12 "lib", 13 "templates" 14 ], 15 16 "scripts": { 17 "build": "rollup -c", 18 "release": "bumpp && npm publish" 19 }, 20 21 "keywords": [ 22 "cli", "react", "typescript", "vite", 23 "component-library", "tooling" 24 ], 25 26 "engines": { 27 "node": ">=16.0.0" 28 } 29} 30
关键点解析:
files字段:- 只发布
bin、lib、templates目录 - 不发布源码
src/,减小包体积
- 只发布
engines字段:- 限制 Node.js 版本 >= 16
- 确保 ESM 特性可用
keywords字段:- 提升 npm 搜索排名
发布流程
1# 1. 构建 2pnpm build 3 4# 2. 版本管理(使用 bumpp) 5pnpm release 6 7# bumpp 会自动: 8# - 提示选择版本号(patch/minor/major) 9# - 更新 package.json 10# - 创建 git tag 11# - 推送到远程 12# - 发布到 npm 13
后续
好了,这就是一个完整的企业级脚手架搭建流程,后续我还会介绍webpack、vite的实现原理,eslint插件开发,babel实现原理,babel插件开发,感兴趣的可以关注下
《【前端工程化】脚手架篇 - 模板引擎 & 动态依赖管理脚手架》 是转载文章,点击查看原文。
