探索create-vite源码,一看就懂!
1.背景
2.指令说明
create-vite 是一个快速构建vite工程的脚手架,使用npm create vite创建工程,其中create是init的别名,具体可查看官方文档
3.调试准备
本文使用川哥项目地址:github.com/lxchuan12/v…
按以下顺序依次执行命令,克隆项目,调试源码
git clone https://github.com/lxchuan12/vite-analysis.git
cd vite-analysis/vite2/packages/create-vite
node index.js
安装依赖时,我这里报错了,说是因为我本地node版本过低14.18.1,所以我还装了nvm,安装了最新版本。 项目下载下来以后,先看README.md和CONTRIBUTING.md,这里会有对项目的一些说明,包括环境及调试方法。
4.项目结构概览
通过package.json文件里的信息我们可以知道,type类型是module,可执行的命令create-vite,入口文件是index.js
index.js文件内的变量及函数
5.代码分析
从index.js文件内部函数命名,我们也可以看出init应该是需要打断点的位置,上面一些其他的方法是为这个函数服务的。
// node依赖
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
// npm包 解析命令行参数
import minimist from 'minimist'
// npm包 命令行交互
import prompts from 'prompts'
// npm包 命令行颜色
import {
blue,
cyan,
green,
lightRed,
magenta,
red,
reset,
yellow
} from 'kolorist'
// 解析指令上附带的命令行参数
const argv = minimist(process.argv.slice(2), { string: ['_'] })
// 获取当前node进程所在的文件目录
const cwd = process.cwd()
// 框架
const FRAMEWORKS = [
{
name: 'vanilla',
color: yellow,
variants: [
{
name: 'vanilla',
display: 'JavaScript',
color: yellow
},
{
name: 'vanilla-ts',
display: 'TypeScript',
color: blue
}
]
},
...
]
// 模板名称
const TEMPLATES = FRAMEWORKS.map(
(f) => (f.variants && f.variants.map((v) => v.name)) || [f.name]
).reduce((a, b) => a.concat(b), [])
// 重写名字映射(电脑系统不一样,gitignore前缀名称不一样)
const renameFiles = {
_gitignore: '.gitignore'
}
// 初始化函数,核心方法
async function init() {
...
}
// 去除字符串的空格,并将\替换为空字符串
function formatTargetDir(targetDir) {
return targetDir?.trim().replace(/\/+$/g, '')
}
// 拷贝文件
function copy(src, dest) {
const stat = fs.statSync(src)
if (stat.isDirectory()) {
copyDir(src, dest)
} else {
fs.copyFileSync(src, dest)
}
}
// 校验包名是否符合规范
function isValidPackageName(projectName) {
return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(
projectName
)
}
// 处理包名为合法名称
function toValidPackageName(projectName) {
return projectName
.trim()
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/^[._]/, '')
.replace(/[^a-z0-9-~]+/g, '-')
}
// 拷贝文件夹
function copyDir(srcDir, destDir) {
fs.mkdirSync(destDir, { recursive: true })
for (const file of fs.readdirSync(srcDir)) {
const srcFile = path.resolve(srcDir, file)
const destFile = path.resolve(destDir, file)
copy(srcFile, destFile)
}
}
// 是否空文件夹
function isEmpty(path) {
const files = fs.readdirSync(path)
return files.length === 0 || (files.length === 1 && files[0] === '.git')
}
// 删除文件夹及文件
function emptyDir(dir) {
if (!fs.existsSync(dir)) {
return
}
for (const file of fs.readdirSync(dir)) {
fs.rmSync(path.resolve(dir, file), { recursive: true, force: true })
}
}
// 包管理器信息
function pkgFromUserAgent(userAgent) {
if (!userAgent) return undefined
const pkgSpec = userAgent.split(' ')[0]
const pkgSpecArr = pkgSpec.split('/')
return {
name: pkgSpecArr[0],
version: pkgSpecArr[1]
}
}
// 错误补获
init().catch((e) => {
console.error(e)
})
6.初始化函数分析
6.1 获取工程目录及模板
// 获取命令行参数的工程目录
let targetDir = formatTargetDir(argv._[0])
// 获取命令行参数的模板
let template = argv.template || argv.t
// 默认的项目名
const defaultTargetDir = 'vite-project'
// 获取项目名称
const getProjectName = () =>
targetDir === '.' ? path.basename(path.resolve()) : targetDir
6.2 命令行的交互(框架、变体、项目名),有输入截取校验,没有则让用户输入及选择
let result = {}
try {
result = await prompts(
[
{
type: targetDir ? null : 'text',
name: 'projectName',
message: reset('Project name:'),
initial: defaultTargetDir,
onState: (state) => {
targetDir = formatTargetDir(state.value) || defaultTargetDir
}
},
{
type: () =>
!fs.existsSync(targetDir) || isEmpty(targetDir) ? null : 'confirm',
name: 'overwrite',
message: () =>
(targetDir === '.'
? 'Current directory'
: `Target directory "${targetDir}"`) +
` is not empty. Remove existing files and continue?`
},
...
],
{
onCancel: () => {
throw new Error(red('✖') + ' Operation cancelled')
}
}
)
// user choice associated with prompts
const { framework, overwrite, packageName, variant } = result
6.3 重写目录或创建目录
// 目录拼接
const root = path.join(cwd, targetDir)
// 重写文件夹(清空再创建或直接创建)
if (overwrite) {
emptyDir(root)
} else if (!fs.existsSync(root)) {
fs.mkdirSync(root, { recursive: true })
}
6.3.1递归删除文件夹
扩展函数
function emptyDir(dir) {
if (!fs.existsSync(dir)) {
return
}
for (const file of fs.readdirSync(dir)) {
fs.rmSync(path.resolve(dir, file), { recursive: true, force: true })
}
}
6.4 确定模板路径
// 获取模板路径
template = variant || framework || template
console.log(`\nScaffolding project in ${root}...`)
const templateDir = path.resolve(
fileURLToPath(import.meta.url),
'..',
`template-${template}`
)
6.5写入文件(包含gitignore文件的特殊处理)
const write = (file, content) => {
const targetPath = renameFiles[file]
? path.join(root, renameFiles[file])
: path.join(root, file)
if (content) {
fs.writeFileSync(targetPath, content)
} else {
copy(path.join(templateDir, file), targetPath)
}
}
const files = fs.readdirSync(templateDir)
// 除了package.json文件,都执行write方法
for (const file of files.filter((f) => f !== 'package.json')) {
write(file)
}
// 获取package.json文件内容
const pkg = JSON.parse(
fs.readFileSync(path.join(templateDir, `package.json`), 'utf-8')
)
// 重写name字段(因为目录名称不一样,所以这里需要特殊处理)
pkg.name = packageName || getProjectName()
// 再写入
write('package.json', JSON.stringify(pkg, null, 2))
6.6 根据所安装的包管理器,对应输出安装信息
const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent)
const pkgManager = pkgInfo ? pkgInfo.name : 'npm'
console.log(`\nDone. Now run:\n`)
if (root !== cwd) {
console.log(` cd ${path.relative(cwd, root)}`)
}
switch (pkgManager) {
case 'yarn':
console.log(' yarn')
console.log(' yarn dev')
break
default:
console.log(` ${pkgManager} install`)
console.log(` ${pkgManager} run dev`)
break
}
7 总结
通过调试源码熟悉了nodejs的api; process.cwd()获取当前node进程所在的文件目录, path.resolve()获取文件绝对路径, path.join()也是获取文件路径,但和resolve有区别。 总的来说,该源码流程就是根据命令行输入,解析出参数,如没有输入参数时,通过prompts进行命令行交互,根据用户的输入匹配已预设好的相应框架模板,然后创建目录名称,写入文件。
转载自:https://juejin.cn/post/7394395254567845899