水一篇create-vite源码分析

392 阅读4分钟

create-vite是vite官方推出的脚手架工具,通过几行简单的命令可以交互式完成vite项目的创建,通过内置丰富的项目模板即可生成多种类型的项目,下面来看一下vite如何创建项目

捕获.PNG

如何调试

vite和create-vite维护在同一个repo中,进入到子目录下调试即可

  • clone项目并安装依赖(使用pnpm workspace管理依赖)
  • 进入packages/create-vite 执行build
  • 运行构建产物dist/index.mjs

按照上述步骤即可打开create-vite的交互式界面

目录结构

捕获.PNG

create-vite目录结构比较简单,代码量也很少

  • __tests__ 单元测试文件
  • src下是源码,只有一个文件
  • template-* 项目模板,vite提前创建好了template
  • build.config.ts 构建工具配置(create-vite用的是unbuild)

关于unbuild

unbuild是一款很简洁的build工具,不像webpack那样“沉重”,create-vite基于unbuild+rollup实现打包功能

贴一下unbuild的配置(很容易看懂)

export default defineBuildConfig({
  entries: ['src/index'],
  clean: true,
  rollup: {
    inlineDependencies: true,
    esbuild: {
      minify: true,
    },
  },
  alias: {
    prompts: 'prompts/lib/index.js',
  },
  hooks: {
    'rollup:options'(ctx, options) {
      options.plugins = [
        options.plugins,
        licensePlugin(
          path.resolve(__dirname, './LICENSE'),
          'create-vite license',
          'create-vite',
        ),
      ]
    },
  },
})

执行流程分析

看完了目录结构及构建工具等内容,下面来看一下源码吧(src/index.ts)

生成交互式ui

大家在使用各种cli工具时肯定见过 “选择” “输入”这样的功能,那如何简单实现这种效果呢

命令行交互式ui

vite使用的是prompts(之前我碰到类似的需求也用这个),通过简单配置即可生成交互ui,下面是vite中的调用代码

  try {
    result = await prompts(
      [
        {
          type: argTargetDir ? null : 'text',
          name: 'projectName',
          message: reset('Project name:'),
          initial: defaultTargetDir,
          onState: (state) => {
            targetDir = formatTargetDir(state.value) || defaultTargetDir
          },
        }
        // some code
      ],
      {
        onCancel: () => {
          throw new Error(red('✖') + ' Operation cancelled')
        },
      },
    )
  } catch (cancelled: any) {
    // some code
  }

没有很复杂的功能,prompts主要用来获取用户输入,create-vite根据用户输入或命令行参生成配置

生成创建项目的config

vite在获取prompts及命令行入参后合成一份构建配置,包含创建一个vite项目需要的全部信息

  const argTargetDir = formatTargetDir(argv._[0])
  const argTemplate = argv.template || argv.t
  // 获取dir相关信息

  let targetDir = argTargetDir || defaultTargetDir
  const getProjectName = () =>
    targetDir === '.' ? path.basename(path.resolve()) : targetDir
  // 项目名称,如果没有指定会使用默认名称
  
  const { framework, overwrite, packageName, variant } = result
  // result 是prompts返回的结果,包含框架等信息
  const root = path.join(cwd, targetDir)

  if (overwrite) {
    emptyDir(root)
  } else if (!fs.existsSync(root)) {
    fs.mkdirSync(root, { recursive: true })
  }
  // 目录非空的晴空下可以选择清空目录
  let template: string = variant || framework?.name || argTemplate

  const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent)
  const pkgManager = pkgInfo ? pkgInfo.name : 'npm'
  const isYarn1 = pkgManager === 'yarn' && pkgInfo?.version.startsWith('1.')
  // 获取package manager

这里的代码基本就是按照流程写的(个人感觉清晰但不优雅,不喜勿喷)

