Whenever you find yourself on the side of the majority, it’s time to stop and think. “– Mark Twain
Since this part is a bit complicated, I will post the github address and video address before explaining:
Project source: github.com/Walker-Leee…
For video explanation, please search the wechat official account “JavaScript Full Stack”.
I believe you all have the following experiences in your work:
-
To develop a new project, a lot of logic such as: project architecture, interface request, state management, internationalization, skin and so on before the project already exists, at this time, we choose “random”, CTRL + C, CTRL + V one after another, chat and laugh, the new project is completed, nothing more than to change some files and package name;
-
When a module is added to the project, copy an existing module and change its name, and the new module is considered to be created successfully.
-
The specifications of the project need to be mentioned all the time by colleagues, and even if there are specifications, you need to keep them in mind.
Using copy and paste has the following disadvantages:
-
Repetitive work, tedious and time-consuming
-
A copy template is prone to extraneous code
-
There are many configuration areas in the project, and it is easy to overlook some configuration points
-
Human error is always possible, and it takes time to correct errors when building a new project
-
The framework will also be constantly iterating, manual construction projects do not know the latest version number, the use of what version of the dependency, it is easy to a lot of bugs.
Bear some of the above pain of the students should be many, how to solve these problems? I think, scaffolding can avoid many think the operation problem, because the scaffolding can according to the predetermined specifications, you create a project and define a new module, packaging, deployment, and so on can be done after beating in a command, promote efficiency and decrease the cost of the recruits training, so, I recommend you to think it over for the team to create a scaffold!
The tripartite library we need to develop scaffolding
The library | describe |
---|---|
commander | Processing console command |
chalk | Colorful console |
semver | Version Check Prompt |
fs-extra | Friendlier FS operation |
inquirer | Console query |
execa | Executing terminal Commands |
download-git-repo | Git remote repository pull |
Responsibilities and execution of scaffolding
Scaffolding can do a lot of things for us, such as project creation, project module addition, project packaging, project unified testing, project release, etc. Let me talk to you about the initial function: project creation.
The image above shows an overview of the scaffolding for creating a project and creating modules in the project, and the image below shows template-based creation in more detail:
The idea is very simple, and we will explain it in detail through code examples.
Package. The json and entrance
The project structure is shown in figure
Specify in package.json how your package is soft-linked to launch: Bin specifies package.json dependencies, so we must pay attention to the difference between dependencies, devDependencies, peerDependencies.
{
"name": "awesome-test-cli"."version": "1.0.0"."description": "One in One takes you to develop scaffolding tools."."main": "index.js"."bin": {
"awesome-test": "bin/main.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"scaffold"."efficient"."react"]."author": "walker"."license": "ISC"."engines": {
"node": "> = 8.9"
},
"dependencies": {
"chalk": "^" 2.4.2."commander": "^ 3.0.0"."download-git-repo": "^ 2.0.0." "."execa": "^ 2.0.4"."fs-extra": "^ 8.1.0"."import-global": "^ 0.1.0 from"."inquirer": "^ 6.5.1." "."lru-cache": "^ 5.1.1." "."minimist": "^ 1.2.0"."nunjucks": "^ 3.2.0"."ora": "^ 3.4.0"."request-promise-native": "^ 1.0.7"."semver": "^ 6.3.0"."string.prototype.padstart": "^ 3.0.0"."valid-filename": "^ 3.1.0"."validate-npm-package-name": "^ 3.0.0"}}Copy the code
Next, write the /bin/main.js entry file. The main operation is to use COMMANDER to handle the console command, and handle different logic according to different parameters.
// Start processing commands
const program = require('commander')
const minimist = require('minimist')
program
.version(require('.. /package').version)
.usage('<command> [options]')
// Create command
program
.command('create <app-name>')
.description('create a new project')
.option('-p, --preset <presetName>'.'Skip prompts and use saved or remote preset')
.option('-d, --default'.'Skip prompts and use default preset')
.action((name, cmd) = > {
const options = cleanArgs(cmd)
if (minimist(process.argv.slice(3))._.length > 1) {
console.log(chalk.yellow('\n ⚠️ detected that you entered multiple names, the first parameter will be used as the project name, discard subsequent parameters oh '))}require('.. /lib/create')(name, options)
})
Copy the code
Create create project
Put the real processing logic in lib so that we can hopefully add more commands later or be more user-friendly. Next we write the lib/create file, which mainly deals with the file name validity check, whether the file exists and other configuration, check, execute the project creation logic, which we put in the lib/Creator file processing.
async function create (projectName, options) {
const cwd = options.cwd || process.cwd()
// Whether it is in the current directory
const inCurrent = projectName === '. '
const name = inCurrent ? path.relative('.. / ', cwd) : projectName
const targetDir = path.resolve(cwd, projectName || '. ')
const result = validatePackageName(name)
// If the package name entered is not a valid NPM package name, exit
if(! result.validForNewPackages) {console.error(chalk.red('Illegal item name:"${name}"`))
result.errors && result.errors.forEach(err= > {
console.error(chalk.red.dim('❌ + err))
})
result.warnings && result.warnings.forEach(warn= > {
console.error(chalk.red.dim('⚠️ ' + warn))
})
exit(1)}// Check if the folder exists
if (fs.existsSync(targetDir)) {
if (options.force) {
await fs.remove(targetDir)
} else {
await clearConsole()
if (inCurrent) {
const { ok } = await inquirer.prompt([
{
name: 'ok'.type: 'confirm'.message: `Generate project in current directory? `}])if(! ok) {return}}else {
const { action } = await inquirer.prompt([
{
name: 'action'.type: 'list'.message: 'Target folder${chalk.cyan(targetDir)}It already exists, please select: '.choices: [{name: 'cover'.value: 'overwrite' },
{ name: 'cancel'.value: false}}]])if(! action) {return
} else if (action === 'overwrite') {
console.log(`\nRemoving ${chalk.cyan(targetDir)}. `)
await fs.remove(targetDir)
}
}
}
}
await clearConsole()
// Start creating the project
const creator = new Creator(name, targetDir)
await creator.create(options)
}
module.exports = (. args) = > {
returncreate(... args).catch(err= > {
stopSpinner(false)
error(err)
})
}Copy the code
Through the above operations, the preparation work before creating the project is completed, and then the formal creation is carried out. The creation operation starts with the following code
const creator = new Creator(name, targetDir)
await creator.create(options)Copy the code
Create logic we put in a separate file called /lib/creator. The main things we do in this file are:
-
Pull remote template;
-
Ask for configuration related to project creation, such as: project name, project version, operator, etc.
-
Copy the pulled template file to the create project folder to generate the READme document;
-
Install project required dependencies;
-
Create git repository and complete project creation.
const chalk = require('chalk')
const execa = require('execa')
const inquirer = require('inquirer')
const EventEmitter = require('events')
const loadRemotePreset = require('.. /lib/utils/loadRemotePreset')
const writeFileTree = require('.. /lib/utils/writeFileTree')
const copyFile = require('.. /lib/utils/copyFile')
const generateReadme = require('.. /lib/utils/generateReadme')
const {installDeps} = require('.. /lib/utils/installDeps')
const {
defaults
} = require('.. /lib/options')
const {
log,
error,
hasYarn,
hasGit,
hasProjectGit,
logWithSpinner,
clearConsole,
stopSpinner,
exit
} = require('.. /lib/utils/common')
module.exports = class Creator extends EventEmitter {
constructor(name, context) {
super(a)this.name = name
this.context = context
this.run = this.run.bind(this)}async create(cliOptions = {}, preset = null) {
const { run, name, context } = this
if (cliOptions.preset) {
// awesome-test create foo --preset mobx
preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
} else {
preset = await this.resolvePreset(defaults.presets.default, cliOptions.clone)
}
await clearConsole()
log(chalk.blue.bold(`Awesome-test CLI vThe ${require('.. /package.json').version}`))
logWithSpinner(` ✨ `.'Creating a project${chalk.yellow(context)}. `)
this.emit('creation', { event: 'creating' })
stopSpinner()
// Set the file name, version number, etc
const { pkgVers, pkgDes } = await inquirer.prompt([
{
name: 'pkgVers'.message: Please enter the project version number.default: '1.0.0'}, {name: 'pkgDes'.message: 'Please enter project introduction'.default: 'project created by awesome-test-cli',}])// Copy the downloaded temporary files into the project
const pkgJson = await copyFile(preset.tmpdir, preset.targetDir)
const pkg = Object.assign(pkgJson, {
version: pkgVers,
description: pkgDes
})
// write package.json
log()
logWithSpinner('📄'.` generated${chalk.yellow('package.json')}Wait for template file ')
await writeFileTree(context, {
'package.json': JSON.stringify(pkg, null.2)})/ / package management
const packageManager = (
(hasYarn() ? 'yarn' : null) ||
(hasPnpm3OrLater() ? 'pnpm' : 'npm'))await writeFileTree(context, {
'README.md': generateReadme(pkg, packageManager)
})
const shouldInitGit = this.shouldInitGit(cliOptions)
if (shouldInitGit) {
logWithSpinner(` 🗃 `.Initialize Git repository)
this.emit('creation', { event: 'git-init' })
await run('git init')}// Install dependencies
stopSpinner()
log()
logWithSpinner(` ⚙ `.Install dependencies)
// log(' ⚙ installation dependency, please wait... `)
await installDeps(context, packageManager, cliOptions.registry)
// commit initial state
let gitCommitFailed = false
if (shouldInitGit) {
await run('git add -A')
const msg = typeof cliOptions.git === 'string' ? cliOptions.git : 'init'
try {
await run('git'['commit'.'-m', msg])
} catch (e) {
gitCommitFailed = true}}// log instructions
stopSpinner()
log()
log('🎉 project created successfully${chalk.yellow(name)}. `)
if(! cliOptions.skipGetStarted) { log(👉 Please press the following command to start happy development! \n\n` +
(this.context === process.cwd() ? ` ` : chalk.cyan(` ${chalk.gray('$')} cd ${name}\n`)) +
chalk.cyan(` ${chalk.gray('$')} ${packageManager === 'yarn' ? 'yarn start' : packageManager === 'pnpm' ? 'pnpm run start' : 'npm start'}`)
)
}
log()
this.emit('creation', { event: 'done' })
if (gitCommitFailed) {
warn(
'Could not initialize git commit for you because your Git username or email was incorrectly configured, \n' +
'Commit git later. \n`)}}async resolvePreset (name, clone) {
let preset
logWithSpinner(`Fetching remote preset ${chalk.cyan(name)}. `)
this.emit('creation', { event: 'fetch-remote-preset' })
try {
preset = await loadRemotePreset(name, this.context, clone)
stopSpinner()
} catch (e) {
stopSpinner()
error(`Failed fetching remote preset ${chalk.cyan(name)}: `)
throw e
}
// The default parameter is used by default
if (name === 'default' && !preset) {
preset = defaults.presets.default
}
if(! preset) { error(`preset "${name}" not found.`)
exit(1)}return preset
}
run (command, args) {
if(! args) { [command, ...args] = command.split(/\s+/)}return execa(command, args, { cwd: this.context })
}
shouldInitGit (cliOptions) {
if(! hasGit()) {return false
}
// --git
if (cliOptions.forceGit) {
return true
}
// --no-git
if (cliOptions.git === false || cliOptions.git === 'false') {
return false
}
// default: true unless already in a git repo
return! hasProjectGit(this.context)
}
}Copy the code
Now that we’re done creating the project, let’s look at the module creation of the project.
Page create module
Let’s go back to the entry file and add processing for the page command
// Create page command
program
.command('page <page-name>')
.description('create a new page')
.option('-f, --force'.'Overwrite target directory if it exists')
.action((name, cmd) = > {
const options = cleanArgs(cmd)
require('.. /lib/page')(name, options)
})Copy the code
Similar to create, our real logical processing is placed in lib/ Page. Page is responsible for similar tasks to create modules, such as checking whether the modified module already exists in the project and asking whether to overwrite it if it does.
const fs = require('fs-extra')
const path = require('path')
const chalk = require('chalk')
const inquirer = require('inquirer')
const PageCreator = require('./PageCreator')
const validFileName = require('valid-filename')
const {error, stopSpinner, exit, clearConsole} = require('.. /lib/utils/common')
/** * create project * @param {*} pageName * @param {*} options */
async function create (pageName, options) {
// Check whether the file name is compliant
const result = validFileName(pageName)
// If the package name entered is not a valid NPM package name, exit
if(! result) {console.error(chalk.red('Invalid file name:"${pageName}"`))
exit(1)}const cwd = options.cwd || process.cwd()
const pagePath = path.resolve(cwd, './src/pages', (pageName.charAt(0).toUpperCase() + pageName.slice(1).toLowerCase()))
const pkgJsonFile = path.resolve(cwd, 'package.json')
// If package.json does not exist, the root directory is no longer available and cannot be created
if(! fs.existsSync(pkgJsonFile)) {console.error(chalk.red(
'\n'+
'⚠️ Make sure you run this command \n' in the project root directory
))
return
}
// If the page already exists, ask to override or cancel
if (fs.existsSync(pagePath)) {
if (options.force) {
await fs.remove(pagePath)
} else {
await clearConsole()
const { action } = await inquirer.prompt([
{
name: 'action'.type: 'list'.message: ` existing${chalk.cyan(pageName)}Page, please select: '.choices: [{name: 'cover'.value: true},
{name: 'cancel'.value: false}},]])if(! action) {return
} else {
console.log(`\nRemoving ${chalk.cyan(pagePath)}. `)
await fs.remove(pagePath)
}
}
}
// Start creating the page
const pageCreator = new PageCreator(pageName, pagePath)
await pageCreator.create(options)
}
module.exports = (. args) = > {
returncreate(... args).catch(err= > {
stopSpinner(false)
error(err)
})
}Copy the code
After checking, execute the logic created by page with the following code
// Start creating the page
const pageCreator = new PageCreator(pageName, pagePath)
await pageCreator.create(options)Copy the code
From the lib/pageCreator file, we generate the target file by reading from a predefined template file, using a template language — Nunjucks — and place page generation in the utils/generatePage file as follows:
const chalk = require('chalk')
const path = require('path')
const fs = require('fs-extra')
const nunjucks = require('nunjucks')
const {
log,
error,
logWithSpinner,
stopSpinner,
} = require('./common')
const tempPath = path.resolve(__dirname, '.. /.. /temp')
const pageTempPath = path.resolve(tempPath, 'page.js')
const lessTempPath = path.resolve(tempPath, 'page.less')
const ioTempPath = path.resolve(tempPath, 'io.js')
const storeTempPath = path.resolve(tempPath, 'store.js')
async function generatePage(context, {lowerName, upperName}) {
logWithSpinner(` generated${chalk.yellow(`${upperName}/${upperName}.js`)}`)
const ioTemp = await fs.readFile(pageTempPath)
const ioContent = nunjucks.renderString(ioTemp.toString(), { lowerName, upperName })
await fs.writeFile(path.resolve(context, `. /${upperName}.js`), ioContent, {flag: 'a'})
stopSpinner()
}
async function generateLess(context, {lowerName, upperName}) {
logWithSpinner(` generated${chalk.yellow(`${upperName}/${upperName}.less`)}`)
const ioTemp = await fs.readFile(lessTempPath)
const ioContent = nunjucks.renderString(ioTemp.toString(), { lowerName, upperName })
await fs.writeFile(path.resolve(context, `. /${upperName}.less`), ioContent, {flag: 'a'})
stopSpinner()
}
async function generateIo(context, {lowerName, upperName}) {
logWithSpinner(` generated${chalk.yellow(`${upperName}/io.js`)}`)
const ioTemp = await fs.readFile(ioTempPath)
const ioContent = nunjucks.renderString(ioTemp.toString(), { lowerName, upperName })
await fs.writeFile(path.resolve(context, `./io.js`), ioContent, {flag: 'a'})
stopSpinner()
}
async function generateStore(context, {lowerName, upperName}) {
logWithSpinner(` generated${chalk.yellow(`${upperName}/store-${lowerName}.js`)}`)
const ioTemp = await fs.readFile(storeTempPath)
const ioContent = nunjucks.renderString(ioTemp.toString(), { lowerName, upperName })
await fs.writeFile(path.resolve(context, `./store-${lowerName}.js`), ioContent, {flag: 'a'})
stopSpinner()
}
module.exports = (context, nameObj) = > {
Promise.all([
generateIo(context, nameObj),
generatePage(context, nameObj),
generateStore(context, nameObj),
generateLess(context, nameObj)
]).catch(err= > {
stopSpinner(false)
error(err)
})
}Copy the code
It would be friendlier to import the file in PageCreator and execute it, giving some hints.
const chalk = require('chalk')
const EventEmitter = require('events')
const fs = require('fs-extra')
const generatePage = require('./utils/generatePage')
const {
log,
error,
logWithSpinner,
clearConsole,
stopSpinner,
exit
} = require('.. /lib/utils/common')
module.exports = class PageCreator extends EventEmitter {
constructor(name, context) {
super(a)this.name = name
this.context = context
}
async create(cliOptions = {}) {
const fileNameObj = this.getName()
const {context} = this
await clearConsole()
log(chalk.blue.bold(`Awesome-test CLI vThe ${require('.. /package.json').version}`))
logWithSpinner(` ✨ `.'Creating a page... `)
// Create a folder
await fs.mkdir(context, { recursive: true })
this.emit('creation', { event: 'creating' })
stopSpinner()
console.log(context)
await generatePage(context, fileNameObj)
}
getName() {
const originName = this.name
const tailName = originName.slice(1)
const upperName = originName.charAt(0).toUpperCase() + tailName
const lowerName = originName.charAt(0).toLowerCase() + tailName
return {
upperName,
lowerName
}
}
}Copy the code
Well, here we have completed the project creation and module creation of scaffolding, I believe you can’t wait to try it, along with this idea, we can enrich the function of the scaffolding, behind more beautiful creation we explore together!