/blog/
/blog/one/
/blog/resume/
/blog/answer/
/blog/code/
/blog/know/
/blog/posts/1109. 航班预订统计/
/blog/posts/1143. 最长公共子序列/
/blog/posts/1221. 分割平衡字符串/
/blog/posts/1436. 旅行终点站/
/blog/posts/1480. 一维数组的动态和/
/blog/posts/1588. 所有奇数长度子数组的和/
/blog/posts/162. 寻找峰值/
/blog/posts/1646. 获取生成数组中的最大值/
/blog/posts/165. 比较版本号/
/blog/posts/166. 分数到小数/
/blog/posts/187. 重复的DNA序列/
/blog/posts/1894. 找到需要补充粉笔的学生编号/
/blog/posts/208. 实现 Trie (前缀树)/
/blog/posts/211. 添加与搜索单词 - 数据结构设计/
/blog/posts/212. 单词搜索 II/
/blog/posts/223. 矩形面积/
/blog/posts/229. 求众数 II/
/blog/posts/230. 二叉搜索树中第K小的元素/
/blog/posts/240. 搜索二维矩阵 II/
/blog/posts/260. 只出现一次的数字 III/
/blog/posts/273. 整数转换英文表示/
/blog/posts/282. 给表达式添加运算符/
/blog/posts/284. 顶端迭代器/
/blog/posts/292. Nim 游戏/
/blog/posts/299. 猜数字游戏/
/blog/posts/301. 删除无效的括号/
/blog/posts/326. 3的幂/
/blog/posts/335. 路径交叉/
/blog/posts/352. 将数据流变为多个不相交区间/
/blog/posts/36. 有效的数独/
/blog/posts/371. 两整数之和/
/blog/posts/38. 外观数列/
/blog/posts/407. 接雨水 II/
/blog/posts/405. 数字转换为十六进制数/
/blog/posts/430. 扁平化多级双向链表/
/blog/posts/437. 路径总和 III/
/blog/posts/447. 回旋镖的数量/
/blog/posts/453. 最小操作次数使数组元素相等/
/blog/posts/470. 用 Rand7() 实现 Rand10()/
/blog/posts/476. 数字的补数/
/blog/posts/502. IPO/
/blog/posts/517. 超级洗衣机/
/blog/posts/524. 通过删除字母匹配到字典里最长单词/
/blog/posts/528. 按权重随机选择/
/blog/posts/600. 不含连续1的非负整数/
/blog/posts/639. 解码方法 II/
/blog/posts/66. 加一/
/blog/posts/678. 有效的括号字符串/
/blog/posts/68. 文本左右对齐/
/blog/posts/704. 二分查找/
/blog/posts/725. 分隔链表/
/blog/posts/789. 逃脱阻碍者/
/blog/posts/787. K 站中转内最便宜的航班/
/blog/posts/869. 重新排序得到 2 的幂/
/blog/posts/881. 救生艇/
/blog/posts/797. 所有可能的路径/
/blog/posts/
/blog/posts/剑指 Offer 10- I. 斐波那契数列/
/blog/posts/剑指 Offer 22. 链表中倒数第k个节点/
/blog/posts/面试题 17.14. 最小K个数/
/blog/workshop/
/blog/answer/common/
/blog/answer/interview/
/blog/answer/invest/
/blog/answer/webRTC/01-前置知识/
/blog/answer/webRTC/02-RTCPeerConnection/
/blog/answer/webRTC/03-实践/
/blog/answer/webRTC/
/blog/code/lodash/01/
/blog/code/lodash/
/blog/code/nuxt/
/blog/code/npm/01-pify/
/blog/code/npm/02-downlaod/
/blog/code/npm/03-video.js/
/blog/code/npm/04-craco/
/blog/code/npm/05-axios/
/blog/code/npm/
/blog/code/opensource/01-第一期/
/blog/code/opensource/02-第二期/
/blog/code/opensource/04-第四期/
/blog/code/opensource/05-第五期/
/blog/code/opensource/07-第七期/
/blog/code/opensource/08-第八期/
/blog/code/opensource/09-第九期/
/blog/code/opensource/10-第十期/
/blog/code/opensource/11-第十一期 玩具vite/
/blog/code/opensource/12-第十二期 ni/
/blog/code/opensource/13-第十三期 open/
/blog/code/opensource/14-第十四期 promisify/
/blog/code/opensource/15-第十五期 element新增组件功能/
/blog/code/opensource/16-第十六期 一行代码统一规范 包管理器/
/blog/code/opensource/17-第十七期 js-cookie/
/blog/code/opensource/18-第十八期 delay/
/blog/code/opensource/21-第未知期/
/blog/code/opensource/
/blog/code/react/
/blog/code/vitepress/
/blog/know/back/
/blog/know/computer/
/blog/know/front/
/blog/know/javascript/
/blog/know/network/01-网路/
/blog/know/network/
/blog/know/tool/
/blog/know/typescript/
/blog/workshop/cli/01-项目创建/
/blog/workshop/cli/02-npm包发布/
/blog/workshop/cli/
/blog/workshop/windows11/
/blog/answer/common/business/vuepress搭建之旅/
/blog/answer/common/business/编写代码之前的思考/
/blog/answer/common/offer/03. 数组中重复的数字/
/blog/answer/common/interview/
/blog/answer/common/offer/04. 二维数组中的查找/
/blog/answer/common/offer/05. 替换空格/
/blog/answer/common/offer/07. 重建二叉树/
/blog/answer/common/offer/06. 从尾到头打印链表/
/blog/answer/common/offer/09. 用两个栈实现队列/
/blog/answer/common/offer/10- I. 斐波那契数列/
/blog/answer/common/offer/10- II. 青蛙跳台阶问题/
/blog/answer/common/offer/11. 旋转数组的最小数字/
/blog/answer/common/offer/
/blog/answer/common/offer/剑指 Offer 10- I. 斐波那契数列/
/blog/answer/common/offer/剑指 Offer II 069. 山峰数组的顶部/
/blog/answer/common/web/
/blog/answer/common/offer/剑指 Offer 22. 链表中倒数第k个节点/
/blog/answer/common/web/拖拽/
/blog/answer/common/web/类型判断/
/blog/answer/invest/book/
/blog/answer/invest/book/不可不知的经济真相/
/blog/answer/invest/book/投资第一课/
/blog/answer/invest/book/纳瓦尔宝典/
/blog/answer/interview/basic/01-html/
/blog/answer/interview/basic/02-css/
/blog/answer/interview/basic/03-javascript/
/blog/answer/interview/basic/
/blog/answer/interview/basic/vue/
/blog/code/react/react/
/blog/answer/webRTC/janus/01-init/
/blog/answer/webRTC/janus/03-attach/
/blog/answer/webRTC/janus/
/blog/code/react/redux/
/blog/code/react/router/01-环境设置/
/blog/answer/webRTC/janus/02-janus/
/blog/code/react/router/
/blog/know/back/Egg/01-基础/
/blog/know/back/Egg/egg兼容mysql和mogodb/
/blog/know/back/Egg/
/blog/know/back/nodejs/01-install/
/blog/know/back/nodejs/98-process/
/blog/know/back/nodejs/99-file/
/blog/know/back/nodejs/
/blog/know/computer/algorithm/01-多选投票算法/
/blog/know/computer/algorithm/02-二叉树的各种遍历/
/blog/know/computer/algorithm/03-位运算/
/blog/know/computer/algorithm/04-距离相关/
/blog/know/computer/algorithm/06-线性表/
/blog/know/computer/algorithm/05-字符串/
/blog/know/computer/algorithm/07-队列/
/blog/know/computer/algorithm/08-栈/
/blog/know/computer/algorithm/09-哈希表/
/blog/know/computer/algorithm/10-dfs/
/blog/know/computer/algorithm/11-bfs/
/blog/know/computer/algorithm/
/blog/know/computer/data/01-队列/
/blog/know/computer/data/03-链表/
/blog/know/computer/data/04-树/
/blog/know/computer/data/05-栈/
/blog/know/computer/data/06-其他/
/blog/know/computer/data/06-堆/
/blog/know/computer/data/
/blog/know/computer/dayOne/1011. 在 D 天内送达包裹的能力/
/blog/know/computer/dayOne/1310. 子数组异或查询/
/blog/know/computer/dayOne/137. 只出现一次的数字 II/
/blog/know/computer/dayOne/1473. 粉刷房子 III/
/blog/know/computer/dayOne/1482. 制作 m 束花所需的最少天数/
/blog/know/computer/dayOne/1486. 数组异或操作/
/blog/know/computer/dayOne/1720. 解码异或后的数组/
/blog/know/computer/dayOne/1723. 完成所有工作的最短时间/
/blog/know/computer/dayOne/1734. 解码异或后的排列/
/blog/know/computer/dayOne/554. 砖墙/
/blog/know/computer/dayOne/633. 平方数之和/
/blog/know/computer/dayOne/690. 员工的重要性/
/blog/know/computer/dayOne/7. 整数反转/
/blog/know/computer/dayOne/403. 青蛙过河/
/blog/know/computer/dayOne/740. 删除并获得点数/
/blog/know/computer/dayOne/872. 叶子相似的树/
/blog/know/computer/dayOne/938. 二叉搜索树的范围和/
/blog/know/computer/dayOne/
/blog/know/computer/network/01-网路协议/
/blog/know/computer/network/98-关于options请求/
/blog/know/computer/network/
/blog/know/engineering/babel/
/blog/know/engineering/npm/78-npm push/
/blog/know/engineering/npm/
/blog/know/engineering/react/01-简介/
/blog/know/engineering/react/03-react-router/
/blog/know/engineering/react/04-hooks/
/blog/know/engineering/react/
/blog/know/engineering/webpack/01-基础/
/blog/know/engineering/webpack/02-loader/
/blog/know/engineering/webpack/03-plugin/
/blog/know/engineering/webpack/
/blog/know/front/css/01-选择器/
/blog/know/front/css/02-盒模型/
/blog/know/front/css/03-布局/
/blog/know/front/css/04-文本属性/
/blog/know/front/css/
/blog/know/front/network/01-网路/
/blog/know/front/network/
/blog/know/front/html/01-head/
/blog/know/front/html/02-body/
/blog/know/front/html/09-canvas/
/blog/know/front/html/10-svg/
/blog/know/front/html/
/blog/know/front/react/01-xx/
/blog/know/front/react/02-props/
/blog/know/front/react/03-state/
/blog/know/front/react/04-Lifecycle/
/blog/know/front/react/05-hook/
/blog/know/front/react/06-redux/
/blog/know/front/react/
/blog/know/front/webRTC/
/blog/know/front/webpack/01-基础/
/blog/know/front/webpack/02-loader/
/blog/know/front/webpack/03-plugin/
/blog/know/front/webpack/
/blog/know/javascript/BOM和DOM/01-navigator/
/blog/know/javascript/BOM和DOM/
/blog/know/javascript/advance/07-迭代器与生成器/
/blog/know/javascript/advance/11-promise/
/blog/know/javascript/advance/12-正则表达式/
/blog/know/javascript/advance/
/blog/know/javascript/basic/02-类/
/blog/know/javascript/basic/03-数据类型/
/blog/know/javascript/basic/04-函数进阶/
/blog/know/javascript/basic/
/blog/know/javascript/api/99-promise/
/blog/know/javascript/basic/01-语言基础/
/blog/know/javascript/basic/05-原型链/
/blog/know/tool/chromedevtools/
/blog/know/tool/git/01-基础/
/blog/know/tool/git/02-log/
/blog/know/tool/git/03-checkout/
/blog/know/tool/git/09-submodule/
/blog/know/tool/git/04-diff/
/blog/know/tool/git/10-workflow/
/blog/know/tool/git/
/blog/know/tool/vscode/
/blog/know/typescript/basic/01-基础概念/
/blog/know/typescript/basic/02-基础类型/
/blog/know/typescript/basic/03-接口/
/blog/know/typescript/basic/04-类/
/blog/know/typescript/basic/05-函数/
/blog/know/typescript/basic/06-泛型/
/blog/know/typescript/basic/07-枚举/
/blog/know/typescript/basic/08-高级类型/
/blog/know/typescript/basic/09-模块/
/blog/know/typescript/basic/10-模块解析/
/blog/know/typescript/basic/11-命名空间/
/blog/know/typescript/basic/
/blog/know/typescript/declarationfiles/01-示例/
/blog/know/typescript/declarationfiles/02-结构/
/blog/know/typescript/declarationfiles/03-模板/
/blog/know/typescript/declarationfiles/04-最佳实践/
/blog/know/typescript/declarationfiles/05-深入/
/blog/know/typescript/declarationfiles/

