Frontend Development 14 min read

Implementing Customizable and Upgradable Template Management in a Frontend CLI Tool

This article demonstrates how to design and develop a CLI tool that lets users add, update, list, and download customizable project templates, leveraging Inquirer for interactive prompts, download‑git‑repo for fetching repositories, and the GitHub API for branch selection, with full TypeScript code examples.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Implementing Customizable and Upgradable Template Management in a Frontend CLI Tool

Introduction

The third part of the front‑end engineering series focuses on template management for a CLI tool, addressing the need for customizable and upgradable templates in real‑world projects.

Feature Design

To avoid repeatedly updating the CLI for each new template, the tool should support custom configuration and upgradable templates, allowing users to add, delete, and update template information locally and fetch the latest versions dynamically.

Key functional requirements include:

Store the template source URL.

Fetch different template code based on user selection.

Save the template locally.

Implementation – Local Template Storage

Using inquirer for interactive prompts, the CLI gathers the repository URL, template name, and description, then saves the data to a JSON file.

import inquirer from 'inquirer';
import { addTpl } from '@/tpl';

const promptList = [
  {
    type: 'input',
    message: '请输入仓库地址:',
    name: 'tplUrl',
    default: 'https://github.com/boty-design/react-tpl'
  },
  {
    type: 'input',
    message: '模板标题(默认为 Git 名作为标题):',
    name: 'name',
    default({ tplUrl }) {
      return tplUrl.substring(tplUrl.lastIndexOf('/') + 1);
    }
  },
  {
    type: 'input',
    message: '描述:',
    name: 'desc'
  }
];

export default () => {
  inquirer.prompt(promptList).then((answers) => {
    const { tplUrl, name, desc } = answers;
    addTpl(tplUrl, name, desc);
  });
};

The template information is then persisted to a local .tpl.json file:

import { loggerError, loggerSuccess, getDirPath } from '@/util';
import { loadFile, writeFile } from '@/util/file';

interface ITpl {
  tplUrl: string;
  name: string;
  desc: string;
}

const addTpl = async (tplUrl: string, name: string, desc: string) => {
  const cacheTpl = getDirPath('../cacheTpl');
  try {
    const tplConfig = loadFile
(`${cacheTpl}/.tpl.json`);
    let file = [{ tplUrl, name, desc }];
    if (tplConfig) {
      const isExist = tplConfig.some(tpl => tpl.name === name);
      if (isExist) {
        file = tplConfig.map(tpl => (tpl.name === name ? { tplUrl, name, desc } : tpl));
      } else {
        file = [...tplConfig, ...file];
      }
    }
    writeFile(cacheTpl, '.tpl.json', JSON.stringify(file, null, "\t"));
    loggerSuccess('Add Template Successful!');
  } catch (error) {
    loggerError(error);
  }
};

export { addTpl };

Implementation – Downloading Templates

Templates are downloaded using download-git-repo . Users first select a template from the locally stored list via Inquirer:

export const selectTpl = () => {
  const tplList = getTplList();
  const promptList = [
    {
      type: 'list',
      message: '请选择模板下载:',
      name: 'name',
      choices: tplList && tplList.map((tpl: ITpl) => tpl.name)
    },
    {
      type: 'input',
      message: '下载路径:',
      name: 'path',
      default({ name }: { name: string }) {
        return name.substring(name.lastIndexOf('/') + 1);
      }
    }
  ];
  inquirer.prompt(promptList).then((answers) => {
    const { name, path } = answers;
    const select = tplList && tplList.filter((tpl: ITpl) => tpl.name);
    const tplUrl = select && select[0].tplUrl || '';
    loadTpl(name, tplUrl, path);
  });
};

The actual download logic handles both normal and direct URLs:

export const loadTpl = (name: string, tplUrl: string, path: string) => {
  download(`direct:${tplUrl}`, getCwdPath(`./${path}`), (err: string) => {
    if (err) {
      loggerError(err);
    } else {
      loggerSuccess(`Download ${name} Template Successful!`);
    }
  });
};

GitHub API Integration for Branch Selection

To support repositories with multiple branches, the CLI queries the GitHub API ( https://api.github.com/repos/{owner}/{repo}/branches ) to retrieve branch names. If only one branch exists, it is used directly; otherwise, the user is prompted to choose a branch.

export const selectTpl = async () => {
  const prompts: any = new Subject();
  let select: ITpl;
  let githubName: string;
  let path: string;
  let loadUrl: string;
  try {
    const onEachAnswer = async (result: any) => {
      const { name, answer } = result;
      if (name === 'name') {
        githubName = answer;
        select = tplList.filter((tpl: ITpl) => tpl.name === answer)[0];
        const { downloadUrl, org } = select;
        const branches = await getGithubBranch(select) as IBranch[];
        loadUrl = `${downloadUrl}/${org}/zip/refs/heads`;
        if (branches.length === 1) {
          loadUrl = `${loadUrl}/${branches[0].name}`;
          prompts.next({
            type: 'input',
            message: '下载路径:',
            name: 'path',
            default: githubName
          });
        } else {
          prompts.next({
            type: 'list',
            message: '请选择分支:',
            name: 'branch',
            choices: branches.map((branch: IBranch) => branch.name)
          });
        }
      }
      if (name === 'branch') {
        loadUrl = `${loadUrl}/${answer}`;
        prompts.next({
          type: 'input',
          message: '下载路径:',
          name: 'path',
          default: githubName
        });
      }
      if (name === 'path') {
        path = answer;
        prompts.complete();
      }
    };
    const onError = (error: string) => loggerError(error);
    const onCompleted = () => loadTpl(githubName, loadUrl, path);
    inquirer.prompt(prompts).ui.process.subscribe(onEachAnswer, onError, onCompleted);
    const tplList = getTplList() as ITpl[];
    prompts.next({
      type: 'list',
      message: '请选择模板:',
      name: 'name',
      choices: tplList.map((tpl: ITpl) => tpl.name)
    });
  } catch (error) {
    loggerError(error);
  }
};

Conclusion

The CLI now supports adding custom templates, persisting them locally, downloading specific branches, and handling interactive prompts dynamically with RxJS. A summary table of available commands is provided, and the source code is hosted on GitHub for further reference.

CLI Command

Function

fe-cli eslint

Run ESLint on the current project

fe-cli webpack

Build the project with Webpack

fe-cli rollup

Build the project with Rollup

fe-cli git init

Initialize a local Git repository (partial GitLab support)

fe-cli add tpl

Add a custom template

fe-cli init tpl

Initialize a template locally

frontendCLITypeScriptinquirertemplate managementdownload-git-repoGitHub API
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.