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:

  1. 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;

  2. 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.

  3. 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:

  1. Repetitive work, tedious and time-consuming

  2. A copy template is prone to extraneous code

  3. There are many configuration areas in the project, and it is easy to overlook some configuration points

  4. Human error is always possible, and it takes time to correct errors when building a new project

  5. 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!