17

1 Introduction

Hello everyone, my name is . Welcome to pay attention to my public , and recently organized source code reading activity , if you are interested, join for more than two months. .

I want to learn the source code, I highly recommend the "Learning Source Code Overall Architecture Series" contains jQuery , underscore , lodash , vuex , sentry , axios , redux , koa , vue-devtools , vuex4 , koa-compose , vue-next-release , vue-this article.

US Time October 7, 2021 morning, Vue team organized a major contributor Vue Contributor Days online meetings, Jiang Hao group ( know almost fat tea , Vue.js official team members, Vue-CLI core development), will be in create-vue , a brand-new scaffolding tool is published on.

create-vue Using the npm init vue@next line of commands, can initialize the Vue3 project based on vite as fast as lightning.

This article is to learn this more than 300 lines of source code with everyone through debugging.

After reading this article, you will learn:

1. 学会全新的官方脚手架工具 create-vue 的使用和原理
2. 学会使用 VSCode 直接打开 github 项目
3. 学会使用测试用例调试源码
4. 学以致用,为公司初始化项目写脚手架工具。
5. 等等

2. Use npm init vue@next to initialize the vue3 project

create-vue github README is written, An easy way to start a Vue project . A simple way to initialize a vue project.

npm init vue@next

It is estimated that most readers, the first reaction is This is also possible, so simple and fast?

I can't help but want to start outputting commands in the console. I tried it in the terminal. See the picture below.

npm init vue@next

Finally cd vue3-project , npm install , npm run dev open the page http://localhost:3000 .

初始化页面

2.1 npm init && npx

Why npm init can also directly initialize a project, with questions, let's look at the npm document.

npm init

npm init usage:

npm init [--force|-f|--yes|-y|--scope]
npm init <@scope> (same as `npx <@scope>/create`)
npm init [<@scope>/]<name> (same as `npx [<@scope>/]create-<name>`)

npm init <initializer> is converted into npx command:

  • npm init foo -> npx create-foo
  • npm init @usr/foo -> npx @usr/create-foo
  • npm init @usr -> npx @usr/create

After reading the document, we also understand:

# 运行
npm init vue@next
# 相当于
npx create-vue@next

We can find some information create-vue Or find the version and other information npm create-vue

Among them, @next is the specified version. You can see npm dist-tag ls create-vue next version currently corresponds to 3.0.0-beta.6 .

npm dist-tag ls create-vue
- latest: 3.0.0-beta.6
- next: 3.0.0-beta.6

At the time of publication, npm publish --tag next specifies tag . The default label is latest .

Some readers may be npx , then find Yifeng’s blog introduction 16177997620712, nodejs.cn

is a very powerful command, available from version 5.2 of npm (released in July 2017).

Simply put, it is easy to ignore and commonly used scenes, npx a bit similar to the free-use and go-go proposed by the applet.

Run local commands easily

node_modules/.bin/vite -v
# vite/2.6.5 linux-x64 node-v14.16.0

# 等同于
# package.json script: "vite -v"
# npm run vite

npx vite -v
# vite/2.6.5 linux-x64 node-v14.16.0

Use different Node.js versions to run code
In some scenarios, you can temporarily switch the node version, which is sometimes more nvm package management.

npx node@14 -v
# v14.18.0

npx -p node@14 node -v 
# v14.18.0

Command execution without installation

# 启动本地静态服务
npx http-server
# 无需全局安装
npx @vue/cli create vue-project
# @vue/cli 相比 npm init vue@next npx create-vue@next 很慢。

# 全局安装
npm i -g @vue/cli
vue create vue-project

npx vue-cli

npm init vue@next ( npx create-vue@next ) The main reason for the speed is that it has less dependency (you can not rely on the package and does not depend on it), and the number of source code lines is small. At present, index.js only has more than 300 lines.

3. Configure environment debugging source code

3.1 Clone the create-vue project

This article warehouse address create-vue-analysis , ask for a star ~

# 可以直接克隆我的仓库,我的仓库保留的 create-vue 仓库的 git 记录
git clone https://github.com/lxchuan12/create-vue-analysis.git
cd create-vue-analysis/create-vue
npm i

Of course, you can open my warehouse VSCode
Open in Visual Studio Code

By the way: how do I keep the git record of the create-vue

# 在 github 上新建一个仓库 `create-vue-analysis` 克隆下来
git clone https://github.com/lxchuan12/create-vue-analysis.git
cd create-vue-analysis
git subtree add --prefix=create-vue https://github.com/vuejs/create-vue.git main
# 这样就把 create-vue 文件夹克隆到自己的 git 仓库了。且保留的 git 记录

For more information about git subtree , please see Git Subtree Concise User Manual

3.2 package.json analysis

// create-vue/package.json
{
  "name": "create-vue",
  "version": "3.0.0-beta.6",
  "description": "An easy way to start a Vue project",
  "type": "module",
  "bin": {
    "create-vue": "outfile.cjs"
  },
}

bin specifies the executable script. That is why we can use npx create-vue .

outfile.cjs JS file that is packaged and output

{
  "scripts": {
    "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"
  },
}

When npm run test is executed, the hook function pretest will be executed first. run-s is the command provided by npm-run-all run-s build snapshot command is equivalent to npm run build && npm run snapshot .

According to the script prompt, we look at the snapshot.js file.

3.3 Generate snapshot snapshot.js