对于react+swc的情况有额外的判断,我猜测因为只有react有swc的选项,所以这里处理的比较简单

  let template: string = variant || framework?.name || argTemplate
  let isReactSwc = false
  if (template.includes('-swc')) {
    isReactSwc = true
    template = template.replace('-swc', '')
  }
  // some code
  if (isReactSwc) {
    setupReactSwc(root, template.endsWith('-ts'))
  }
  
  // 替换配置文件
  function setupReactSwc(root: string, isTs: boolean) {
    editFile(path.resolve(root, 'package.json'), (content) => {
      return content.replace(
        /"@vitejs\/plugin-react": ".+?"/,
        `"@vitejs/plugin-react-swc": "^3.0.0"`,
      )
    })
    editFile(
      path.resolve(root, `vite.config.${isTs ? 'ts' : 'js'}`),
      (content) => {
        return content.replace('@vitejs/plugin-react', '@vitejs/plugin-react-swc')
      },
    )
  }

customCommand

如果官方配置不能满足需求,create-vite还支持部分非官方配置,在选择variant时选择Other即可进入这一部分逻辑

customCommand

//customCommand option
{
  name: 'create-vite-extra',
  display: 'create-vite-extra ↗',
  color: reset,
  customCommand: 'npm create vite-extra@latest TARGET_DIR',
},

选择后vite通过npm create调用create-vite-extra完成创建过程。

我看到这个npm create还是有点懵逼的,查了一下了解到npm create就是npm init的别名。

而且你会发现包名是create-vite-extra,这里调用的居然是vite-extra,因为在执行 npm create [name]时npm会自动在前面加上create找到create-vite-extra(可以用执行npm create react-app试一下,可以自动找到CRA)

写入template

const templateDir = path.resolve(
  fileURLToPath(import.meta.url),
  '../..',
  `template-${template}`
  )
  
const files = fs.readdirSync(templateDir)
  for (const file of files.filter((f) => f !== 'package.json')) {
    write(file)
}

create-vite解析出template后将内部的template复制到目标目录,完成创建

总结一下create-vite的执行流程

test.png

工具函数

create-vite内有很多工具函数值得一看,下面选几个看一下

isEmpty && emptyDir

function isEmpty(path: string) {
  const files = fs.readdirSync(path)
  return files.length === 0 || (files.length === 1 && files[0] === '.git')
}

imEmpty通过readdir判断一个目录是否为空,这里对.git文件做了特殊处理(如果有大佬知道为什么可以在评论区留言,非常感谢)

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 })
  }
}

emptyDir通过readdir获取所有目录,然后通过fs.rm删除,猜测没有直接用rm -rf是想对.git进行判断

copy && copyDir

copy和copyDir主要用来实现复制template功能

function copy(src: string, dest: string) {
  const stat = fs.statSync(src)
  if (stat.isDirectory()) {
    copyDir(src, dest)
  } else {
    fs.copyFileSync(src, dest)
  }
}

function copyDir(srcDir: string, destDir: string) {
  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)
  }
}

copy会通过fs.stat对文件类型进行判断,如果是file则通过copyFile直接完成复制,如果是dir执行copyDir

copyDir会获取该目录下的所有file和dir,通过调用copy完成复制(copy内部再进行file的dir的判断,重复刚刚的流程)

pkgFromUserAgent

creact-vite通过process.env获取用户的package manager(我从来没获取过,学到了)

const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent)
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],
  }
}

获取template dir

const templateDir = path.resolve(
  fileURLToPath(import.meta.url),
  '../..',
  `template-${template}`,
)

获取template dir的方式很有意思,指利用的是import.meta.url,该属性会返回一个file链接(file://XXXXXX/XXXXXX)通过fileURLToPath即可获取到path

总结

create-vite的代码很是很容易读懂的,推荐愿意读源码的同学尝试一下,个人认为create-vite代码比较好的几个点。

  • 代码简洁易读
  • 支持customCommand,通过npm create引入第三方template,增加了灵活性
  • 工具函数很简洁,值得一看

下一篇可能会写only-allow或者esbuild相关的内容,如果有大佬对源码阅读感兴趣可以在评论区讨论。