create-vue Vue 团队公开的全新脚手架工具

美国时间 2021 年 10 月 7 日早晨,Vue 团队等主要贡献者举办了一个 Vue Contributor Days 在线会议,蒋豪群(知乎胖茶,Vue.js 官方团队成员,Vue-CLI 核心开发),在会上公开了create-vue,一个全新的脚手架工具。

craete vue

An easy way to start a Vue project

npm init vue@next

npm init

npm init 文档

执行 npm init vue@next, 相当于执行 npx create-vue@next

源码解析

克隆下项目仓库

git clone https://github.com/vuejs/create-vue

package.json

bin:

"bin": {
    "create-vue": "outfile.cjs"
},

bin指的可执行二进制文件,这也是我们使用 create-vue的原因。

scripts:

"scripts": {
    "prepare": "husky install",
    "format": "prettier --write .",
    "build": "esbuild --bundle index.js --format=cjs --platform=node --outfile=outfile.cjs",
    "snapshot": "node snapshot.js",
    "pretest": "run-s build snapshot",
    "test": "node test.js",
    "prepublishOnly": "run-s build snapshot"
},

index.js

:::details 源码

#!/usr/bin/env node
// @ts-check

import fs from 'fs'
import path from 'path'

import minimist from 'minimist'
import prompts from 'prompts'
import { red, green, bold } from 'kolorist'

