npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zjy79866/zjy-cli

v1.0.1

Published

A modern frontend CLI scaffolding tool

Readme

从零搭建前端 CLI 脚手架

一、为什么要搭建自己的脚手架

在企业级前端开发中,团队通常会面临以下痛点:

  • 项目初始化重复劳动:每个新项目都要手动搭建目录结构、配置构建工具、编写基础组件,耗时且容易出错。
  • 规范难以统一:不同成员对目录结构、代码规范、构建工具的理解不一致,导致项目之间差异大,维护成本高。
  • 公共配置散落各处:Vite/Webpack 配置、ESLint、Prettier 等工具的配置每个项目各自维护,升级或修改时需要逐个调整。
  • 新人上手成本高:新成员需要了解大量重复的基础设施配置,而非专注于业务开发。

CLI 脚手架正是解决这些问题的关键基础设施——它将项目初始化、构建、开发等流程标准化、自动化,各前端服务能够专注于自己系统的业务逻辑。


二、架构设计

zjy-cli/
├── package.json              # 包配置,定义 bin 入口
├── bin/
│   └── index.js              # CLI 可执行入口(系统命令入口)
├── src/                      # 源码(ESM 格式,开发阶段)
│   ├── index.js              # 应用入口:node版本检查、环境初始化
│   ├── cli.js                # 命令注册中心:Commander 初始化 + 动态加载子命令
│   ├── utils/                # 公共工具模块
│   └── commands/             # 每个命令对应一个单独文件夹
│       ├── create/           # 项目初始化命令
│       ├── serve/            # 开发服务器命令
│       └── build/            # 构建命令
├── templates/                # 内置模板目录(随包发布)
│   └── vue3-vite/
├── dist/                     # 构建产物(Rollup 打包)
│   └── bundle.js
└── rollup.config.js          # 打包配置

核心设计原则:

  1. 命令独立:每个命令对应 commands/ 下的独立目录,结构清晰,职责单一。
  2. 懒加载:子命令模块通过 await import() 动态加载,减少 CLI 启动时间。
  3. 公共与私有分离:utils/ 只放公共工具,命令逻辑全部在 commands/ 下。
  4. 配置分层:脚手架默认配置 < 目标项目自定义配置 < CLI 命令行参数,优先级逐层覆盖。

三、技术栈选型

| 技术 | 用途 | 选型理由 | |------|------|----------| | commander | CLI 命令解析、参数定义 | 最成熟的 Node.js CLI 框架,支持链式注册、自动帮助信息生成 | | prompts | 交互式命令行问答 | 轻量、灵活,支持文本、选择、确认等多种交互类型 | | lodash.template | 模板变量渲染 | 使用 <% %> 语法,轻量且稳定,适合文件内容替换 | | fs-extra | 文件/目录操作 | 封装 Node.js fs 模块,提供 ensureDir、remove 等便捷 API | | vite | 构建工具 API 调用 | 通过动态 import 加载,保持 external,不打入产物 | | signale | 彩色日志输出 | 开箱即用的美观日志,支持 info/success/error/warn 等级别 | | rollup | CLI 代码打包 | 擅长打包 ESM 格式,输出体积小,适合单文件 CLI 场景 |


四、搭建步骤

步骤 1:初始化脚手架项目

先搭建脚手架目录结构

在 package.json 中声明:

{
  "name": "zjy-cli",
  "version": "1.0.0",
  "type": "module", // 项目使用 ESM 模块格式
  "bin": {
    "zjy": "./bin/index.js" // 声明全局命令名 `zjy` 对应的入口文件
  },
  "files": [ // 发布到 npm 时只打包这些目录
    "bin",
    "dist",
    "templates"
  ]
}

步骤 2:创建 CLI 入口 bin/index.js

bin/index.js 是 CLI 的"系统入口层",解决了开发/生产双模式切换:

  • 有 -tp → 加载 src/ 源码(开发)
  • 无 -tp → 加载 dist/bundle.js(生产)
#!/usr/bin/env node

import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const cliDir = resolve(__dirname, '..');

const tpIndex = process.argv.indexOf('-tp');
const isDev = tpIndex !== -1;

if (isDev) {
  console.log('[zjy-cli] 本地调试,走src源码...');
  process.argv.splice(tpIndex, 1);
  await import(resolve(cliDir, 'src', 'index.js'));
} else {
  console.log('[zjy-cli] 生产模式,走打包产物...');
  await import(new URL('../dist/bundle.js', import.meta.url));
}

#!/usr/bin/env node(shebang)的作用:

让系统识别这个文件是一个 Node.js 脚本。当用户在全局安装 CLI 后,可以直接通过命令名(如 zjy)执行,而不需要显式写 node。

为什么需要 bin/index.js 而不是直接用 src/index.js 作为入口?

  1. bin/ 是 npm 社区惯例,表示"可执行入口",src/ 表示源代码。混在一起会让 package.json 的意图变得不清晰
  2. 如果去掉 bin/index.js,这个切换逻辑就要搬到 src/index.js 里,但打包后 src/index.js 会变成 bundle.js,动态 import('src/...') 的路径解析会变得别扭

步骤 3:创建应用入口 src/index.js

import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import { createCli } from './cli.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = resolve(dirname(__filename), '..');

// Node.js 版本校验
const nodeVersion = parseInt(process.version.split('.')[0]);
if (nodeVersion < 18) {
  console.error('[zjy-cli] Node.js version must be >= 18.0.0');
  process.exit(1);
}

