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 init
Initialization package. Json2, NPM I typescript ts-node tslint rimraf -d
Installation development dependencyNPM I typescript Chalk Commander Execa FS - Extra Globby handlebars inquirer ora Pacote
Installation production dependencyScripts 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 field
See below (2)Files field
NPM 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 bin
Field, executeprivatify-local
You can see the correct executionsrc/index.ts
The contents of the
(5) Core code implementation
src/index.ts
Is the entry file,src/commands/*.*s
Lower the specific command file, here we according to3.1
In the design of the scaffolding command to createcreate.ts
,scope.ts
,package.ts
file
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 name1
You can see
program.version(pkgVersion)
Is the command--version
We go through the followingGetPkgInfo function
readpackage.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 package
Gets 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-template
Path, 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 createdname
And,package.json
Is 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 lint
Check the code, after all, are sent to avoid problemsNPM run build
Typescript packaging3, NPM publish
Published 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