From Zero to One: Building a React CLI Scaffolding Tool Based on Vue CLI Insights
This article documents the complete process of researching, analyzing, and developing a custom React CLI scaffolding tool, including a detailed examination of Vue CLI's architecture, plugin mechanism, and codebase, as well as the implementation of interactive command‑line features, configuration options, and publishing steps.
Introduction
The author reflects on the importance of content over flashy titles and aims to let readers experience the entire journey of developing a scaffolding tool from scratch.
1. Market Research and Discussion
Initial investigation of popular solutions revealed that create-react-app is too rigid because it hides webpack configuration, making complex projects difficult to customize. The eject option defeats the purpose of using the tool.
Consequently, the team turned to vue-cli , which was praised for its balance of flexibility and convention, aligning well with the philosophy of vue.js . The CLI's interactive command line and plugin system were especially appealing.
2. @vue/cli Source Analysis
The author cloned version 4.5.14 of @vue/cli (skipping the beta 5.0 version) and discovered that the project is managed with lerna + yarn-workspace under the @vue npm scope.
The core of @vue/cli consists of three parts:
@vue/cli – command‑line argument collection.
@vue/cli-service – core engine that provides the webpack configuration.
@vue/plugin-xxx – individual plugins that map to specific features, following a layered architecture similar to webpack-cli and @babel/cli .
Reading the source starts with the package.json bin field, which points to bin/vue.js . This file uses the commander library to register commands, notably the create command.
When vue create <project-name> is executed, the flow proceeds to ../lib/create , which builds the target directory path and initiates the Creator class (extending Node's EventEmitter ) to manage the plugin lifecycle.
Debugging reveals that the Creator class orchestrates the following steps:
Collect command‑line arguments.
Initialize plugins via this.initPlugins() , which calls each plugin's apply method with an api object.
Render templates using api.render() , which stores middleware functions for later execution.
Resolve all middleware to produce a map of file paths to file contents ( this.files ).
Write the files to disk with writeFileTree .
The plugin mechanism allows optional features to be injected on demand, improving extensibility and reducing code coupling.
3. Defining Features and Goals
Based on the analysis, the team identified three key design points:
Use a CLI for feature selection.
Separate the CLI from the core webpack service for independent version updates.
Leverage the plugin mechanism to generate different feature templates.
For a team‑specific scaffolding tool, the complexity of Vue CLI's plugin system was deemed unnecessary. The focus shifted to providing sensible defaults (e.g., antd for PC, postcss-pxtorem and flexible.js for mobile) and allowing optional state management and router versions.
4. Feature Development
(1) Project Initialization
The project uses a monorepo managed by lerna and yarn workspace to share node_modules and simplify version management. The CLI package is created under packages/react-booster-cli with a bin entry pointing to bin/booster.js .
#!/usr/bin/env node
// command‑line parsing
const program = require('commander');
program.version(require('../package').version).usage('
[options]');
program.command('create
')
.description('Create a new project')
.action((projectName) => {
require('../lib/create')(projectName);
});
program.parse(process.argv);The script is installed as an executable via the bin field, and yarn workspace creates a symlink in node_modules/.bin for easy execution with npx booster .
(2) Version Check
const request = require('request');
const semver = require('semver');
const chalk = require('chalk');
const packageConfig = require('../package.json');
module.exports = function checkVersion() {
return new Promise((resolve, reject) => {
if (!semver.satisfies(process.version, packageConfig.engines.node)) {
console.log(chalk.red(`You must upgrade node to >= ${packageConfig.engines.node}`));
return;
}
request({ url: 'https://registry.npmjs.org/react-booster-cli' }, (err, res, body) => {
if (!err && res.statusCode === 200) {
const latestVersion = JSON.parse(body)['dist-tags'].latest;
const localVersion = packageConfig.version;
if (semver.lt(localVersion, latestVersion)) {
console.log();
console.log(chalk.yellow('A newer version of booster-cli is available.'));
console.log();
console.log(` latest: ${chalk.green(latestVersion)}`);
console.log(` installed: ${chalk.red(localVersion)}`);
console.log();
}
resolve();
} else {
reject();
}
});
});
};(3) Interactive Command‑Line Prompts
const { prompt } = require('inquirer');
const questions = [
{ name: 'platform', type: 'list', message: 'Which platform?', choices: [{ name: 'PC', value: 'pc' }, { name: 'Mobile', value: 'mobile' }] },
{ name: 'isMPA', type: 'list', message: 'SPA or MPA?', choices: [{ name: 'SPA', value: false }, { name: 'MPA', value: true }] },
{ name: 'stateLibrary', type: 'list', message: 'State management library?', choices: [{ name: 'mobx', value: 'mobx' }, { name: 'redux', value: 'redux' }] },
{ name: 'reactRouterVersion', type: 'list', message: 'React Router version?', choices: [{ name: 'v5 (recommended)', value: 'v5' }, { name: 'v6 (hook support)', value: 'v6' }] }
];
module.exports = async function ask() { return prompt(questions); };(4) Project Generation
The generate module walks the template directory with globby , renders files with ejs (injecting the answers), and builds a file‑tree object.
const { isBinaryFileSync } = require('isbinaryfile');
const fs = require('fs');
const ejs = require('ejs');
const path = require('path');
function renderFile(filePath, ejsOptions = {}) {
if (isBinaryFileSync(filePath)) return fs.readFileSync(filePath);
const content = fs.readFileSync(filePath, 'utf-8');
if (/\/src\/.+/.test(filePath)) return ejs.render(content, ejsOptions);
return content;
}
async function generate(answers, targetDir) {
const globby = require('globby');
const fileList = await globby(['**/*'], { cwd: path.resolve(__dirname, '../template'), gitignore: true, dot: true });
const { isMPA } = answers;
const ejsData = { ...answers, projectDir: targetDir, pageName: 'index' };
const filesTreeObj = {};
fileList.forEach(oriPath => {
let targetPath = oriPath;
const absolutePath = path.resolve(__dirname, '../template', oriPath);
if (isMPA && /^src[\/].+/.test(oriPath)) {
const [dir, file] = oriPath.split(/[\/]+/);
['index', 'pageA', 'pageB'].forEach(pageName => {
targetPath = `${dir}/pages/${pageName}/${file}`;
filesTreeObj[targetPath] = renderFile(absolutePath, { ...ejsData, pageName });
});
} else {
filesTreeObj[targetPath] = renderFile(absolutePath, ejsData);
}
});
return filesTreeObj;
}
module.exports = generate;(5) Create Command Implementation
The main create function handles directory existence, prompts the user, checks versions, updates package.json based on selected options, renders the template, writes files, initializes a git repository, and prints next‑step instructions.
const path = require('path');
const fs = require('fs');
const exists = fs.existsSync;
const rm = require('rimraf').sync;
const ask = require('./ask');
const inquirer = require('inquirer');
const ora = require('ora');
const chalk = require('chalk');
const checkVersion = require('./check-version');
const generate = require('./generate');
const { writeFileTree } = require('./util/file');
const runCommand = require('./util/run');
const spinner = ora();
async function create(projectName) {
const cwd = process.cwd();
const projectPath = path.resolve(cwd, projectName);
if (exists(projectPath)) {
const answers = await inquirer.prompt([{ type: 'confirm', message: 'Target directory exists. Replace?', name: 'ok' }]);
if (answers.ok) {
console.log(chalk.yellow('Deleting old project...'));
rm(projectPath);
await create(projectName);
}
return;
}
const answers = await ask();
spinner.start('check version');
await checkVersion();
spinner.succeed();
console.log(`✨ Creating project in ${chalk.yellow(projectPath)}.`);
const pkg = require('../template/package.json');
const appConfig = {};
const { platform, isMPA, stateLibrary, reactRouterVersion } = answers;
if (platform === 'mobile') {
pkg.devDependencies['postcss-pxtorem'] = '^6.0.0';
pkg.dependencies['lib-flexible'] = '^0.3.2';
} else if (platform === 'pc') {
pkg.dependencies['antd'] = 'latest';
}
pkg.dependencies[stateLibrary] = 'latest';
if (reactRouterVersion === 'v5') pkg.devDependencies['react-router'] = '5.1.2';
else if (reactRouterVersion === 'v6') pkg.dependencies['react-router'] = '^6.x';
appConfig.platform = platform;
spinner.start('rendering template');
const filesTreeObj = await generate(answers, projectPath);
spinner.succeed();
spinner.start('🚀 invoking generators...');
await writeFileTree(projectPath, {
...filesTreeObj,
'package.json': JSON.stringify(pkg, null, 2),
'app.config.json': JSON.stringify(appConfig, null, 2)
});
spinner.succeed();
console.log('🗃 Initializing git repository...');
await runCommand('git init');
console.log();
console.log(`🎉 Successfully created project ${chalk.yellow(projectName)}.`);
console.log(`👉 Get started with the following commands:\n\n` +
chalk.cyan(` $ cd ${projectName}\n`) +
chalk.cyan(' $ npm install or yarn\n') +
chalk.cyan(' $ npm run dev'));
console.log();
}
module.exports = (...args) => create(...args).catch(err => {
spinner.fail('create error');
console.error(chalk.red.dim('Error: ' + err));
process.exit(1);
});5. Publishing the NPM Package
The final steps involve logging into npm and using lerna publish . The author shares pitfalls such as name collisions on npm, the need to retry publishing with lerna publish from-git , and the delay when using the Taobao mirror.
Additional notes clarify the difference between dependencies and devDependencies for end‑users of the published package.
References
[1] https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli
[2] https://juejin.cn/post/6844903870578032647#heading-15
[3] https://www.npmjs.com/
[4] https://www.npmjs.com/package/react-booster-cli
[5] https://github.com/FEyudong/booster/tree/master/packages/react-booster-cli
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.