借鉴create-vite搭建自己的创建项目工具(1)源码分析

背景

写这个工具的起因是因为公司一些新建项目的时候都会从老的项目中cv一套出来,然后把里面的代码删掉,重新在这个基础上开发。这样造成了很多的问题,比如1. 上一个代码并没有删除完全,很多无用代码影响的项目的维护和可读性。2. 人工的手动搬运和修改很容易造成意想不到的问题。等等这些问题我就打算自己写一个创建项目的脚手架帮助大家来快速新建项目。

文章结构

这篇文章会从以下几点来介绍

  1. create-vite源码介绍
  2. 如何自己从0到1搭建
  3. 如何将自己的项目发到npm

create-vite源码

我是直接从github上clone的vite的源码,因为要学vite其他的东西嘛,索性一起下下来了。create-vite的结构接在这里

可以看到很多都是模板文件。而核心代码就在src/index.ts里面。一共504行代码。这个我们一会再说,先来看看这个项目的package.json。

package.json

create-vite里的package.json很简单,这里记录较重要的点bin ,files ,scripts ,devDependencies。

bin

json 复制代码
  "bin": {
    "create-vite": "index.js",
    "cva": "index.js"
  },

bin里面的命令就是咱们使用vite创建项目时会使用到的命令。比如全局安装了vite后,在终端执行create-vite,就会执行create-vite中dist文件夹下的index.js文件去执行创建项目的逻辑。如果新建一个脚手架也可以定义自己的脚本命令。

files

json 复制代码
  "files": [
    "index.js",
    "template-*",
    "dist"
  ],

这个字段用我自己的理解就是当你发布一个npm包时所要包含的文件。这个files字段的优先级最大不会=被npmignore和.gitignore覆盖。就比如我不想把我src下的开发源码一起发到npm包上去,那我就在fules里面不包含src文件夹,这样上传后不仅减少了体积,还避免了一些开发源码泄露的问题。

scripts

json 复制代码
  "scripts": {
    "dev": "unbuild --stub",
    "build": "unbuild",
    "typecheck": "tsc --noEmit",
    "prepublishOnly": "npm run build"
  },

这里的unbuild在create-vite包里是找不到安装依赖的,因为vite用的monorepo架构的,所以unbuild依赖是放在最外层的package.json中的。而unbuild跟rollup类似,是一个js库打包工具。这里就不详细介绍啦,有兴趣的可以去看一下:unbuild。这里还要写一个对应的build.config.ts配置文件。来配置打包规则。

devDependencies

perl 复制代码
  "devDependencies": {
    "@types/minimist": "^1.2.2",
    "@types/prompts": "^2.4.4",
    "cross-spawn": "^7.0.3",
    "kolorist": "^1.8.0",
    "minimist": "^1.2.8",
    "prompts": "^2.4.2"
  }

两个声明文件包不做过多介绍

  • cross-spawn 自动根据运行平台(windows、mac、linux 等)生成 shell 命令,并执行
  • minimist 解析命令行传入的参数;
  • prompts 命令行交互提示;
  • kolorist 给输入输出上颜色;

这几个都要用到,没啥好说的。接下来直接看src/index.ts里的核心内容

src/index.ts

这里补充一下我的调试方法。首先在create-vite文件夹起一个终端,然后运行pnpm run dev命令,这样就进入到开发模式了,接着终端执行 node dist/index.mjs 来执行代码。

index.ts文件中最核心的函数就是init函数。直接从这里看

javascript 复制代码
async function init() {
	const argTargetDir = formatTargetDir(argv._[0]) // 获取传入参数中的项目名称
  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'
  >
	....
}

一开始定义了一些变量,接收命令行的参数,比如我输入这样一行命令

arduino 复制代码
node dist/index.mjs  my-vue-app --template vue

那么argTargetDir就是"my-vue-app", argTemplate就是"vue"。这两个就是取命令行里的参数,其他两个没啥好讲的,语义就能看懂。

接下来就是使用prompts来进行命令行交互,一大堆交互逻辑,这里也不展开细讲。

