【若川视野 x 源码共读】第37期 | create-vite

156 阅读2分钟

【若川视野 x 源码共读】第37期 | create-vite

node新手学习大佬是怎么写脚手架的

//源码地址
https://github.com/vitejs/vite/blob/HEAD/packages/create-vite/index.js

1.工具函数

格式化文件夹名

/**
 * @param {string | undefined} targetDir
 */
function formatTargetDir(targetDir) {
  return targetDir?.trim().replace(/\/+$/g, '')
}
formatTargetDir(' /d/ ')
// '/d'

递归复制src目录下的文件以及文件夹内容到dest目录下

function copy(src, dest) {
  const stat = fs.statSync(src)
  if (stat.isDirectory()) {
    copyDir(src, dest)
  } else {
    fs.copyFileSync(src, dest)
  }
}
/**
 * @param {string} srcDir
 * @param {string} destDir
 */
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)
  }
}

清空文件夹内容,对git的配置都做了保留

/**
 * @param {string} dir
 */
function emptyDir(dir) {
  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 isEmpty(path) {
  const files = fs.readdirSync(path)
  return files.length === 0 || (files.length === 1 && files[0] === '.git')
}

对包名做规范检查以及兼容处理

/**
 * @param {string} projectName
 */
function isValidPackageName(projectName) {
  return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(
    projectName
  )
}

/**
 * @param {string} projectName
 */
function toValidPackageName(projectName) {
  return projectName
    .trim()
    .toLowerCase()
    .replace(/\s+/g, '-')
    .replace(/^[._]/, '')
    .replace(/[^a-z0-9-~]+/g, '-')
}

pkgFromUserAgent函数

/**
 * @param {string | undefined} userAgent process.env.npm_config_user_agent
 * @returns object | undefined
 */
function pkgFromUserAgent(userAgent) {
  if (!userAgent) return undefined
  const pkgSpec = userAgent.split(' ')[0]
  const pkgSpecArr = pkgSpec.split('/')
  return {
    name: pkgSpecArr[0],
    version: pkgSpecArr[1]
  }
}
const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent)
const pkgManager = pkgInfo ? pkgInfo.name : 'npm'
// 检查脚本环境的包管理器类型

fileURLToPath(import.meta.url) //为了找回模板的路径
// fileURLToPath 解码百分比编码字符,并确保跨平台有效的绝对路径字符串。
// import.meta.url  当前模块文件 URL 
  • 从工具函数中可以看出大佬做了不少的代码防御
  • 基本都用了fs的同步写法操作文件,便捷和性能的取舍

2.minimist 解析命令行参数, 便捷配置项目名和模板

// index.js
const argv = minimist(process.argv.slice(2), { string: ['_','v'] }) // _,v内的参数都转成字符串类型
// node index.js a 3 4 -v 6
{ _: [ 'a', '3', '4' ], v: '6' }
{ _: [ 'a', 3, 4 ], v: 6 }  // 没配置string的效果

3.prompts 命令行交互

轻量级、美观、用户友好的交互式提示

  try {
    result = await prompts(
      [
        {
          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 } = {}) => {
            if (overwrite === false) {
              throw new Error(red('✖') + ' Operation cancelled')
            }
            return null
          },
          name: 'overwriteChecker'
        },
        {
          type: () => (isValidPackageName(getProjectName()) ? null : 'text'),
          name: 'packageName',
          message: reset('Package name:'),
          initial: () => toValidPackageName(getProjectName()),
          validate: (dir) =>
            isValidPackageName(dir) || 'Invalid package.json name'
        },
      ],
      {
        onCancel: () => {
          throw new Error(red('✖') + ' Operation cancelled')
        }
      }
    )
  } catch (cancelled) {
    console.log(cancelled.message)
    return
  }
  • 利用type支持的函数类型动态设置类型
  • overwrite覆盖提醒
  • overwrite设置为否后终止交互以及利用catch里面的return终止执行后续代码
  • 项目名称校验格式化
  • prompts选择template的部分略过了....

4.最后就是根据输入的命令进行模板拷贝到新建项目中以及动态设置package.json

前面做的的一切都是为这步做准备

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

 for (const file of files.filter((f) => f !== 'package.json')) {
    write(file)
  }
  // 配置项目名到json文件
  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))

大佬配置各种vite模板

  • vanilla
  • vanilla-ts
  • vue
  • vue-ts
  • react
  • react-ts
  • preact
  • preact-ts
  • lit
  • lit-ts
  • svelte
  • svelte-ts