This document is a major role const featureFlags = ['typescript', 'jsx', 'router', 'vuex', 'with-tests'] generating composition 31 is coupled Species default total 32 in combination, in a snapshot playground directory.

outfile.cjs code generated by the package has some processing, it is not convenient for debugging, we can modify it to index.js facilitate debugging.

// 路径 create-vue/snapshot.js
const bin = path.resolve(__dirname, './outfile.cjs')
// 改成 index.js 便于调试
const bin = path.resolve(__dirname, './index.js')

We can hit breakpoints for and createProjectWithFeatureFlags

createProjectWithFeatureFlags is actually similar to entering the following command in the terminal to execute such a command

node ./index.js --xxx --xxx --force
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)
  }
}

// 路径 create-vue/snapshot.js
for (const flags of flagCombinations) {
  createProjectWithFeatureFlags(flags)
}
debugging : VSCode open the project, VSCode high version (1.50+) can be in create-vue/package.json => scripts => "test": "node test.js" . Hover the mouse over test and there will be a debugging script prompt, select the debugging script. If you are not familiar with debugging, you can read my previous article koa-compose , which is very detailed.

When debugging, there is a high probability that you will encounter: __dirname error in the create-vue/index.js file. It can be solved as follows. After import , add the following statement to debug happily.

// 路径 create-vue/index.js
// 解决办法和nodejs issues
// https://stackoverflow.com/questions/64383909/dirname-is-not-defined-in-node-14-version
// https://github.com/nodejs/help/issues/2907

import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

Then we debug the index.js file to learn.

4. Debug the main process of index.js

Review the above npm init vue@next initialization project.

npm init vue@next

Just look at the output diagram of the initial project. There are mainly three steps.

1. 输入项目名称,默认值是 vue-project
2. 询问一些配置 渲染模板等
3. 完成创建项目,输出运行提示
async function init() {
  // 省略放在后文详细讲述
}

// async 函数返回的是Promise 可以用 catch 报错
init().catch((e) => {
  console.error(e)
})

4.1 Parsing command line parameters

// 返回运行当前脚本的工作目录的路径。
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
})

minimist

Simply put, this library parses command line parameters. Looking at the examples, it is easier for us to understand the parameter transfer and analysis results.

$ node example/parse.js -a beep -b boop
{ _: [], a: 'beep', b: 'boop' }

$ node example/parse.js -x 3 -y 4 -n5 -abc --beep=boop foo bar baz
{ _: [ 'foo', 'bar', 'baz' ],
  x: 3,
  y: 4,
  n: 5,
  a: true,
  b: true,
  c: true,
  beep: 'boop' }

for example

npm init vue@next --vuex --force

4.2 If feature flags are set, skip prompts

This writing method is convenient for code testing and so on. Skip the interactive inquiry directly and save time at the same time.

// 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]
  // 默认 vue-projects
  const defaultProjectName = !targetDir ? 'vue-project' : targetDir
  // 强制重写文件夹,当同名文件夹存在时
  const forceOverwrite = argv.force

4.3 Interactively ask for some configuration

As shown in the icon initialized by npm init vue@next

  • Enter project name
  • And whether to delete the existing directory with the same name
  • Ask about using JSX Router vuex cypress etc.
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: '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)
  }

4.4 Initialization asks for the parameters given by the user, and also gives default values

// `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}...`)

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

4.5 Generate the required files for the initial project based on the template file

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

4.6 Rendering the generated code template

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

4.7 If ts is required if configured

Rename all .js files into .ts .
Rename the jsconfig.json file to tsconfig.json .

jsconfig.json is the configuration file of VSCode, which can be used to configure jumps and so on.

The index.html documents in the main.js rename main.ts .

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

4.8 No test required if configured

Because all templates have test files, if you do not need to test, delete the cypress and /__tests__/

  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)
        }
      },
      () => {}
    )
  }

4.9 Generate the README.md file according to the npm / yarn / pnpm used, and give tips for running the project

// 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()

5. npm run test => node test.js test

// create-vue/test.js
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}`)
    }
  }
}

The following tests playground generated when the snapshot is generated.

pnpm test:unit:ci

pnpm test:e2e:ci

6. Summary

We used npm init vue@next , which is lightning fast, and learned the npx command. Learned its principles.

npm init vue@next => npx create-vue@next

The reason why it is lightning fast is that it depends on very little. Many of them are realized by themselves. For example: Vue-CLI command in vue create vue-project is to use the official npm package validate-npm-package-name , delete the folder generally use rimraf . And create-vue is self-fulfilling emptyDir and isValidPackageName .

Highly recommended readers used in accordance with the proposed method VSCode debugging create-vue source. There are still many details in the source code. Due to limited space, the article has not been fully described.

After studying this article, you can create similar initialization scaffolding for yourself or your company.

The current version is 3.0.0-beta.6 . We continue to focus on learning it. In addition to create-vue, we can also look at the source code implementation of create-vite and create-umi

Finally, welcome to add me to WeChat ruochuan12 communicate, participate in the source code reading activity, everyone learn the source code together and make progress together.

7. References

When I found create-vue, I planned to write an article and join the source code to read learn together. And the source code is read by the small partners in the group upupming before I finish writing the article.

@upupming vue-cli will be replaced by create-vue? Why is it so easy to initialize a vue3 project based on vite?


若川
7k 声望3.2k 粉丝

你好,我是若川。写有 《学习源码整体架构系列》 几十篇。