DBQ >_< title is confusing, but this article is all dry

I. Background:

In the past six months, because of the needs of the work business, I have written two scaffolds, and are applied in the business of the front-end group, so I have also been affirmed by the director, and applied for a patent by the way, one of which has applied for a patent successfully, and another application (such as application success to write an article to share)…

Screenshots as proof (three are written by myself), give directionsAn automated tool that processes private source NPM packages as offline packages

Scaffolding although usually as advanced front-end necessary skills, but as a work for more than a year of primary front-end, personal experience tells you, master the basic development of scaffolding so easy ~

In this paper,Pure dry, how can you get to develop oneOn the tallThe scaffold

Two, what is scaffolding, what problems can be solved:

Scaffolding is also often referred to as CLI, which stands for command-line interface, translated as command line interface. Front-end scaffolding CLI, also known as a command line tool

In daily development, we often encounter these three types of scaffolding

  • Common Open Source Scaffolding

    Vue-cli and create-react-app are both familiar scaffolders that provide the ability to build, run, and build projects. NPM, which we use every day, is also scaffolding

  • Enterprise scaffolding

    General enterprises, the need to customize a set of business within the group of scaffolding, and with some of the company’s basic services through, upstream and downstream cohesion, built-in tool set, etc., generally based on common open source scaffolding secondary packaging

  • Other scaffolding

    There is also a large category, for a business pain point, manual solution is more complex, high cost, low efficiency, designed for automatic scaffolding implementation

Through the above examples, it is not difficult to find that scaffolding can solve the following problems:

  • Reduce repetitive tasks
  • The team unified development style, standardized project development directory structure, facilitating cross-team cooperation and later maintenance, reducing the cost of new recruits
  • Provide one-click front-end project creation, configuration, local development, plug-in extension and other functions, so that developers can focus more time on business

Three, how to build a scaffold

3.1 Design scaffolding functions

First, you need to design scaffolding before you can get started, as evidenced by the successful patents for NPM-Package-Fy.

  • First, get a name for your scaffolding, like Privatify in my case

  • Second, the scaffold provides a list of commands, such as a command to initialize the project like vue create [options]

    For example, vue add [options] [pluginOptions] provides an operation command to add a plug-in…

    There is also the basic –version view, where –help provides a list of commands, which are as follows: Privatify design

3.2 Dependent base packages

It is said that standing on the shoulders of giants, a good bag can get you twice the result with half the effort, and building a basic scaffold depends on the following bag

The package name use
commander Command line tools, reading command line commands, knowing what the user wants to do
inquirer Interactive command tool that provides users with a stream of questions
chalk The color plug-in is used to modify the output style of the command line. It is clear and intuitive to distinguish info and error logs by color
ora The loading effect is displayed, similar to the loading effect of the front-end page. The loading effect can remind users that they are in progress and please wait patiently when loading a template
globby Used to retrieve files
fs-extra Enhanced version of the Node FS file system module
pacote Obtain the latest node package version
handlebars Provides the necessary functionality to enable you to efficiently build semantic templates

3.3 Scaffolding code implementation

TypeScript Node repository: github.com/zxyue25/cli… Clone down

(1) Directory structure
Cli-template ├─.gitignore ├─ readme.md ├─ build // Build // Build // See ├─ bin-local.js // Background Debug executable file, see ├─ package.json // config file, │ ├─ SRC │ ├─ commands │ │ ├─ create.ts │ │ ├─ scope.ts │ │ ├─ package.ts // scope │ │ ├─ package Package ordered │ │ └ ─ utils / / public function │ ├ ─ index. The ts / / entry documents │ └ ─ lib / / public third-party packages │ ├ ─ consts. Ts / / constant │ ├ ─ but ts │ ├ ─ Logger. Ts/color/console output │ └ ─ spinner. Ts / / console loading ├ ─ tsconfig. Json / / TypeScript configuration file └ ─ tslint. Json / / tslint configuration fileCopy the code
(2) package. The json
  • 1, NPM initInitialization package. Json
  • 2, NPM I typescript ts-node tslint rimraf -dInstallation development dependency
  • NPM I typescript Chalk Commander Execa FS - Extra Globby handlebars inquirer ora PacoteInstallation production dependency
  • Scripts configures the clear, build, publish, lint commands, the specific use of the final package will be described

