我正在参与掘金创作者训练营第6期,点击了解活动详情
本文已参与「新人创作礼」活动,一起开启掘金创作之路。
本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
【若川视野 x 源码共读】第37期 | vite 3.0 都发布了,这次来手撕 create-vite 源码 点击了解本期详情一起参与。
前言
npm init
npm init === npm create
其实是同一个意思,create是init的别名
npm create vite@latest
- 创建一个最新版本的
vite项目
源码
当我们项目克隆下来后,先看README.md,这个工具是做什么的,使用方式是怎么样的。
其次,可以看看CONTRIBUTING.md,会有相关提示说明,怎么去调试该项目。
调试代码
首先我们看下有什么依赖包
用来解析命令行的信息
输入:npm create vite@latest my-app --template vue
输出:
{
_:['my-app'],
template:'vue'
}
用户交互友好提示
控制台输出彩色文字
解决跨平台路径问题
init函数
步骤已经写在注释里面了
整个过程比较简单
- 用户输入
npm create vite@latest - 如果项目存在,则询问是否覆盖
- 检查项目名称是否符合
npm要求 - 读取用户指定的
template,如果没有则让用户选择 - 拉取对应的模板仓库,copy到指定的目录中
- 根据包管理器修改
package.json - 结束
async function init() {
// 去掉最后一个斜杠 '/'
const argTargetDir = formatTargetDir(argv._[0])
// 获取是否有指定template
const argTemplate = argv.template || argv.t
// 获取指定文件夹名称
let targetDir = argTargetDir || defaultTargetDir
const getProjectName = () =>
targetDir === '.' ? path.basename(path.resolve()) : targetDir
// 选择模板
let result: prompts.Answers<
'projectName' | 'overwrite' | 'packageName' | 'framework' | 'variant'
>
try {
result = await prompts(
[
{
type: argTargetDir ? 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?`
},
{
type: (_, { overwrite }: { overwrite?: boolean }) => {
if (overwrite === false) {
throw new Error(red('✖') + ' Operation cancelled')
}
return null
},
name: 'overwriteChecker'
},
// 检查是否符合npm名称
{
type: () => (isValidPackageName(getProjectName()) ? null : 'text'),
name: 'packageName',
message: reset('Package name:'),
initial: () => toValidPackageName(getProjectName()),
validate: (dir) =>
isValidPackageName(dir) || 'Invalid package.json name'
},
// 如果没有传参模板信息,让用户选择
{
type:
argTemplate && TEMPLATES.includes(argTemplate) ? null : 'select',
name: 'framework',
message:
typeof argTemplate === 'string' && !TEMPLATES.includes(argTemplate)
? reset(
`"${argTemplate}" isn't a valid template. Please choose from below: `
)
: reset('Select a framework:'),
initial: 0,
choices: FRAMEWORKS.map((framework) => {
const frameworkColor = framework.color
return {
title: frameworkColor(framework.display || framework.name),
value: framework
}
})
},
{
type: (framework: Framework) =>
framework && framework.variants ? 'select' : null,
name: 'variant',
message: reset('Select a variant:'),
choices: (framework: Framework) =>
framework.variants.map((variant) => {
const variantColor = variant.color
return {
title: variantColor(variant.display || variant.name),
value: variant.name
}
})
}
],
{
onCancel: () => {
throw new Error(red('✖') + ' Operation cancelled')
}
}
)
} catch (cancelled: any) {
console.log(cancelled.message)
return
}
// user choice associated with prompts
const { framework, overwrite, packageName, variant } = result
const root = path.join(cwd, targetDir)
// 获取上面的结果,执行删除指定目录
if (overwrite) {
emptyDir(root)
} else if (!fs.existsSync(root)) {
fs.mkdirSync(root, { recursive: true })
}
// 获取当前的包管理名称,看下是否需要替换
// determine template
const template: string = variant || framework || argTemplate
const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent)
const pkgManager = pkgInfo ? pkgInfo.name : 'npm'
const isYarn1 = pkgManager === 'yarn' && pkgInfo?.version.startsWith('1.')
const { customCommand } =
FRAMEWORKS.flatMap((f) => f.variants).find((v) => v.name === template) ?? {}
if (customCommand) {
const fullCustomCommand = customCommand
.replace('TARGET_DIR', targetDir)
.replace(/^npm create/, `${pkgManager} create`)
// Only Yarn 1.x doesn't support `@version` in the `create` command
.replace('@latest', () => (isYarn1 ? '' : '@latest'))
.replace(/^npm exec/, () => {
// Prefer `pnpm dlx` or `yarn dlx`
if (pkgManager === 'pnpm') {
return 'pnpm dlx'
}
if (pkgManager === 'yarn' && !isYarn1) {
return 'yarn dlx'
}
// Use `npm exec` in all other cases,
// including Yarn 1.x and other custom npm clients.
return 'npm exec'
})
const [command, ...args] = fullCustomCommand.split(' ')
const { status } = spawn.sync(command, args, {
stdio: 'inherit'
})
process.exit(status ?? 0)
}
console.log(`\nScaffolding project in ${root}...`)
const templateDir = path.resolve(
fileURLToPath(import.meta.url),
'../..',
`template-${template}`
)
const write = (file: string, content?: string) => {
const targetPath = path.join(root, renameFiles[file] ?? file)
if (content) {
fs.writeFileSync(targetPath, content)
} else {
copy(path.join(templateDir, file), targetPath)
}
}
const files = fs.readdirSync(templateDir)
for (const file of files.filter((f) => f !== 'package.json')) {
write(file)
}
const pkg = JSON.parse(
fs.readFileSync(path.join(templateDir, `package.json`), 'utf-8')
)
pkg.name = packageName || getProjectName()
write('package.json', JSON.stringify(pkg, null, 2))
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
}
console.log()
}
辅助函数
- 递归删除文件夹
function emptyDir(dir: string) {
if (!fs.existsSync(dir)) {
return
}
for (const file of fs.readdirSync(dir)) {
if (file === '.git') {
continue
}
fs.rmSync(path.resolve(dir, file), { recursive: true, force: true })
}
}
- 获取当前用户的包管理名称
function pkgFromUserAgent(userAgent: string | undefined) {
if (!userAgent) return undefined
const pkgSpec = userAgent.split(' ')[0]
const pkgSpecArr = pkgSpec.split('/')
return {
name: pkgSpecArr[0],
version: pkgSpecArr[1]
}
}
总结
整个代码行数不多,大概在400多行,但受益匪浅,从源码中知道了如何去构建一个脚手架工具,通过命令行的形式去快速的生成对应的模板。源码中的代码抽象逻辑,功能的拆分值得我学习的,还有功能测试,考虑到程序不同情况,保证程序了功能实现和正常运行。
源码共读活动获益良多,可以拓展视野。我们不应该只局限于业务逻辑代码上,应该从更高的维度去考虑问题和通解,提高自身的编码水平。