import renderTemplate from './utils/renderTemplate.js'
import { postOrderDirectoryTraverse, preOrderDirectoryTraverse } from './utils/directoryTraverse.js'
import generateReadme from './utils/generateReadme.js'
import getCommand from './utils/getCommand.js'

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 canSafelyOverwrite(dir) {
  return !fs.existsSync(dir) || fs.readdirSync(dir).length === 0
}

function emptyDir(dir) {
  postOrderDirectoryTraverse(
    dir,
    (dir) => fs.rmdirSync(dir),
    (file) => fs.unlinkSync(file)
  )
}

async function init() {
  const cwd = process.cwd()
  // possible options:
  // --default
  // --typescript / --ts
  // --jsx
  // --router / --vue-router
  // --vuex
  // --with-tests / --tests / --cypress
  // --force (for force overwriting)
  const argv = minimist(process.argv.slice(2), {
    alias: {
      typescript: ['ts'],
      'with-tests': ['tests', 'cypress'],
      router: ['vue-router']
    },
    // all arguments are treated as booleans
    boolean: true
  })

  // if any of the feature flags is set, we would skip the feature prompts
  // use `??` instead of `||` once we drop Node.js 12 support
  const isFeatureFlagsUsed =
    typeof (argv.default || argv.ts || argv.jsx || argv.router || argv.vuex || argv.tests) ===
    'boolean'

  let targetDir = argv._[0]
  const defaultProjectName = !targetDir ? 'vue-project' : targetDir

  const forceOverwrite = argv.force

  let result = {}

  try {
    // Prompts:
    // - Project name:
    //   - whether to overwrite the existing directory or not?
    //   - enter a valid package name for package.json
    // - Project language: JavaScript / TypeScript
    // - Add JSX Support?
    // - Install Vue Router for SPA development?
    // - Install Vuex for state management? (TODO)
    // - Add Cypress for testing?
    result = await prompts(
      [
        {
          name: 'projectName',
          type: targetDir ? null : 'text',
          message: 'Project name:',
          initial: defaultProjectName,
          onState: (state) => (targetDir = String(state.value).trim() || defaultProjectName)
        },
        {
          name: 'shouldOverwrite',
          type: () => (canSafelyOverwrite(targetDir) || forceOverwrite ? null : 'confirm'),
          message: () => {
            const dirForPrompt =
              targetDir === '.' ? 'Current directory' : `Target directory "${targetDir}"`

            return `${dirForPrompt} is not empty. Remove existing files and continue?`
          }
        },
        {
          name: 'overwriteChecker',
          type: (prev, values = {}) => {
            if (values.shouldOverwrite === false) {
              throw new Error(red('✖') + ' Operation cancelled')
            }
            return null
          }
        },
        {
          name: 'packageName',
          type: () => (isValidPackageName(targetDir) ? null : 'text'),
          message: 'Package name:',
          initial: () => toValidPackageName(targetDir),
          validate: (dir) => isValidPackageName(dir) || 'Invalid package.json name'
        },
        {
          name: 'needsTypeScript',
          type: () => (isFeatureFlagsUsed ? null : 'toggle'),
          message: 'Add TypeScript?',
          initial: false,
          active: 'Yes',
          inactive: 'No'
        },
        {
          name: 'needsJsx',
          type: () => (isFeatureFlagsUsed ? null : 'toggle'),
          message: 'Add JSX Support?',
          initial: false,
          active: 'Yes',
          inactive: 'No'
        },
        {
          name: 'needsRouter',
          type: () => (isFeatureFlagsUsed ? null : 'toggle'),
          message: 'Add Vue Router for Single Page Application development?',
          initial: false,
          active: 'Yes',
          inactive: 'No'
        },
        {
          name: 'needsVuex',
          type: () => (isFeatureFlagsUsed ? null : 'toggle'),
          message: 'Add Vuex for state management?',
          initial: false,
          active: 'Yes',
          inactive: 'No'
        },
        {
          name: 'needsTests',
          type: () => (isFeatureFlagsUsed ? null : 'toggle'),
          message: 'Add Cypress for testing?',
          initial: false,
          active: 'Yes',
          inactive: 'No'
        }
      ],
      {
        onCancel: () => {
          throw new Error(red('✖') + ' Operation cancelled')
        }
      }
    )
  } catch (cancelled) {
    console.log(cancelled.message)
    process.exit(1)
  }

  // `initial` won't take effect if the prompt type is null
  // so we still have to assign the default values here
  const {
    packageName = toValidPackageName(defaultProjectName),
    shouldOverwrite,
    needsJsx = argv.jsx,
    needsTypeScript = argv.typescript,
    needsRouter = argv.router,
    needsVuex = argv.vuex,
    needsTests = argv.tests
  } = result
  const root = path.join(cwd, targetDir)

  if (shouldOverwrite) {
    emptyDir(root)
  } else if (!fs.existsSync(root)) {
    fs.mkdirSync(root)
  }

  console.log(`\nScaffolding project in ${root}...`)

  const pkg = { name: packageName, version: '0.0.0' }
  fs.writeFileSync(path.resolve(root, 'package.json'), JSON.stringify(pkg, null, 2))

  // todo:
  // work around the esbuild issue that `import.meta.url` cannot be correctly transpiled
  // when bundling for node and the format is cjs
  // const templateRoot = new URL('./template', import.meta.url).pathname
  const templateRoot = path.resolve(__dirname, 'template')
  const render = function render(templateName) {
    const templateDir = path.resolve(templateRoot, templateName)
    renderTemplate(templateDir, root)
  }

  // Render base template
  render('base')

  // Add configs.
  if (needsJsx) {
    render('config/jsx')
  }
  if (needsRouter) {
    render('config/router')
  }
  if (needsVuex) {
    render('config/vuex')
  }
  if (needsTests) {
    render('config/cypress')
  }
  if (needsTypeScript) {
    render('config/typescript')
  }

  // Render code template.
  // prettier-ignore
  const codeTemplate =
    (needsTypeScript ? 'typescript-' : '') +
    (needsRouter ? 'router' : 'default')
  render(`code/${codeTemplate}`)

  // Render entry file (main.js/ts).
  if (needsVuex && needsRouter) {
    render('entry/vuex-and-router')
  } else if (needsVuex) {
    render('entry/vuex')
  } else if (needsRouter) {
    render('entry/router')
  } else {
    render('entry/default')
  }

  // Cleanup.

  if (needsTypeScript) {
    // rename all `.js` files to `.ts`
    // rename jsconfig.json to tsconfig.json
    preOrderDirectoryTraverse(
      root,
      () => {},
      (filepath) => {
        if (filepath.endsWith('.js')) {
          fs.renameSync(filepath, filepath.replace(/\.js$/, '.ts'))
        } else if (path.basename(filepath) === 'jsconfig.json') {
          fs.renameSync(filepath, filepath.replace(/jsconfig\.json$/, 'tsconfig.json'))
        }
      }
    )

    // Rename entry in `index.html`
    const indexHtmlPath = path.resolve(root, 'index.html')
    const indexHtmlContent = fs.readFileSync(indexHtmlPath, 'utf8')
    fs.writeFileSync(indexHtmlPath, indexHtmlContent.replace('src/main.js', 'src/main.ts'))
  }

  if (!needsTests) {
    // All templates assumes the need of tests.
    // If the user doesn't need it:
    // rm -rf cypress **/__tests__/
    preOrderDirectoryTraverse(
      root,
      (dirpath) => {
        const dirname = path.basename(dirpath)

        if (dirname === 'cypress' || dirname === '__tests__') {
          emptyDir(dirpath)
          fs.rmdirSync(dirpath)
        }
      },
      () => {}
    )
  }

  // Instructions:
  // Supported package managers: pnpm > yarn > npm
  // Note: until <https://github.com/pnpm/pnpm/issues/3505> is resolved,
  // it is not possible to tell if the command is called by `pnpm init`.
  const packageManager = /pnpm/.test(process.env.npm_execpath)
    ? 'pnpm'
    : /yarn/.test(process.env.npm_execpath)
    ? 'yarn'
    : 'npm'

  // README generation
  fs.writeFileSync(
    path.resolve(root, 'README.md'),
    generateReadme({
      projectName: result.projectName || defaultProjectName,
      packageManager,
      needsTypeScript,
      needsTests
    })
  )

  console.log(`\nDone. Now run:\n`)
  if (root !== cwd) {
    console.log(`  ${bold(green(`cd ${path.relative(cwd, root)}`))}`)
  }
  console.log(`  ${bold(green(getCommand(packageManager, 'install')))}`)
  console.log(`  ${bold(green(getCommand(packageManager, 'dev')))}`)
  console.log()
}