The completed package.json configuration file is as follows

{
  "name": "cli-template"."version": "Hundreds"."description": "Cli Template Repository"."main": "./build"."scripts": {
    "clear": "rimraf build"."build": "npm run clear && tsc"."publish": "npm run build && npm publish"."lint": "tslint ./src/**/*.ts --fix"
  },
  "repository": {
    "type": "git"."url": "https://github.com/zxyue25/cli-template.git"
  },
  "bin": {
    "privatify": "./bin.js"."privatify-local": "./bin-local.js"
  },
  "files": [
    "build"."bin.js"]."keywords": [
    "cli"."node"."typescript"."command"]."author": "zxyue25"."license": "ISC"."devDependencies": {
    "rimraf": "^ 3.0.2." "."ts-node": "^ 10.3.0"."tslint": "^ also 6.1.3." "."typescript": "^ 4.4.4." "
  },
  "dependencies": {
    "chalk": "^ 4.1.2." "."commander": "^ 8.2.0"."execa": "^ 5.1.1." "."fs-extra": "^ 10.0.0"."globby": "^ 11.0.4"."inquirer": "^ 8.2.0"."leven": "^ 4.0.0"."ora": "^ 5.4.1." "."pacote": "^ 12.0.2"}}Copy the code

Focus on the bin field and files field

  • Bin fieldSee below (2)
  • Files fieldNPM whitelist = NPM whitelist = NPM whitelist = NPM whitelist = NPM whitelist = NPM whitelist = NPM whitelist So here we configure"files": [ "build", "bin.js" ], including build and bin.js, SRC folders are not whitelisted

(3) tsconfig.json configuration file

If a tsconfig.json file exists in a directory, it means that the directory is the root of the TypeScript project. The tsconfig.json file specifies the root file and compilation options to compile this project, as follows

If you’re not familiar with typescript, you can skip ahead and assume that you need to configure a configuration file to write in typescript

// tsconfig.json
{
  "compilerOptions": {
    "target": "es2017"."module": "commonjs"."moduleResolution": "node"."emitDecoratorMetadata": true."experimentalDecorators": true."allowSyntheticDefaultImports": true."alwaysStrict": true."sourceMap": false."noEmit": false."noEmitHelpers": false."importHelpers": false."strictNullChecks": false."allowUnreachableCode": true."lib": ["es6"]."typeRoots": ["./node_modules/@types"]."outDir": "./build".// Redirect the output directory
    "rootDir": "./src" // Only to control the output directory structure
  },
  "exclude": [ // Do not participate in packing directories
    "node_modules"."build"."**/*.test.ts"."temp"."scripts"."./src/__tests__"]."esModuleInterop": true."allowSyntheticDefaultImports": true."compileOnSave": false."buildOnSave": false
}
Copy the code
(4) local debugging of bin.js and NPM link
  "bin": {
    "privatify": "./bin.js"."privatify-local": "./bin-local.js"
  }
Copy the code

This field defines the command name (that is, the name of your scaffold) and the associated execution file, starting with a #! Line. /usr/bin/env node specifies that the current script is parsed by Node.js

The production environment

// bin.js
#!/usr/bin/env node   
require('./build')
Copy the code

Local debugging

// bin-local.js
#!/usr/bin/env node   
require('ts-node/register')
require('./src')
Copy the code

If you want to debug the scaffolding locally, you can build it directly to the build folder. If you want to debug the scaffolding locally, you don’t want to change a line of code, compile it, pack it into build, and debug it again. Ts-node/Register provides just-in-time compilation, pointing to SRC

performnpm link, it will read by defaultPackage. The json binField, executeprivatify-localYou can see the correct executionsrc/index.tsThe contents of the

(5) Core code implementation

src/index.tsIs the entry file,src/commands/*.*sLower the specific command file, here we according to3.1In the design of the scaffolding command to createcreate.ts,scope.ts,package.tsfile

Review the following

As you can see, each command includes a command, a description, an action function, and partial option support

SRC /commands/*.*s are written as follows, such as create.ts

// src/commands/create.ts
const action = (projectName) = > { 
     console.log('projectName:', projectName)
}
export default {
    command: 'create <registry-name>'.description: 'Create an NPM private server repository'.optionList: [['--context <context>'.'Context path']],
    action,
}
Copy the code

The SRC /index.ts code is implemented as follows

// src/index.ts
import * as globby from 'globby'
import * as commander from 'commander'
import * as path from 'path'
import { error, chalk, fs, info } from './lib'
import * as pacote from 'pacote'
const { program } = commander

let commandsPath = []
let pkgVersion = ' '
let pkgName = ' '

// Obtain the commands in the SRC /command path
const getCommand = () = > {
  commandsPath =
    (globby as any).sync('./commands/*.*s', { cwd: __dirname, deep: 1}) | | []return commandsPath
}

// Get information about the current package
const getPkgInfo = () = > {
  const jsonPath = path.join(__dirname, '.. /package.json')
  const jsonContent = fs.readFileSync(jsonPath, 'utf-8')
  const jsonResult = JSON.parse(jsonContent)
  pkgVersion = jsonResult.version
  pkgName =  jsonResult.name
}

// Get the latest package version
const getLatestVersion = async() = > {const manifest = await pacote.manifest(`${pkgName}@latest`)
    return manifest.version
}

function start() {
  getPkgInfo()
  const commandsPath = getCommand()
  program.version(pkgVersion)
  commandsPath.forEach((commandPath) = > {
    const commandObj = require(`. /${commandPath}`)
    const { command, description, optionList, action } = commandObj.default
    const curp = program
      .command(command)
      .description(description)
      .action(action)

    optionList &&
      optionList.map((option: [string]) = >{ curp.option(... option) }) }) program.on('command:*'.async ([cmd]) => {
    program.outputHelp()
    error('Unknown command${chalk.yellow(cmd)}. `)
    const latestVersion = await getLatestVersion() 
    if(latestVersion ! == pkgVersion){ info('Updatable version,${chalk.green(pkgVersion)} -> ${chalk.green(latestVersion)}`)
    }
    process.exitCode = 1
  })

  program.parseAsync(process.argv)
}

start()
Copy the code

SRC /index.ts; SRC /commands/*.*s; SRC /commands/*.*s; Option (parameters) using the commander package, put commander.program.com mand (command). The description (description). The action (action). Option (option)

Commander official website, command line tool, provides very powerful functions github.com/tj/commande…

To perform,privatify-local create name1You can see

program.version(pkgVersion)Is the command--versionWe go through the followingGetPkgInfo functionreadpackage.jsonOf the fileversionField to get the version

In addition, if the command you typed does not exist, you should prompt the user that the COMMANDER package provides the program.outputhelp () implementation

And when you upgrade the scaffolding, naturally expect users to update the local scaffolding to the latest version, usingPacote packageGets the specified package version and prompts for updates

(6) Public third-party dependency encapsulation

Scaffolding is an interactive command line, which involves friendly prompts on the interface, colors of success or failure, copywriting, loading, ICONS, etc., and is encapsulated in the path SRC /lib without expansion. It is a basic encapsulation

(7) Initialize the implementation of the project template

Create [options]

: initialize a project interactively, use the Inquirer package to query the name, description, and author fields of the user package.json. Start to download the project template. There are two ways to implement the location of the project template, as follows

  • The first option is to package the project template with the scaffold, which will be stored in the global directory when the scaffold is installed. This way, the project is quickly copied from local each time the project is created, but the project template itself is more difficult to upgrade.
  • The second way is to store the project template in a remote warehouse (such as gitLab warehouse). In this way, every time a project is created, it is dynamically downloaded from a certain address, and the project template is convenient to update

Let’s use it here firstThe first kind ofThe project template is placed inproject-templatePath, and the second one is next

// project-template/package.json
{
  "name": "{{ name }}"."version": "1.0.0"."description": "{{ description }}"."main": "index.js"."author": "{{ author }}"."scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "license": "ISC"."dependencies": {
    "npm-package-privatify": "^ 1.1.4." "}}Copy the code

Use the Handlebars package for template replacement, that is, replace {{name}}, {{description}}, {{author}} with user input

// // src/commands/create.ts
import * as path from 'path'
import * as handlebars from 'handlebars'
import * as inquirer from 'inquirer'
import {
  chalk,
  execa,
  fs,
  startSpinner,
  succeedSpiner,
  warn,
  info,
} from '.. /lib'

// Check whether the project with the same name already exists
export const checkProjectExist = async (targetDir) => {
  if (fs.existsSync(targetDir)) {
    const answer = await inquirer.prompt({
      type: 'list'.name: 'checkExist'.message: '\n Warehouse path${targetDir}It already exists, please select '.choices: ['cover'.'cancel'],})if (answer.checkExist === 'cover') {
      warn(` delete${targetDir}. `)
      fs.removeSync(targetDir)
    } else {
      return true}}return false
}

export const getQuestions = async (projectName) => {
  return await inquirer.prompt([
    {
      type: 'input'.name: 'name'.message: `package name: (${projectName}) `.default: projectName,
    },
    {
      type: 'input'.name: 'description'.message: 'description'}, {type: 'input'.name: 'author'.message: 'author',})}export const cloneProject = async (targetDir, projectName, projectInfo) => {
  startSpinner('start creating private server repository${chalk.cyan(targetDir)}`)
  // copy 'private-server-boilerplate' to the target path to create the project
  await fs.copy(
    path.join(__dirname, '.. '.'.. '.'private-server-boilerplate'),
    targetDir
  )

  // The Handlebars template engine parses the user input into package.json
  const jsonPath = `${targetDir}/package.json`
  const jsonContent = fs.readFileSync(jsonPath, 'utf-8')
  const jsonResult = handlebars.compile(jsonContent)(projectInfo)
  fs.writeFileSync(jsonPath, jsonResult)

  // New project package
  execa.commandSync('npm install', {
    stdio: 'inherit'.cwd: targetDir,
  })

  succeedSpiner(
    Private server warehouse created${chalk.yellow(projectName)}\n👉 Enter the following command to start the private server: '
  )

  info(`$ cd ${projectName}\n$ sh start.sh\n`)}const action = async(projectName: string, cmdArgs? : any) => {console.log('projectName:', projectName)
}

export default {
  command: 'create <registry-name>'.description: 'Create an NPM private server repository'.optionList: [['--context <context>'.'Context path']],
  action,
}
Copy the code

Alert the user when the created file exists and providecoverTo canceloptions, the execution effect is shown in the figure

You can see that the project is creatednameAnd,package.jsonIs the value entered by the user

If you need to create a new template repository, use the download-git-repo package to download the remote repository

The remote repository address is not the git clone address, but needs to be adjusted slightly

For example, the git repository address is github.com/zxyue25/vue… – > github.com: zxyue25 / vue – demo – cl…

import * as download from 'download-git-repo'
// https://github.com/zxyue25/vue-demo-cli-templateA.git
download("https://github.com:zxyue25/vue-demo-cli-templateA#master", projectName, {clone: true}, err= > {
  if(err){
    spinner.fail()
    return 
  }
  spinner.succeed()
  inquirer.prompt([
    {
        type: 'input'.name: 'name'.message: 'Please enter project name'
    },
    {
        type: 'input'.name: 'description'.message: 'Please enter project profile'
    },
    {
        type: 'input'.name: 'author'.message: 'Please enter author name'
    },
]).then((answers) = > {
    const packagePath = `${projectName}/package.json`
    const packageContent = fs.readFileSync(packagePath,'utf-8')
    // Parse the template engine using Handlebars
    const packageResult = handlebars.compile(packageContent)(answers)
    // Rewrite the parsed results into package.json file
    fs.writeFileSync(packagePath,packageResult)
    console.log(logSymbols.success, chalk.yellow('Template initialization successful'))})})Copy the code
(8) Delivery

At this point, the basic scaffolding construction and development is complete and published to NPM

  • NPM run lintCheck the code, after all, are sent to avoid problems
  • NPM run buildTypescript packaging
  • 3, NPM publishPublished to the NPM

After the package is sent, check the installation

npm i npm-package-privatify -g
privatify
Copy the code

Write at the end

The focus of scaffolding is to help reduce cost and increase efficiency, more important than the basic development skills of scaffolding is the design thinking of scaffolding, and these thinking is generally need us to think about the pain and difficulties encountered in daily development and formed, the project

It took me some time to write this article, but IN this process, I also learned a lot of knowledge. Thank you for your precious time to read this article. If this article gives you some help or inspiration, please be generous with your praise and Star