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