init().catch((e) => {
  console.error(e)
})

:::

可运行主要函数:

init().catch((e) => {
  console.error(e)
})

init函数

::: details init 函数

async function init(){
  const cwd = process.cwd()
  // possible options:
  // --default
  // --typescript / --ts
  // --jsx
  // --router / --vue-router
  // --vuex
  // --with-tests / --tests / --cypress
  // --force (for force overwriting)

  // 拿到 命令行中的参数
  const argv = minimist(process.argv.slice(2), {
    alias: {
      typescript: ['ts'],
      'with-tests': ['tests', 'cypress'],
      router: ['vue-router']
    },
    // all arguments are treated as booleans
    boolean: true
  })

  // if any of the feature flags is set, we would skip the feature prompts
  // use `??` instead of `||` once we drop Node.js 12 support
  const isFeatureFlagsUsed =
    typeof (argv.default || argv.ts || argv.jsx || argv.router || argv.vuex || argv.tests) ===
    'boolean'

  let targetDir = argv._[0]
  const defaultProjectName = !targetDir ? 'vue-project' : targetDir

  const forceOverwrite = argv.force

  let result = {}

  try {
    // Prompts:
    // - Project name:
    //   - whether to overwrite the existing directory or not?
    //   - enter a valid package name for package.json
    // - Project language: JavaScript / TypeScript
    // - Add JSX Support?
    // - Install Vue Router for SPA development?
    // - Install Vuex for state management? (TODO)
    // - Add Cypress for testing?
    result = await prompts(
      [
        {
          name: 'projectName',
          type: targetDir ? null : 'text',
          message: 'Project name:',
          initial: defaultProjectName,
          onState: (state) => (targetDir = String(state.value).trim() || defaultProjectName)
        },
        {
          name: 'shouldOverwrite',
          type: () => (canSafelyOverwrite(targetDir) || forceOverwrite ? null : 'confirm'),
          message: () => {
            const dirForPrompt =
              targetDir === '.' ? 'Current directory' : `Target directory "${targetDir}"`

            return `${dirForPrompt} is not empty. Remove existing files and continue?`
          }
        },
        {
          name: 'overwriteChecker',
          type: (prev, values = {}) => {
            if (values.shouldOverwrite === false) {
              throw new Error(red('✖') + ' Operation cancelled')
            }
            return null
          }
        },
        {
          name: 'packageName',
          type: () => (isValidPackageName(targetDir) ? null : 'text'),
          message: 'Package name:',
          initial: () => toValidPackageName(targetDir),
          validate: (dir) => isValidPackageName(dir) || 'Invalid package.json name'
        },
        {
          name: 'needsTypeScript',
          type: () => (isFeatureFlagsUsed ? null : 'toggle'),
          message: 'Add TypeScript?',
          initial: false,
          active: 'Yes',
          inactive: 'No'
        },
        {
          name: 'needsJsx',
          type: () => (isFeatureFlagsUsed ? null : 'toggle'),
          message: 'Add JSX Support?',
          initial: false,
          active: 'Yes',
          inactive: 'No'
        },
        {
          name: 'needsRouter',
          type: () => (isFeatureFlagsUsed ? null : 'toggle'),
          message: 'Add Vue Router for Single Page Application development?',
          initial: false,
          active: 'Yes',
          inactive: 'No'
        },
        {
          name: 'needsVuex',
          type: () => (isFeatureFlagsUsed ? null : 'toggle'),
          message: 'Add Vuex for state management?',
          initial: false,
          active: 'Yes',
          inactive: 'No'
        },
        {
          name: 'needsTests',
          type: () => (isFeatureFlagsUsed ? null : 'toggle'),
          message: 'Add Cypress for testing?',
          initial: false,
          active: 'Yes',
          inactive: 'No'
        }
      ],
      {
        onCancel: () => {
          throw new Error(red('✖') + ' Operation cancelled')
        }
      }
    )
  } catch (cancelled) {
    console.log(cancelled.message)
    process.exit(1)
  }

  // `initial` won't take effect if the prompt type is null
  // so we still have to assign the default values here
  const {
    packageName = toValidPackageName(defaultProjectName),
    shouldOverwrite,
    needsJsx = argv.jsx,
    needsTypeScript = argv.typescript,
    needsRouter = argv.router,
    needsVuex = argv.vuex,
    needsTests = argv.tests
  } = result
  const root = path.join(cwd, targetDir)

  if (shouldOverwrite) {
    emptyDir(root)
  } else if (!fs.existsSync(root)) {
    fs.mkdirSync(root)
  }

  console.log(`\nScaffolding project in ${root}...`)

  const pkg = { name: packageName, version: '0.0.0' }
  fs.writeFileSync(path.resolve(root, 'package.json'), JSON.stringify(pkg, null, 2))

  // todo:
  // work around the esbuild issue that `import.meta.url` cannot be correctly transpiled
  // when bundling for node and the format is cjs
  // const templateRoot = new URL('./template', import.meta.url).pathname
  const templateRoot = path.resolve(__dirname, 'template')
  const render = function render(templateName) {
    const templateDir = path.resolve(templateRoot, templateName)
    renderTemplate(templateDir, root)
  }

  // Render base template
  render('base')

  // Add configs.
  if (needsJsx) {
    render('config/jsx')
  }
  if (needsRouter) {
    render('config/router')
  }
  if (needsVuex) {
    render('config/vuex')
  }
  if (needsTests) {
    render('config/cypress')
  }
  if (needsTypeScript) {
    render('config/typescript')
  }

  // Render code template.
  // prettier-ignore
  const codeTemplate =
    (needsTypeScript ? 'typescript-' : '') +
    (needsRouter ? 'router' : 'default')
  render(`code/${codeTemplate}`)

  // Render entry file (main.js/ts).
  if (needsVuex && needsRouter) {
    render('entry/vuex-and-router')
  } else if (needsVuex) {
    render('entry/vuex')
  } else if (needsRouter) {
    render('entry/router')
  } else {
    render('entry/default')
  }

  // Cleanup.

  if (needsTypeScript) {
    // rename all `.js` files to `.ts`
    // rename jsconfig.json to tsconfig.json
    preOrderDirectoryTraverse(
      root,
      () => {},
      (filepath) => {
        if (filepath.endsWith('.js')) {
          fs.renameSync(filepath, filepath.replace(/\.js$/, '.ts'))
        } else if (path.basename(filepath) === 'jsconfig.json') {
          fs.renameSync(filepath, filepath.replace(/jsconfig\.json$/, 'tsconfig.json'))
        }
      }
    )

    // Rename entry in `index.html`
    const indexHtmlPath = path.resolve(root, 'index.html')
    const indexHtmlContent = fs.readFileSync(indexHtmlPath, 'utf8')
    fs.writeFileSync(indexHtmlPath, indexHtmlContent.replace('src/main.js', 'src/main.ts'))
  }

  if (!needsTests) {
    // All templates assumes the need of tests.
    // If the user doesn't need it:
    // rm -rf cypress **/__tests__/
    preOrderDirectoryTraverse(
      root,
      (dirpath) => {
        const dirname = path.basename(dirpath)

        if (dirname === 'cypress' || dirname === '__tests__') {
          emptyDir(dirpath)
          fs.rmdirSync(dirpath)
        }
      },
      () => {}
    )
  }

  // Instructions:
  // Supported package managers: pnpm > yarn > npm
  // Note: until <https://github.com/pnpm/pnpm/issues/3505> is resolved,
  // it is not possible to tell if the command is called by `pnpm init`.
  const packageManager = /pnpm/.test(process.env.npm_execpath)
    ? 'pnpm'
    : /yarn/.test(process.env.npm_execpath)
    ? 'yarn'
    : 'npm'

  // README generation
  fs.writeFileSync(
    path.resolve(root, 'README.md'),
    generateReadme({
      projectName: result.projectName || defaultProjectName,
      packageManager,
      needsTypeScript,
      needsTests
    })
  )

  console.log(`\nDone. Now run:\n`)
  if (root !== cwd) {
    console.log(`  ${bold(green(`cd ${path.relative(cwd, root)}`))}`)
  }
  console.log(`  ${bold(green(getCommand(packageManager, 'install')))}`)
  console.log(`  ${bold(green(getCommand(packageManager, 'dev')))}`)
  console.log()
}

