Fundamentals 14 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Design and Implementation of a Configurable CLI Plugin System for Front‑End Projects

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 = eslintCommand

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

cliTypeScriptPlugin Architecturenode.jsfs-extracommand registration
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.