1. Introduction

Hi, I’m Wakawa. Welcome to follow my official account Ruochuan Vision. Recently, I organized an activity to read the source code together. If you are interested, you can join in with ruoChuan12 on wechat.

If you want to learn source code, Highly recommend before I wrote “learning source code overall architecture series” Underscore, Lodash, Vuex, Sentry, AXIos, Redux, KOA, VuE-devtools, VUex4, koa-compose, VUe-next-release, vue-this and more than 10 source code articles.

On the morning of October 7, 2021, THE Vue Contributor team and other major contributors held a Vue Contributor Days online conference. Create-vue, a new scaffold tool, was unveiled at the conference.

Create-vue can initialize a Vue3 vite project lightning-fast with a single command, NPM init vue@next.

This article is through debugging and we learn the source code of more than 300 lines.

Reading this article, you will learn:

1. Learn the use and principle of create-Vue, a new official scaffolding tool. 3. Learn to use test cases to debug source code 4. Put what I learned into practice, writing scaffolding tools for company initialization projects. 5. Etc.Copy the code

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

Create-vue Github README says, An easy way to start a Vue project. A simple way to initialize a VUE project.

npm init vue@next
Copy the code

For most readers, the first reaction is how can this be so easy and quick?

I can’t resist the urge to print commands on the console. I tried it on the terminal, as shown below.

Finally, CD vue3-project, NPM install, and NPM run dev open http://localhost:3000.

2.1 NPM init && NPX

Wondering why NPM init can also initialize a project directly, we look at the NPM documentation.

npm init

NPM init

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>`)
Copy the code

NPM init

convert to NPX:

  • 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:

# run
npm init vue@next
# is equivalent to
npx create-vue@next
Copy the code

We can find some information here, create-Vue. Or find the version information in NPM create-vue.

Where @next is the specified version, as can be seen from the command NPM dist-tag ls create-vue, next version is currently corresponding to 3.0.0-beta.6.

NPM dist-tag ls create-vue-latest: 3.0.0-beta.6-next: 3.0.0-beta.6Copy the code

NPM publish –tag next specifies the tag. The default tag is Latest.

If you are not familiar with NPX, please find ruan Yifeng’s blog NPX introduction, nodejs.cn NPX

NPX is a very powerful command, available as of version 5.2 of NPM (released in July 2017).

Briefly speaking of scenarios that are easily overlooked and commonly used, NPX is a bit like the walk-on scenario proposed by applets.

Easily run local commands

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

# is equal to
# package.json script: "vite -v"
# npm run vite

npx vite -v
# vite / 2.6.5 Linux - x64 node - v14.16.0
Copy the code

Running code with different Node.js versions You can switch Node versions temporarily in some scenarios, which is sometimes easier than NVM package management.

npx node@14 -v
# v14.18.0

npx -p node@14 node -v 
# v14.18.0
Copy the code

Command execution without installation

Start local static service
npx http-server
Copy the code
No global installation required
npx @vue/cli create vue-project
# @vue/cli is slower than NPM init vue@next NPX create-vue@next.

# global install
npm i -g @vue/cli
vue create vue-project
Copy the code

NPM init vue@next (NPX create-vue@next) fast reason, mainly lies in the dependence (can not rely on the package does not rely on), the source line is small, currently index.js only more than 300 lines.

3. Configure the environment debugging source code

3.1 Clone create-vue project

Create -vue-analysis

I can clone my repository directly. My repository keeps git records of create-Vue repository
git clone https://github.com/lxchuan12/create-vue-analysis.git
cd create-vue-analysis/create-vue
npm i
Copy the code

Of course, it can be used directly without cloningVSCodeOpen up my warehouse

Incidentally: how DO I keep git records in the Create-Vue repository?

Create a repository 'create-vue-Analysis' on Github and clone it
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
Clone the create-vue folder to your git repository. And retain Git records
Copy the code

For more information about Git subtrees, see the Git Subtree User Guide

3.2 package. Json analysis

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

Bin Specifies an executable script. That’s why we can use NPX create-vue.

Outfile.cjs is a JS file packaged with 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"}},Copy the code

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

Following the script prompts, let’s look at the snapshot.js file.

3.3 Generating Snapshot snapshot.js

Const featureFlags = [‘typescript’, ‘JSX ‘, ‘router’, ‘vuex’, ‘with-tests’] Create a snapshot in the playground directory.

Because the outfile. CJS code generated by packaging has some processing, which is not convenient for debugging, we can change it to index.js for debugging.

/ / path the create - vue/snapshot. Js
const bin = path.resolve(__dirname, './outfile.cjs')
// Change to index.js for easy debugging
const bin = path.resolve(__dirname, './index.js')
Copy the code

We can in the for and createProjectWithFeatureFlags a breakpoint.

Enter the following createProjectWithFeatureFlags actually similar to the terminal execute this command

node ./index.js --xxx --xxx --force
Copy the code
function createProjectWithFeatureFlags(flags) {
  const projectName = flags.join(The '-')
  console.log(`Creating project ${projectName}`)
  const { status } = spawnSync(
    'node',
    [bin, projectName, ...flags.map((flag) = > `--${flag}`), '--force'] and {cwd: playgroundDir,
      stdio: ['pipe'.'pipe'.'inherit']})if(status ! = =0) {
    process.exit(status)
  }
}

/ / path the create - vue/snapshot. Js
for (const flags of flagCombinations) {
  createProjectWithFeatureFlags(flags)
}
Copy the code

Json => scripts => “test”: “node test.js”. Hovering over Test will prompt you to debug scripts. Select debug scripts. If you’re not familiar with debugging, check out my previous article koa-compose, which goes into great detail.

In create-vue/index.js, there is a good chance that __dirname will fail. You can solve the problem as follows. After the import statement, add the following statement and you can debug happily.

The create / / path - vue/index. Js
// Solutions and 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);
Copy the code

Then we debug the index.js file to learn.

4. Debug the index.js main flow

Review the NPM init vue@next initialization project above.

Just look at the initialization project output diagram. There are three main steps.

1. Enter the project name, the default value is vue-project 2. Finish creating the project and print a run promptCopy the code
async function init() {
  // Ellipsis will be discussed later
}

// Async returns a Promise and a catch
init().catch((e) = > {
  console.error(e)
})
Copy the code

4.1 Parsing command Line Parameters

// Returns the path to the working directory where the current script is running.
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
})
Copy the code

minimist

The library, in short, parses command-line arguments. By looking at examples, we can easily understand the result of passing parameters and parsing.

$ 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' }
Copy the code

Such as

npm init vue@next --vuex --force
Copy the code

4.2 If Feature Flags is set, prompts are skipped to ask

This is convenient for code testing and so on. Skip the interactive queries and save 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'

// Generate a directory
  let targetDir = argv._[0]
  / / the default vue - projects
  constdefaultProjectName = ! targetDir ?'vue-project' : targetDir
  // Forces the folder to be overwritten if the folder of the same name exists
  const forceOverwrite = argv.force
Copy the code

4.3 Interactive Ask some configurations

As illustrated above, NPM init vue@next initialization

  • Enter the project name
  • And whether to delete existing directories with the same name
  • Ask to use 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)
        },
        // Omit several configurations
        {
          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)
    Exit the current process.
    process.exit(1)}Copy the code

4.4 Initialization The user is asked for the given parameters, and the default values are also given

// `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 you need to force a rewrite, clear the folder

  if (shouldOverwrite) {
    emptyDir(root)
    // If no folder exists, create one
  } else if(! fs.existsSync(root)) { fs.mkdirSync(root) }// Scaffolding project directory
  console.log(`\nScaffolding project in ${root}. `)

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