:::

拿到命令行中的参数

  // process.cwd() 方法返回 Node.js 进程的当前工作目录。
  const cwd = process.cwd()
  // 可出现的命令选项:
  // --default
  // --typescript / --ts
  // --jsx
  // --router / --vue-router
  // --vuex
  // --with-tests / --tests / --cypress
  // --force (for force overwriting)

  // process.argv 属性返回数组,其中包含启动 Node.js 进程时传入的命令行参数。
  //  第一个元素将是 process.execPath。 如果需要访问 argv[0] 的原始值,请参阅 process.argv0。 
  // 第二个元素将是正在执行的 JavaScript 文件的路径。 其余元素将是任何其他命令行参数。
  const argv = minimist(process.argv.slice(2), {
    alias: {
      typescript: ['ts'],
      'with-tests': ['tests', 'cypress'],
      router: ['vue-router']
    },
    // all arguments are treated as booleans
    boolean: true
  })

  // if any of the feature flags is set, we would skip the feature prompts
  // use `??` instead of `||` once we drop Node.js 12 support
  const isFeatureFlagsUsed =
    typeof (argv.default || argv.ts || argv.jsx || argv.router || argv.vuex || argv.tests) ===
    'boolean'
  // argv._ 包含了所有未被关联的参数命令
  let targetDir = argv._[0]
  // 默认项目文件名
  const defaultProjectName = !targetDir ? 'vue-project' : targetDir
  // 强制,则意味着覆盖文件
  const forceOverwrite = argv.force