拿到交互结果以后,就要去创建文件夹和进行写入了。这一段代码就是创建文件夹的逻辑,判断了是否重写还是创建新文件夹。

scss 复制代码
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 })
}

这一段就是确认使用的模板

ini 复制代码
 // determine template
  let template: string = variant || framework?.name || argTemplate
  let isReactSwc = false
  if (template.includes('-swc')) {
    isReactSwc = true
    template = template.replace('-swc', '')
  }

这一段是确认包管理器

javascript 复制代码
  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(/^npm create /, () => {
        // `bun create` uses it's own set of templates,
        // the closest alternative is using `bun x` directly on the package
        if (pkgManager === 'bun') {
          return 'bun x create-'
        }
        return `${pkgManager} create `
      })
      // Only Yarn 1.x doesn't support `@version` in the `create` command
      .replace('@latest', () => (isYarn1 ? '' : '@latest'))
      .replace(/^npm exec/, () => {
        // Prefer `pnpm dlx`, `yarn dlx`, or `bun x`
        if (pkgManager === 'pnpm') {
          return 'pnpm dlx'
        }
        if (pkgManager === 'yarn' && !isYarn1) {
          return 'yarn dlx'
        }
        if (pkgManager === 'bun') {
          return 'bun x'
        }
        // Use `npm exec` in all other cases,
        // including Yarn 1.x and other custom npm clients.
        return 'npm exec'
      })

    const [command, ...args] = fullCustomCommand.split(' ')
    // we replace TARGET_DIR here because targetDir may include a space
    const replacedArgs = args.map((arg) => arg.replace('TARGET_DIR', targetDir))
    const { status } = spawn.sync(command, replacedArgs, {
      stdio: 'inherit',
    })
    process.exit(status ?? 0)
  }

这一段就是开始进行写入操作了

typescript 复制代码
 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) + '\n')

  if (isReactSwc) {
    setupReactSwc(root, template.endsWith('-ts'))
  }

  const cdProjectName = path.relative(cwd, root)
  console.log(`\nDone. Now run:\n`)
  if (root !== cwd) {
    console.log(
      `  cd ${
        cdProjectName.includes(' ') ? `"${cdProjectName}"` : cdProjectName
      }`,
    )
  }

最后打印了一下提示

javascript 复制代码
switch (pkgManager) {
    case 'yarn':
      console.log('  yarn')
      console.log('  yarn dev')
      break
    default:
      console.log(`  ${pkgManager} install`)
      console.log(`  ${pkgManager} run dev`)
      break
  }

主要的步骤和环节就梳理完全啦,还有一些不在init中定义的方法我在这里贴出来,

php 复制代码
function formatTargetDir(targetDir: string | undefined) {
  return targetDir?.trim().replace(//+$/g, '')
}

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

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

function toValidPackageName(projectName: string) {
  return projectName
    .trim()
    .toLowerCase()
    .replace(/\s+/g, '-')
    .replace(/^[._]/, '')
    .replace(/[^a-z\d-~]+/g, '-')
}

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

function isEmpty(path: string) {
  const files = fs.readdirSync(path)
  return files.length === 0 || (files.length === 1 && files[0] === '.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 })
  }
}

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],
  }
}

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

function editFile(file: string, callback: (content: string) => string) {
  const content = fs.readFileSync(file, 'utf-8')
  fs.writeFileSync(file, callback(content), 'utf-8')
}

以上就是对create-vite中index.ts文件的分析。记录的有点简单,好在代码不难看懂,建议大家自己down下来看一下,地址:vite。create-vite。

下篇开坑我如何从0到1搭建这个项目的# 借鉴create-vite搭建自己的创建项目工具(1)项目搭建与发包。就酱,拜!

相关推荐
修炼的dance12 分钟前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M19 分钟前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写21 分钟前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李22 分钟前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易36 分钟前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马1 小时前
前沿视界
前端
kyriewen1 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易1 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员
IT_陈寒1 小时前
Vue的这个响应式陷阱,差点让我加班到天亮
前端·人工智能·后端
Flynt3 小时前
别写拖拽行为,只声明"能拖到哪":UIST 最佳论文的拖拽方案我实测了
前端·svg·preact