4.5 Generate files required for project initialization based on template files

  // 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 the configuration
  // 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')}Copy the code

4.6 Rendering generates code templates

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

4.7 If TS is configured, it is required

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

Jsconfig. json is a VSCode configuration file that can be used to configure jumps, etc.

Rename main.js in the index.html file to 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'))}Copy the code

4.8 No test is required if the configuration is complete

Because all templates have test files, delete cypress, /__tests__/ folder when no tests are needed

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

4.9 Generate the readme. md file based on the NPM, YARN, or PNPM used to provide prompts 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()
Copy the code

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'] and {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'] and {cwd: path.resolve(playgroundDir, projectName),
      stdio: 'inherit'.shell: true
    })
    if(e2eTestResult.status ! = =0) {
      throw new Error(`E2E tests failed in ${projectName}`)}}}Copy the code

The following tests are performed on the 32 folders on playground generated during snapshot generation.

pnpm test:unit:ci

pnpm test:e2e:ci
Copy the code

6. Summary

We used lightning-fast NPM init vue@next to learn the NPX command. I learned how it works.

npm init vue@next => npx create-vue@next
Copy the code

Lightning fast because there’s so little to rely on. A lot of it is done on your own. For example, in vue-CLI, the Vue create vue-project command uses the official validate-npm-package-name NPM package. Rimraf is generally used to delete folders. Create-vue implements emptyDir and isValidPackageName on its own.

It is highly recommended that readers use VSCode to debug create-vue source code in accordance with the method in the article. There are many details in the source code due to space constraints, not fully developed.

By the end of 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.

Finally, you are welcome to join us in ruochuan12 to learn source code and make progress together.

7. Reference materials

When create-Vue was discovered, I planned to write articles to join the source code reading plan, and we learned together. Upupming, my friend in the source group, finished the article before ME.

@upupming vue-cli will be replaced by creation-vue? Why is it so easy to initialize vuE3 based Vite projects?


About && communication groups

Recently, I organized a reading activity for source code. If you are interested, you can join me in wechat ruochuan12 for long-term communication and learning.

Author: Often in the name of ruochuan mixed traces in rivers and lakes. Welcome to add my wechat account ruochuan12. Front road | know very little, only good study. Concern about the public number if chuan vision, every week to learn the source code, learn to see the source code, advanced advanced front end. Wakawa’s blog segmentfault wakawa’s view column, has opened a column on wakawa’s view, welcome to follow ~ dig gold column, welcome to follow ~ github blog, beg a star^_^~