minimist,用于解析命令中的参数。

prompts 命令行交互式获取参数

这一步,根据npm init vue@next式出现的交互信息:

  1. 项目的文件名 projectName
  2. 是否需要 typescript
  3. 是否需要jsx
  4. 是否需要router
  5. 是否需要vuex
  6. 是否需要测试

通过 prompts 进行交互后,将数据 写入 result 中。

根据命令行和交互中获取的信息进行创建

这里涉及到了 解构赋值并赋初始值的 语法。

    //关于 解构并赋初始值
    const {
        packageName = toValidPackageName(defaultProjectName),
        shouldOverwrite,
        needsJsx = argv.jsx,
        needsTypeScript = argv.typescript,
        needsRouter = argv.router,
        needsVuex = argv.vuex,
        needsTests = argv.tests
    } = result
    // 拿到 项目 文件夹目录
    const root = path.join(cwd, targetDir)
    // 判断是否要重写
    if (shouldOverwrite) {
        emptyDir(root)
        // 判断文件夹是否已存在
    } else if (!fs.existsSync(root)) {
        // 同步方式创建一个文件夹
        fs.mkdirSync(root)
    }
    console.log(`\nScaffolding project in ${root}...`)

根据参数渲染配置文件

这里主要的函数 renderTemplate,会在后面进行介绍。

    const pkg = { name: packageName, version: '0.0.0' }
    // 写入  package.json 文件夹
    fs.writeFileSync(path.resolve(root, 'package.json'), JSON.stringify(pkg, null, 2))

    const templateRoot = path.resolve(__dirname, 'template')
    // 拼接 模板路径 调用  renderTemplate 进行渲染
    const render = function render(templateName) {
        const templateDir = path.resolve(templateRoot, templateName)
        renderTemplate(templateDir, root)
    }

    // Render   template
    render('base')

    // Add configs. 渲染添加配置文件
    if (needsJsx) {
        render('config/jsx')
    }
    if (needsRouter) {
        render('config/router')
    }
    if (needsVuex) {
        render('config/vuex')
    }
    if (needsTests) {
        render('config/cypress')
    }
    if (needsTypeScript) {
        render('config/typescript')
    }

渲染代码