// 挂载项目根目录到全局,供子模块使用
globalThis.__MY_CLI_DIR__ = __dirname;

// 启动 CLI
createCli();

职责非常纯粹:校验运行环境、挂载全局路径、启动命令解析。

步骤 4:注册命令 — src/cli.js

import { Command } from 'commander';
import { readFileSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'));

export async function createCli() {
  const program = new Command();

  program
    .name('zjy')
    .description('自定义前端脚手架')
    .version(pkg.version);

  // 动态加载各命令模块(懒加载,减少启动开销)
  const createModule = await import('./commands/create/index.js');
  const buildModule = await import('./commands/build/index.js');
  const serveModule = await import('./commands/serve/index.js');

  // 注册子命令
  createModule.register(program);
  buildModule.register(program);
  serveModule.register(program);

  // 解析命令行参数,触发对应命令执行
  program.parse();
}

注册命令的核心流程:

  1. 用 Commander 创建 CLI 实例,设置名称、描述、版本号。
  2. await import() 动态导入各命令模块(而非顶部静态 import),只在需要时加载对应代码。
  3. 调用各模块导出的 register(program) 方法,将子命令挂载到 program 上。
  4. program.parse() 解析用户输入,匹配到对应命令后执行。

步骤 5:实现 create 命令(项目初始化)

实现原理:

  1. 通过 command 注册命令
  2. 通过 prompts 交互式问答(当没传必要options时)
  3. 获取模板
  4. 递归渲染模板文件,通过 lodash.template 变量替换渲染,复制到目标目录

模板初始化方案对比:

| 方案 | 说明 | 优点 | 缺点 | |------|------|------|------| | 模板内置(本文采用) | 模板随 CLI 发布,放在 templates/ 目录 | 开箱即用,无需额外网络请求,维护简单 | 更新模板需升级 CLI 版本,包体积较大 | | 模板独立发布 | 模板是独立 npm 包,运行时按需下载安装 | 模板独立迭代,不同团队可维护各自模板 | 依赖 npm registry,增加复杂度 |

步骤 6:实现 serve 和 build 命令

zjy serve 和 zjy build 通过 CLI 内嵌的 Vite API 启动,目标项目无需安装 vite 或 @vitejs/plugin-vue,但开发环境和 CI 流水线需先 npm install -g zjy-cli

根据目标项目的 package.json 配置 ,npm run dev、npm run build 实际执行的是 zjy,然后通过脚手架里的 vite 运行:

"scripts": {
  "dev": "zjy serve",
  "build": "zjy build"
}

补充:如果你希望别人拿到你的项目可以直接 npm i + npm run dev 跑起来,需要:

{
  "scripts": {
    "dev": "npx zjy serve", // 2. 前面加npx(如果没有npx,只会在全局找zjy命令)
    "build": "npx zjy build"
  },
  "dependencies": {
    "zjy-cli": "^1.0.0", // 1. 加入依赖
    "pinia": "^2.1.7",
    "vue": "^3.4.0"
  }
}

配置管理体系

serve 和 build 共用同一套配置加载逻辑:

CLI flag(通过命令选项传入) > zjy.config.js(目标项目根目录) > 脚手架默认配置(zjy-cli的vite.common.config.js)
  1. 未找到 zjy.config.js → 使用脚手架默认配置
  2. 找到 zjy.config.js → 合并覆盖脚手架默认配置
  3. CLI flag 优先级最高 → 如 zjy serve -p 1333 覆盖 zjy.config.js 中的端口

这种设计的优势:脚手架提供一套默认公共配置保证团队规范统一,目标项目可以通过配置文件覆盖特定配置,CLI 参数拥有最高优先级提供灵活调整能力。

步骤 7:rollup 打包发布

// rollup.config.js
export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'es',
    banner: '#!/usr/bin/env node',     // 产物添加 shebang
    inlineDynamicImports: true          // 内联动态 import
  },
  external: ['vite', '@vitejs/*'],      // 排除构建工具,保持 external
  plugins: [nodeResolve(), commonjs(), json()]
};
npm run build        # 打包
npm publish          # 发布到 npm

最终发布到npm的是:

  • bin/ — CLI 入口
  • dist/ — Rollup 打包产物
  • templates/ — 内置模板
  • package.json、README.md 等(npm 默认包含)
    src/、node_modules/、rollup.config.js 等不会发布

本地调试

-tp 是 CLI 全局开发标记,不是 任何脚手架命令的参数。它的作用是让 bin/index.js 加载源码而非打包产物。
npm link 是 建立全局命令与本地目录的符号链接。核心价值是开发过程中无需反复 npm publish,修改源码后直接用 zjy xxx -tp 命令测试(如果不-tp,就是走dist,脚手架要先npm build再zjy xxx)

npm link # 建立软连接
npm ls -g --depth 0 # 查看软连接
npm unlink -g zjy-cli

npm link(推荐)

# 在 zjy-cli 项目根目录下(此时全局命令 zjy 实际指向 ./bin/index.js 的符号链接)
npm link

# 切换到其他目录(目标项目所在目录)
cd /path/to/target-project
zjy create zjy-app -tp     # -tp 走源码,不走打包产物

或

npm link  
npm run build 

# 切换到其他目录(目标项目所在目录)
cd /path/to/target-project
zjy create my-app     # 非-tp 走dist,需要脚手架先重新打包

直接运行源码

# 在 zjy-cli 项目根目录下
node src/index.js create my-app

断点调试

1、在源码中打断点
2、在脚手架项目中启动 Debug 终端,cd到目标项目,执行 zjy xxx -tp