Design and Implementation of a Configurable CLI Plugin System for Front‑End Projects
This article describes how to redesign a front‑end CLI tool by introducing a router‑like entry, converting hard‑coded commands into configurable objects, implementing a third‑party plugin registration workflow, and refactoring file handling with fs‑extra to improve maintainability and scalability across teams.
The article begins by reviewing the existing CLI tool that already supports building, linting, and template management, but notes that these features are insufficient for larger teams that need a shared, extensible infrastructure.
Design & Research
A router‑style main entry is proposed to differentiate between built‑in and third‑party commands. The original hard‑coded commands are refactored into configuration objects that can be iterated and registered dynamically.
program
.version('0.1.0')
.description('start eslint and fix code')
.command('eslint')
.action((value) => { execEslint() })Commands are now exported as objects, for example:
import { execEslint } from '@/index'
export const eslintCommand = {
version: '0.1.0',
description: 'start eslint and fix code',
command: 'eslint',
action: () => execEslint()
}
export default [eslintCommand]The main entry loads these configurations and registers them in bulk:
import path from "path";
import alias from "module-alias";
alias(path.resolve(__dirname, "../../"));
import { Command } from 'commander';
import internallyCommand from './internally';
const program = new Command(require('../../package').commandName);
interface ICommand {
version: string;
description: string;
command: string;
action: (value?: any) => void;
}
const initCommand = (commandConfig: ICommand[]) => {
commandConfig.forEach(config => {
const { version, description, command, action } = config;
program.version(version)
.description(description)
.command(command)
.action((value) => { action(value) })
})
}
initCommand(internallyCommand);
program.parse(process.argv);Developing Custom Plugin Registration
The workflow for adding third‑party plugins includes entering the plugin name, validating it, checking the latest version, installing dependencies with shelljs , and caching the plugin configuration.
import inquirer from 'inquirer';
const promptList = [{
type: 'input',
message: '请输入插件名称:',
name: 'pluginName',
default: 'fe-plugin-eslint',
validate(v) { return v.includes('fe-plugin-') },
transformer(v) { return `@boty-design/${v}` }
}];
export const registerPlugin = () => {
inquirer.prompt(promptList).then((answers) => {
const { pluginName } = answers;
console.log(pluginName);
})
}After validation, the plugin is installed using:
export const npmInstall = async (packageName: string) => {
try {
shelljs.exec(`yarn add ${packageName}`, { cwd: process.cwd() });
} catch (error) {
loggerError(error);
process.exit(1);
}
}The plugin metadata is saved to a JSON cache file:
export const updatePlugin = async (params: IPlugin) => {
const { name } = params;
let isExist = false;
try {
const pluginConfig = loadFile
(`${cacheTpl}/.plugin.json`);
let file = [{ name }];
if (pluginConfig) {
isExist = pluginConfig.some(tpl => tpl.name === name);
if (!isExist) {
file = [...pluginConfig, { name }];
}
}
writeFile(cacheTpl, '.plugin.json', file);
loggerSuccess(`${isExist ? 'Update' : 'Add'} Template Successful!`);
} catch (error) {
loggerError(error);
}
}CLI Plugin Template
A minimal template must expose the same command registration shape as built‑in commands, enabling maximum flexibility for third‑party developers.
import { getEslint } from './eslint'
export const execEslint = async () => { await getEslint() }
export const eslintCommand = {
version: '0.1.0',
description: 'start eslint and fix code',
command: 'tEslint',
action: () => execEslint()
}
export default [eslintCommand]
module.exports = eslintCommandPlugin Management
To avoid manual tracking of many plugins, a pseudo‑market is built that fetches plugin information from a GitHub organization via the GitHub API, allowing operations such as listing, updating, and deprecating plugins.
Project Refactor
Several refactorings improve the CLI:
Replace fs with fs-extra to simplify JSON file operations.
Store cache files in the system’s home directory using os.homedir() to survive project upgrades.
Use latest-version to check for newer CLI releases and warn users.
export const writeFile = (path: string, fileName: string, file: object, system: boolean = true) => {
const rePath = system ? `${os.homedir()}/${path}` : path;
try {
fs.outputJsonSync(`${rePath}/${fileName}`, file);
loggerSuccess('Writing file successful!');
} catch (err) {
loggerError(`Error writing file from disk: ${err}`);
}
}Conclusion
The CLI now supports configurable third‑party plugins, improved file handling, version checking, and a basic plugin marketplace, providing a solid foundation for further enhancements and team‑wide infrastructure development.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
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.