这里根据参数去添加对应的配置文件

    // prettier-ignore
    // 渲染模板代码
    const codeTemplate =   (needsTypeScript ? 'typescript-' : '') + (needsRouter ? 'router' : 'default')
    render(`code/${codeTemplate}`)

    // Render entry file (main.js/ts).
    if (needsVuex && needsRouter) {
        render('entry/vuex-and-router')
    } else if (needsVuex) {
        render('entry/vuex')
    } else if (needsRouter) {
        render('entry/router')
    } else {
        render('entry/default')
    }

ts项目的处理

如果需要使用到TS,这里会将前面渲染得到的所有js文件,将后缀替换TS,同时将 jsconfig.json 文件名替换成 tsconfig.json

重新修改 index.html 中 mian.js 的 文件。

    if (needsTypeScript) {
    // rename all `.js` files to `.ts`
    // rename jsconfig.json to tsconfig.json
    preOrderDirectoryTraverse(
      root,
      () => {},
      (filepath) => {
        if (filepath.endsWith('.js')) {
          fs.renameSync(filepath, filepath.replace(/\.js$/, '.ts'))
        } else if (path.basename(filepath) === 'jsconfig.json') {
          fs.renameSync(filepath, filepath.replace(/jsconfig\.json$/, 'tsconfig.json'))
        }
      }
    )

    // Rename entry in `index.html`
    const indexHtmlPath = path.resolve(root, 'index.html')
    const indexHtmlContent = fs.readFileSync(indexHtmlPath, 'utf8')
    fs.writeFileSync(indexHtmlPath, indexHtmlContent.replace('src/main.js', 'src/main.ts'))
  }

项目测试的处理

一般所有渲染,会自带测试,如果不需要,会进行删除。

    if (!needsTests) {
    // All templates assumes the need of tests.
    // If the user doesn't need it:
    // rm -rf cypress **/__tests__/
    preOrderDirectoryTraverse(
      root,
      (dirpath) => {
        const dirname = path.basename(dirpath)

        if (dirname === 'cypress' || dirname === '__tests__') {
          emptyDir(dirpath)
          fs.rmdirSync(dirpath)
        }
      },
      () => {}
    )
  }

包管理的处理

支持的包管理: pnpm > yarn > npm

pnpm npm库

    // Instructions:
  // Supported package managers: pnpm > yarn > npm
  // Note: until <https://github.com/pnpm/pnpm/issues/3505> is resolved,
  // it is not possible to tell if the command is called by `pnpm init`.
  const packageManager = /pnpm/.test(process.env.npm_execpath)
    ? 'pnpm'
    : /yarn/.test(process.env.npm_execpath)
    ? 'yarn'
    : 'npm'

  // README generation
  fs.writeFileSync(
    path.resolve(root, 'README.md'),
    generateReadme({
      projectName: result.projectName || defaultProjectName,
      packageManager,
      needsTypeScript,
      needsTests
    })
  )

关于包名

isValidPackageName 这个函数用于检验包名是否合法。

function isValidPackageName(projectName) {
  // 正则表达式
  return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(projectName)
}

toValidPackageName 这个函数用于将项目名变得合法

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

postOrderDirectoryTraverse 与 preOrderDirectoryTraverse

这两个函数在 utils/directoryTraverse.js 中, 同时根据函数名的推测,一个是后序遍历文件夹,一个是先序遍历文件夹。

postOrderDirectoryTraverse

function postOrderDirectoryTraverse(dir, dirCallback, fileCallback) {
  for (const filename of fs.readdirSync(dir)) {
    // 得到路径
    const fullpath = path.resolve(dir, filename)
    // 判断路径是否为文件夹,如果是则递归调用, 然后再调用 dirCall
    if (fs.lstatSync(fullpath).isDirectory()) {
      postOrderDirectoryTraverse(fullpath, dirCallback, fileCallback)
      dirCallback(fullpath)
      continue
    }
    fileCallback(fullpath)
  }
}

preOrderDirectoryTraverse

function preOrderDirectoryTraverse(dir, dirCallback, fileCallback) {
  for (const filename of fs.readdirSync(dir)) {
    const fullpath = path.resolve(dir, filename)
    if (fs.lstatSync(fullpath).isDirectory()) {
      // 先调用 dirCall 再递归
      dirCallback(fullpath)
      // in case the dirCallback removes the directory entirely
      if (fs.existsSync(fullpath)) {
        preOrderDirectoryTraverse(fullpath, dirCallback, fileCallback)
      }
      continue
    }
    fileCallback(fullpath)
  }
}

emptyDir

这里调用前面了 postOrderDirectoryTraverse 函数, 先删除了文件,再删除了文件夹,已达到清空路径的作用。

function emptyDir(dir) {
  postOrderDirectoryTraverse(
    dir,
    (dir) => fs.rmdirSync(dir),
    (file) => fs.unlinkSync(file)
  )
}

renderTemplate

最重要的函数之一,生成函数代码和文件,基本上都是它来完成的,代码在 utils/renderTemplate.js

import fs from 'fs'
import path from 'path'

import deepMerge from './deepMerge.js'
import sortDependencies from './sortDependencies.js'

/**
 * Renders a template folder/file to the file system,
 * by recursively copying all files under the `src` directory,
 * with the following exception:
 *   - `_filename` should be renamed to `.filename`
 *   - Fields in `package.json` should be recursively merged
 * @param {string} src source filename to copy
 * @param {string} dest destination filename of the copy operation
 */
