How to Build Your Own Front‑End CLI Scaffold from Scratch
This article walks through why a custom front‑end scaffolding tool is needed, analyzes Vue CLI’s architecture, explains how to locate global commands, examines required dependencies, and provides a step‑by‑step implementation—including project initialization, command design, create‑command logic, Node version checks, template features, CDN upload, commit linting, and a custom ESLint plugin—so readers can create a reusable CLI tailored to their business needs.
Introduction
Standard scaffolding tools such as Vue CLI and Create‑React‑App accelerate project bootstrapping but cannot satisfy internal requirements like a shared architecture, unified request handling, theming, business components, and custom ESLint rules. A custom CLI scaffold solves these problems by providing a zero‑configuration project generator that incorporates company‑specific templates and standards.
Vue CLI Architecture Overview
Vue CLI consists of: @vue/cli – interactive project scaffolding. @vue/cli-service-global – zero‑configuration prototype development. @vue/cli-service – runtime dependency built on webpack with upgradeable defaults.
Official plugin ecosystem for extensibility.
Graphical UI for project creation and management.
Locating the Global Vue Command
On macOS the command where vue shows the executable path. The bin field in Vue CLI’s package.json creates a global symlink that points to the command file.
Dependency Analysis
commander – full‑featured command‑line argument parser.
shelljs – executes shell commands.
inquirer – interactive CLI prompts.
semver – semantic version comparison.
chalk – terminal string styling.
Features Required for a Custom Scaffold
Interactive CLI that gathers user input.
Zero‑configuration project generation from predefined templates.
Implementation Steps
Project Initialization
Create a folder dt-fe-cli, run npm init -y, add a bin directory with cli.mjs as the entry point, and configure the bin field in package.json:
{
"name": "@auto/dt-fe-cli",
"version": "0.0.1",
"bin": {
"dt-fe-cli": "bin/cli.mjs"
}
}CLI Commands
The tool provides --version, --help, and a create command. The create command accepts a project name and a -f, --force flag to overwrite an existing directory.
import create from '../lib/create.mjs';
program
.command('create <project-name>')
.description('create a new project powered by dt-fe-cli')
.option('-f, --force', 'overwrite target directory if it exists')
.action((projectName, options) => {
create(projectName, options);
});Create Command Logic
The command performs the following steps:
Verify that git is installed.
Check whether the target directory already exists; if so, either remove it automatically (when --force is used) or ask the user to confirm overwriting.
Prompt the user to select a project type (e.g., pc or h5).
Clone the corresponding template repository from an internal GitLab server.
Remove the original .git folder, then display next‑step instructions.
import chalk from 'chalk';
import fse from 'fs-extra';
import shelljs from 'shelljs';
import path from 'path';
import inquirer from 'inquirer';
export async function create(projectName, options) {
const targetDir = path.join(process.cwd(), projectName);
if (!shelljs.which('git')) {
console.log(chalk.red('dt-fe-cli requires git'));
return;
}
const exists = await fse.pathExists(targetDir);
if (exists) {
if (options.force) {
await fse.remove(targetDir);
} else {
const { isOverwrite } = await inquirer.prompt([
{
name: 'isOverwrite',
type: 'list',
message: 'Target directory already exists. Choose an action:',
choices: [
{ name: 'Overwrite', value: true },
{ name: 'Cancel', value: false }
]
}
]);
if (!isOverwrite) return;
await fse.remove(targetDir);
}
}
const { projectType } = await inquirer.prompt([
{
name: 'projectType',
type: 'list',
message: 'Select project type:',
choices: [
{ name: 'pc', value: 'pc' },
{ name: 'h5', value: 'h5' }
]
}
]);
const PROJECT_MAP = { pc: 'pc.git', h5: 'h5.git' };
shelljs.exec(`git clone ${PROJECT_MAP[projectType]} ${projectName}`, async code => {
if (code === 0) {
await fse.remove(path.join(process.cwd(), projectName, '.git'));
console.log(`
Successfully created project ${chalk.cyan(projectName)}`);
console.log(`
cd ${chalk.cyan(projectName)}`);
console.log(' git init');
console.log(' pnpm install');
console.log(' pnpm dev');
}
});
}Node Version Check
The CLI reads the engines.node field from its own package.json and uses semver to ensure the running Node version satisfies the requirement. If not, it aborts with an error message.
import { readFile } from 'fs/promises';
import semverSatisfies from 'semver/functions/satisfies.js';
import chalk from 'chalk';
const { engines: { node: requiredVersion } } = JSON.parse(
await readFile(new URL('../package.json', import.meta.url))
);
function checkNodeVersion(wanted, id) {
if (!semverSatisfies(process.version, wanted, { includePrerelease: true })) {
console.log(
chalk.red(
`Current Node ${process.version} does not satisfy ${id} requirement ${wanted}. Please upgrade.`
)
);
process.exit(1);
}
}
checkNodeVersion(requiredVersion, 'dt-fe-cli');Template Design Support
The generated projects are based on Vite 3 and include the following built‑in capabilities:
TypeScript support (native .ts files).
Automatic CDN upload after a production build.
Commit linting via Husky.
A custom ESLint plugin that forbids console.log.
Automatic CDN Upload Example
const { execSync } = require('child_process');
const { loadEnv } = require('vite');
const env = process.argv[2];
const { VITE_BASE_URL } = loadEnv(env, process.cwd(), '');
const prefix = `${env}${VITE_BASE_URL}`;
execSync(`vite build --mode ${env} --base=https://cdn.com/${prefix}`);
// Assume uploadCDN is a helper that pushes the built assets.
uploadCDN({ Dir: 'dist/assets', Prefix: prefix });Commit Validation with Husky
npm install husky --save-dev
npm pkg set scripts.prepare="husky install"
npm run prepare
npx husky add .husky/pre-commit "npm run lint"Custom ESLint Plugin Creation
The plugin is scaffolded with Yeoman (or manually) and consists of a package.json, an entry file lib/index.js, and rule definitions under lib/rules. Below is a minimal rule that reports an error whenever console.log is used.
module.exports = {
meta: {
type: 'suggestion',
fixable: 'code',
schema: []
},
create(context) {
return {
'CallExpression MemberExpression'(node) {
const { object, property } = node;
if (object.name === 'console' && property.name === 'log') {
context.report({
node,
message: 'console.log is forbidden.'
});
}
}
};
}
};Conclusion
The guide demonstrates a complete, step‑by‑step process for building a custom front‑end CLI scaffold: initializing the project, defining commands, handling template cloning, enforcing Node version constraints, and integrating auxiliary tools such as Vite, Husky, and a bespoke ESLint plugin. The resulting CLI can be published to an internal npm registry and reused across multiple business units, ensuring consistent architecture, coding standards, and automated setup.
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.
