preface

Some time ago, Yuxi answered a question that many netizens are curious about: Will Vite replace Vue CLI?

The answer is: yes!

So, have you started Vite yet? For those of you who have used Vite, creating a project template for Vite is done via NPM init. vitejs/app. The NPM init command is available at [email protected], which actually installs Vite’s @vitejs/create-app package before executing create-app.

@vitejs/create-app is in the Vite project packages/create-app folder. Its overall directory structure:

// packages/create-app| -- - the template element - lit - | -- - the template - lit - element - ts | -- - the template - preact | -- - the template - preact - ts | -- - The template - the react | | -- -- -- -- the template - the react - ts template - vanilla | -- - the template - vue | -- - the template - vue - ts index. Js package.jsonCopy the code

Vite create-app CLI (collectively referred to as create-app CLI) has few capabilities, currently only supports the creation of the basic template, so the total code is only 160 lines, the overall architecture diagram:

As you can see, the CREate-app CLI is a good example to get started on how to implement a simplified VERSION of the CLI.

This article will take you through the following two sections to learn how to implement a simple version of the CREate-app CLI:

  • Libraries used in create-app (Minimist, Kolorist)

  • Step by step disassemble and analyze create-app CLI source code

Create-app Indicates the library used in the CLI

The library (NPM) used by the CREate-app CLI implementation is really interesting, ranging from the familiar Enquirer (for command line prompts) to the unfamiliar minimist and Kolorist. So, what are the latter two for? Below, we come to understand some ~

minimist

Minimist is a lightweight tool for parsing command-line arguments. When it comes to tools that parse the command line, I think you can easily think of Commander. Compared to Commander, Minimist wins with light! Because it is only 32.4 kB, commander is 142 kB, which is about 1/5 of the latter.

So, let’s look at the basics of minimist.

For example, at this point on the command line we type:

node index.js my-project
Copy the code

So, in the index.js file, you can use minimist to get the myProject parameter as input:

var argv = require('minimist')(process.argv.slice(2));
console.log(argv._[0]); 
/ / output my - project
Copy the code

In this case, argv is an object, and the value of the _ attribute in the object is an array of parameters parsed from Node index.js.

kolorist

Kolorist is a lightweight tool to color command line output. And I think chalk is the one that comes to mind. However, compared with chalk, the size difference between the two packets is not obvious, with the former being 49.9 kB and the latter 33.6 kB. Kolorist may be relatively niche, however, with NPM having far fewer downloads than Chalk, which has a more detailed API.

Similarly, let’s look at the basic uses of Kolorist.

For example, when an exception occurs in the application, we need to print a red exception message to inform the user of the exception. We can use the red function provided by Kolorist:

import { red } from 'kolorist'

console.log(red("Something is wrong"))
Copy the code

Alternatively, kolorist provides stripColors to print colored strings directly:

import { red, stripColors } from 'kolorist'