function renderTemplate(src, dest) {
  const stats = fs.statSync(src)

  if (stats.isDirectory()) {
    // if it's a directory, render its subdirectories and files recusively
    fs.mkdirSync(dest, { recursive: true })
    for (const file of fs.readdirSync(src)) {
      renderTemplate(path.resolve(src, file), path.resolve(dest, file))
    }
    return
  }
  // path.basename() 方法返回 path 的最后一部分 即 最后一个 / 后面的东西
  const filename = path.basename(src)
  
  // 如果 packjson.json 文件已存在,则进行合并而不是重写
  if (filename === 'package.json' && fs.existsSync(dest)) {
    // merge instead of overwriting

    // 因为 package.json 文件,所以可以进行转换成 js 对象
    const existing = JSON.parse(fs.readFileSync(dest))
    const newPackage = JSON.parse(fs.readFileSync(src))
    const pkg = sortDependencies(deepMerge(existing, newPackage))
    // 写入新的package.json文件
    fs.writeFileSync(dest, JSON.stringify(pkg, null, 2) + '\n')
    return
  }

  if (filename.startsWith('_')) {
    // rename `_file` to `.file`
    dest = path.resolve(path.dirname(dest), filename.replace(/^_/, '.'))
  }
  
  // 把 src 文件拷贝到 dest
  fs.copyFileSync(src, dest)
}

export default renderTemplate

deepMerge

const isObject = (val) => val && typeof val === 'object'
// 去重
const mergeArrayWithDedupe = (a, b) => Array.from(new Set([...a, ...b]))
/**
 * Recursively merge the content of the new object to the existing one
 * @param {Object} target the existing object
 * @param {Object} obj the new object
 */
function deepMerge(target, obj) {
  for (const key of Object.keys(obj)) {
    const oldVal = target[key]
    const newVal = obj[key]

    if (Array.isArray(oldVal) && Array.isArray(newVal)) {
      // 去重合并
      target[key] = mergeArrayWithDedupe(oldVal, newVal)
    } else if (isObject(oldVal) && isObject(newVal)) {
      // 如果是对象则递归调用  
      target[key] = deepMerge(oldVal, newVal)
    } else {
      // 如果都不是,则直接用新值覆盖
      target[key] = newVal
    }
  }

  return target
}

sortDependencies

根据 depTypes 中数组的顺序将 json文件的依赖排序。

function sortDependencies(packageJson) {
  const sorted = {}

  const depTypes = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']

  for (const depType of depTypes) {
    if (packageJson[depType]) {
      sorted[depType] = {}

      Object.keys(packageJson[depType])
        .sort()
        .forEach((name) => {
          sorted[depType][name] = packageJson[depType][name]
        })
    }
  }

  return {
    ...packageJson,
    ...sorted
  }
}

test

关于测试

  "scripts": {
    "snapshot": "node snapshot.js",
    "pretest": "run-s build snapshot",
    "test": "node test.js",
  },

test.js 文件

借助 子线程 对 生成在 palyground 文件夹下面的 with-tests的目录,使用 如下两个命令进行 test和 e2e的测试。

pnpm test:unit:ci
pnpm test:e2e:ci
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'

import { spawnSync } from 'child_process'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const playgroundDir = path.resolve(__dirname, './playground/')

for (const projectName of fs.readdirSync(playgroundDir)) {
  if (projectName.endsWith('with-tests')) {
    console.log(`Running unit tests in ${projectName}`)
    const unitTestResult = spawnSync('pnpm', ['test:unit:ci'], {
      cwd: path.resolve(playgroundDir, projectName),
      stdio: 'inherit',
      shell: true
    })
    if (unitTestResult.status !== 0) {
      throw new Error(`Unit tests failed in ${projectName}`)
    }

    console.log(`Running e2e tests in ${projectName}`)
    const e2eTestResult = spawnSync('pnpm', ['test:e2e:ci'], {
      cwd: path.resolve(playgroundDir, projectName),
      stdio: 'inherit',
      shell: true
    })
    if (e2eTestResult.status !== 0) {
      throw new Error(`E2E tests failed in ${projectName}`)
    }
  }
}

snapshot.js

根据flags生成对应的项目快照,便于测试

import { spawnSync } from 'child_process'
import path from 'path'

const __dirname = path
  .dirname(new URL(import.meta.url).pathname)
  .substring(process.platform === 'win32' ? 1 : 0)

const bin = path.resolve(__dirname, './outfile.cjs')
const playgroundDir = path.resolve(__dirname, './playground/')

function createProjectWithFeatureFlags(flags) {
  const projectName = flags.join('-')
  console.log(`Creating project ${projectName}`)
  const { status } = spawnSync(
    'node',
    [bin, projectName, ...flags.map((flag) => `--${flag}`), '--force'],
    {
      cwd: playgroundDir,
      stdio: ['pipe', 'pipe', 'inherit']
    }
  )

  if (status !== 0) {
    process.exit(status)
  }
}

const featureFlags = ['typescript', 'jsx', 'router', 'vuex', 'with-tests']

// The following code & comments are generated by GitHub CoPilot.
function fullCombination(arr) {
  const combinations = []

  // for an array of 5 elements, there are 2^5 - 1= 31 combinations
  // (excluding the empty combination)
  // equivalent to the following:
  // [0, 0, 0, 0, 1] ... [1, 1, 1, 1, 1]
  // We can represent the combinations as a binary number
  // where each digit represents a flag
  // and the number is the index of the flag
  // e.g.
  // [0, 0, 0, 0, 1] = 0b0001
  // [1, 1, 1, 1, 1] = 0b1111

  // Note we need to exclude the empty comination in our case
  for (let i = 1; i < 1 << arr.length; i++) {
    const combination = []
    for (let j = 0; j < arr.length; j++) {
      if (i & (1 << j)) {
        combination.push(arr[j])
      }
    }
    combinations.push(combination)
  }

  return combinations
}

const flagCombinations = fullCombination(featureFlags)
flagCombinations.push(['default'])

for (const flags of flagCombinations) {
  createProjectWithFeatureFlags(flags)
}

收获

这一次的源码阅读的收获还是挺多的。

  1. 对一些文件,文件夹的操作有了基本的参考面,了解了一些nodejs中对fs文件操作的API
  2. 关于 minist,promtps有了一定的了解
  3. 了解到了新的pnpm包管理器。。
  4. 项目快照的生成和对应测试用例的编写。

参考资料