console.log(stripColors(red("Something is wrong"))
Copy the code

Step by step disassemble and analyze create-app CLI source code

For those of you who are familiar with CLI, the commands we usually use are configured in the package.json file bin. The bin configuration of the create-app CLI file in the root directory is as follows:

// pacakges/create-app/package.json
"bin": {
  "create-app": "index.js"."cva": "index.js"
}
Copy the code

As you can see, the create-app command is registered here and points to the index.js file in the current directory. Also, it’s worth noting that two commands are registered here, which means we can also use the CVA command to create a Vite based project template (surprise 😲).

The core of create-app CLI implementation is in the index.js file. So, let’s look at the implementation of the code in index.js

Base dependency introduction

As mentioned above, create-app CLI introduced minimist, enquire, Kolorist, and other dependencies, so we introduced them first:

const fs = require('fs')
const path = require('path')
const argv = require('minimist')(process.argv.slice(2))
const { prompt } = require('enquirer')
const {
  yellow,
  green,
  cyan,
  magenta,
  lightRed,
  stripColors
} = require('kolorist')
Copy the code

Fs and PATH are built-in modules of Node. The former is used for file-related operations and the latter for file-path-related operations. The introduction of minimist, Enquirer, and Kolorist, which have been mentioned above, will not be repeated here

Define project base templates (colors) and files

From the/Packages /create-app directory, we can see that the CREate-app CLI provides us with nine project base templates. In addition, during command line interaction, each template has a different color, that is, the CLI uses the color function provided by Kolorist to define the corresponding color for the template:

const TEMPLATES = [
  yellow('vanilla'),
  green('vue'),
  green('vue-ts'),
  cyan('react'),
  cyan('react-ts'),
  magenta('preact'),
  magenta('preact-ts'),
  lightRed('lit-element'),
  lightRed('lit-element-ts')]Copy the code

Second, due to the special nature of the.gitignore file, under each project template the _gitignore file is created first, and the name of the file is replaced with.gitignore when the project is created later. Therefore, the CLI pre-defines an object to hold the file to be renamed:

const renameFiles = {
  _gitignore: '.gitignore'
}
Copy the code

Define utility functions related to file operations

To create a project, you need to perform operations related to files, so the CLI defines three tool functions:

CopyDir function

The copyDir function is used to copy files in a folder srcDir to the specified folder destDir. It creates the specified folder by calling the fs.mkdirsync function, and then enumerates an array of filenames retrieved from the srcDir folder, fs.readdirsync (srcDir).

The corresponding code is as follows:

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

The copy function

The copy function is used to copy files or folders SRC to the specified folder dest. It gets the SRC status stat, and if SRC is a folder (stat.isdirectory () is true), it calls the copyDir function described above to copy files from the SRC folder to dest. If SRC is a file, call fs.copyfilesync directly to copy the SRC file to the dest folder.

The corresponding code is as follows:

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

EmptyDir function

The emptyDir function is used to empty the code in the dir folder. It will first determine whether the dir folder exists, then enumerate the files under this folder, construct the path abs of this file, call fs.unlinkSync function to delete this file, and when abs is a folder, recursively call emptyDir function to delete the files under this folder. Then call fs.rmdirsync to delete the folder.

The corresponding code is as follows:

function emptyDir(dir) {
  if(! fs.existsSync(dir)) {return
  }
  for (const file of fs.readdirSync(dir)) {
    const abs = path.resolve(dir, file)
    if (fs.lstatSync(abs).isDirectory()) {
      emptyDir(abs)
      fs.rmdirSync(abs)
    } else {
      fs.unlinkSync(abs)
    }
  }
}
Copy the code

The CLI implements core functions

The core function of the CLI implementation is init, which is responsible for implementing the corresponding function using the functions and toolkits described above. Here’s a point-by-point analysis of the init function implementation:

1. Create a project folder

In general, we can use create-app my-project to specify the project folder to be created, i.e., under which folder:

let targetDir = argv._[0]
cwd = process.cwd()
const root = path.join(cwd, targetDir)
console.log(`Scaffolding project in ${root}. `)
Copy the code

Where argv._[0] represents the first argument after create-app, and root is the full file path built through the path.join function. Then, on the command line, you will be prompted to tell you what path to your Scaffolding project will create:

Scaffolding project in /Users/wjc/Documents/project/vite-project...
Copy the code

Of course, sometimes we don’t want to type the project folder after create-app, but just the create-app command. So, tagertDir doesn’t exist at this point. The CLI uses the Enquirer package prompt to print a query on the command line:

? project name: > vite-project
Copy the code

You can enter the project folder name here, or you can enter the default project folder name given by the CLI. The code for this procedure is:

if(! targetDir) {const { name } = await prompt({
    type: "input".name: "name".message: "Project name:".initial: "vite-project"
  })
  targetDir = name
}
Copy the code

The CLI then determines if the folder exists under the current working directory (CWD), and if it does not, creates a folder using fs.mkdirsync:

if(! fs.existsSync(root)) { fs.mkdirSync(root, {recursive: true})}Copy the code

Otherwise, if the folder exists, the system will determine whether there are files in the folder, that is, use fs.readdirsync (root) to obtain the files in the folder:

const existing = fs.readdirSync(root)
Copy the code

“Existing” is going to be an array, and if that array is not zero, then there are files in that folder. The CLI will ask you whether to delete the files in this folder:

Target directory vite-project is not empty. 
Remove existing files and continue? (y/n) : yCopy the code

You can optionally tell the CLI whether to clear the directory by typing y or n. Also, if you type n at this point, that is, do not clear the folder, then the entire CLI execution will exit. The code for this procedure is:

if (existing.length) {
  const { yes } = await prompt({
    type: 'confirm'.name: 'yes'.initial: 'Y'.message:
      `Target directory ${targetDir} is not empty.\n` +
      `Remove existing files and continue? `
  })
  if (yes) {
    emptyDir(root)
  } else {
    return}}Copy the code

2. Determine the project template

After the project folder is created, the CLI gets the –template option when we type a command like this:

NPM init. vitejs/app --template folder nameCopy the code

If the –template option does not exist (i.e., undefined), the project template to select will be asked:

let template = argv.t || argv.template
if(! template) {const { t } = await prompt({
    type: "select".name: "t".message: "Select a template:".choices: TEMPLATES
  })
  template = stripColors(t)
}
Copy the code

Because TEMPLATES only defines the type of TEMPLATES, the project TEMPLATES folder is named differently (missing the template prefix) from the Packages /create-app directory. For example, if template is equal to vue-ts, then we need to concatenate the prefix to template and build the full directory:

const templateDir = path.join(__dirname, `template-${template}`)
Copy the code

So now templateDir will be equal to the current working directory + template-vue-ts.

3. Write the project template file

After determining the template of the project to be created, the CLI reads the files in the project template folder selected by the user and writes them one by one to the created project folder:

For example, if you want to create a project folder named viet-project, you will write the files in the create-app/template-vue-ts folder to the viet-project folder.

const files = fs.readdirSync(templateDir)
for (const file of files.filter((f) = >f ! = ='package.json')) {
  write(file)
}
Copy the code

Since the fs.readdirSync function returns an array of file names under that folder, the array is enumerated by for of, and each enumeration calls the write function to write the file.

Note that the package.json file is skipped at this point, and I’ll explain why you need to skip the package.json file later.

The write function, which takes two parameters file and content, has two capabilities:

  • Write the specified content to the specified file, call fs.writeFileSync function to write the content to the file

  • Copy the files in the template folder to the specified folder, and call the copy function described earlier to copy the files

Write function definition:

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

Also, it’s worth noting that the targetPath acquisition process builds the full file path for file and is compatible with handling _gitignore files.

After writing these files within the template, the CLI processes the package.json file. The reason for treating package.json files separately is that the package.json name in each project template is written dead, and when the user creates the project, the name should be named for the project’s folder. The code for this process would look like this:

const pkg = require(path.join(templateDir, `package.json`))
pkg.name = path.basename(root)
write('package.json'.JSON.stringify(pkg, null.2))
Copy the code

The path.basename function is used to get the last folder name of a full path

Finally, the CLI will output some prompts telling you that the project has been created and the commands you need to run to start the project:

console.log(`\nDone. Now run:\n`)
if(root ! == cwd) {console.log(`  cd ${path.relative(cwd, root)}`)}console.log(` npm install (or \`yarn\`)`)
console.log(` npm run dev (or \`yarn dev\`)`)
console.log()
Copy the code

conclusion

Vite’s create-App CLI implementation is only 160 lines of code, but it takes into account the various scenarios for creating a project and makes it compatible. In short, very small and beautiful. So, I believe that after learning Vite create-app CLI implementation, should be able to throw out (implementation) a CLI code 😎 ~

Thumb up 👍

If you get something from this post, please give me a “like”. This will be my motivation to keep sharing. Thank you

My name is Wu Liu. I like innovation and source Code manipulation, and I focus on source Code (Vue 3, Vite), front-end engineering, cross-end and other technology learning and sharing. Welcome to follow my wechat official account: Code Center.