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

polo-cli

v1.0.1

Published

cli

Downloads

9

Readme

chuhc-cli 脚手架

安装

$ npm i @chuhc/cli -g
$ chuhc create test

前言

掘金文章

后面去看了@vue/cli的源码,不得不说,确实很正式很规范化。参考了@vue/cli 的部分功能,对脚手架进行修改。

效果栗子

image

项目结构

项目chuhc-react的结构分成了四个部分:

  • @chuhc/cli 脚手架命令行内容,通过命令去初始化项目等等操作。
  • @chuhc/scripts 项目编译运行打包内容,暂未接入部署等流程。
  • @chuhc/template 模板文件。
  • @chuhc/plugin-xxx 项目封装的插件,例如@chuhc/plugin-typescript等等。

cli

之前是直接通过inquirer的一些交互命令,获取信息后,通过download-git-repo去对应github拉取模板文件,这样虽然比较简单,但是多个模板的话,维护就很难受了。

而在@vue/cli里,是通过多个插件组合成一个整体模板,在很多脚手架根目录下,都有一个xxx.config.js暴露出来。然后在运行node命令时,去读取配置文件,根据配置文件的内容去进行对应操作,例如:使用webpack-chain动态修改config,最后再调用toConfig去生成新的webpack配置内容。

插件判断,生成 pkg

一个基本的package.json模板,除了常规不变的versionprivatelicense等等,像namescriptsdependenciesdevDependencies需要我们去手动添加进去。

name就使用脚手架初始化传入的参数,而scripts则是在成功引入@chuhc/scripts后,使用其运行命令。

像一些必备的,例如reactreact-dom,我们可以直接放到dependencies里,而devDependencies一般是初始化时,用户手动选择的plugins

借助inquirer去罗列插件,让用户选择需要引入哪些插件。

const CHUHC_PLUGIN_CHECK = [
  {
    name: 'Typescript',
    value: ['tsx', '@chuhc/plugin-typescript'],
  },
  {
    name: 'Less',
    value: ['less', '@chuhc/plugin-less'],
  },
];

const { plugins } = await inquirer.prompt([
  {
    type: 'checkbox',
    name: 'plugins',
    message: 'Do you need these plugins',
    choices: CHUHC_PLUGIN_CHECK, // 一个结构,自己觉得怎么处理方便,怎么来
  },
]);

那么根据上面的code就可以获取到,用户需要哪些plugin了,那么可以将这些plugin放入到jsondevDependencies里。

获取依赖最新版本

上述无论是react还是plugin,都需要一个版本号,我这里是采用命令行去获取最新版本,然后作为其value值。如果直接遍历运行execSync的话,会阻塞住,oraloading也要卡着不动,于是选择promise去运行,通过exec回调来结束promise

const forEachSetV = (list, obj, key) => {
  const promises = [];
  const manager = hasCnpm() ? 'cnpm' : 'npm'; // 判断选择cnpm还是npm

  list.forEach((item) => {
    if (typeof item === 'object') {
      return forEachSetV(item, obj, key);
    }

    const newPromise = new Promise((res) => {
      exec(`${manager} view ${item} version`, (err, stdout, stderr) => {
        obj[key][item] = stdout.slice(0, stdout.length - 1);
        res(0);
      });
    });

    promises.push(newPromise);
  });

  return promises;
};

const promise = [
  ...forEachSetV(depe, pkg, 'dependencies'),
  ...forEachSetV(devD, pkg, 'devDependencies'),
];
await Promise.all(promise);

那么就获取到版本号后,再将其他数据一同填入到json中,将其作为package.json的值,在新项目目录下,新建它。

const fs = require('fs-extra'); // fs-extra是系统fs模块的扩展
const path = require('path');

module.exports = (dir, files) => {
  Object.keys(files).forEach((name) => {
    const pathName = path.join(dir, name);
    fs.ensureDirSync(path.dirname(pathName)); // 如果没有文件夹则新建文件夹
    fs.writeFileSync(pathName, files[name]); // 新建文件
  });
};

writeFileTree(targetDir, {
  'package.json': JSON.stringify(pkg, null, 2),
});

选择包管理工具

因为npm的速度不甚理想,可以将其作为兜底处理。先判断当前环境中,是否有yarncnpm等等,然后优先选择前者,若都没有,则再使用npm进行操作。

const PM_CONFIG = {
  npm: {
    install: ['install', '--loglevel', 'error'], // 打印error信息
    remove: ['uninstall', '--loglevel', 'error'],
  },
  yarn: {
    install: [],
    remove: ['remove'],
  },
};
PM_CONFIG.cnpm = PM_CONFIG.npm;

module.exports = class PackageManager {
  constructor({ pkgName }) {
    this.pkgName = pkgName;

    if (hasYarn()) {
      this.bin = 'yarn';
    } else if (hasCnpm()) {
      this.bin = 'cnpm';
    } else {
      this.bin = 'npm';
    }
  }

  // 封装了下运行命令函数
  runCommand(command, args = []) {
    const _commands = [this.bin, ...PM_CONFIG[this.bin][command], ...args];
    execSync(_commands.join(' '), { stdio: [0, 1, 2] });
  }

  install() {
    try {
      this.runCommand('install', ['--offline']); // offline指先去拉取缓存区里的,如果没有则去服务器拉
    } catch (e) {
      this.runCommand('install'); // 报错兜底
    }
  }

  git() {
    try {
      execSync('git init');
      return true;
    } catch (e) {
      return false;
    }
  }
};

而判断yarncnpm环境中是否存在,可以通过判断version等等方法,去看是否能够成功执行,若成功执行,则说明环境中存在,反之则否。

const judgeEnv = (name) => {
  const envKey = `_has${name[0].toUpperCase()}${name.slice(1)}`; // 保存下结果

  if (_env[envKey] !== null) {
    return _env[envKey];
  }

  try {
    execSync(`${name} --version`, { stdio: 'ignore' }); // 不打印信息

    return (_env[envKey] = true);
  } catch (e) {
    return (_env[envKey] = false);
  }
};

const hasYarn = judgeEnv.bind(this, 'yarn');

const hasCnpm = judgeEnv.bind(this, 'cnpm');

然后通过install方法去安装依赖,再将一些参数传递给@chuhc/template去把一些基本模板复制过去。

scripts

因为是多页面项目,scripts里主要做了以下几件事情:

  • 通过glob去匹配入口,然后将其作为entry动态传入,并动态传入多个html-webpack-pluginplugins
  • 通过读取项目根目录下的chuhc.config.js文件,来动态修改webpack配置内容并调用对应的插件。
  • 最后生成最终的webpack配置文件,传入给webpack去进行编译运行打包等等操作。

匹配入口

匹配入口主要使用glob去匹配,只有满足匹配要求,才作为入口。然后通过匹配到的信息,去生成对应的entry内容,和plugin内容,传递给webpack配置文件。

const SRC = './src/**/index.?(js|jsx|ts|tsx)';
/**
 * get webpack entry
 */
const getEntries = () => {
  if (entries) return entries;
  entries = {};

  const pages = glob.sync(SRC);
  pages.forEach((page) => {
    // 遍历传entry
    const dirname = path.dirname(page);
    const entry = path.basename(dirname);
    entries[entry] = page;
  });
  return entries;
};

/**
 * get pages info
 * @param {Boolean} isProd
 */
const getPages = (isProd) => {
  const plugins = [];
  let entries = getEntries();

  Object.keys(entries).map((dirname) => {
    // 遍历传plugin
    plugins.push(
      new HtmlWebpackPlugin({
        chunks: isProd ? ['libs', dirname] : [dirname],
        filename: `./${dirname}/index.html`,
        template: path.join(__dirname, './template/index.html'),
      })
    );
  });

  return plugins;
};

链式配置 config

链式配置推荐使用webpack-chain@vue/cli也是使用它。因为我们原本就有一些基本配置内容,可以通过config.merge将我们已有的配置对象合并到配置实例中。

但是不支持直接转化,需要我们对某些配置内容,进行手动去转化,例如:module。而plugins不支持已经newplugin,我这边的处理是跳过对plugin的合并,最后再使用webpack-mergeconfig.toConfig()plugins再合并成最终的配置对象。

const Config = require('webpack-chain');
const chuhcConfig = require(`${process.cwd()}/chuhc.config.js`); // 读取根目录的配置文件
const { setBaseConfig } = require('./util/merge'); // 将已有的配置文件对象合并到配置实例
const BASE = require('./config/webpack.base'); // 配置对象base
const DEVE = merge(BASE, require('./config/webpack.deve')); // 配置对象 deve
const PROD = merge(BASE, require('./config/webpack.prod')); // 配置对象 prod

const config = new Config();

// 我这边就只是对plugin做一下处理,可以做其他很多事情,这里只是举个例子
const handleChuhcConfig = ({ plugins } = {}) => {
  // to do sth.
  if (plugins) {
    plugins.forEach((plugin) => {
      require(plugin[0])(config, plugin[1]);
    });
  }
};

const getConfig = (isDeve) => {
  config.clear(); // 清除配置

  setBaseConfig(isDeve ? DEVE : PROD, config);
  handleChuhcConfig(chuhcConfig);

  return merge(config.toConfig(), {
    plugins: isDeve ? DEVE.plugins : PROD.plugins,
  }); // 最后再合并
};

编译运行

在获取到webpack config后,那么可以根据是dev命令还是build命令,去调用对应的函数,进行编译运行打包等等操作了。(同理,根据program.command

// dev 运行
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const { getDeveConfig } = require('./config');

module.exports = () => {
  const DEVE_CONFIG = getDeveConfig();
  const { devServer } = DEVE_CONFIG;
  const compiler = webpack(DEVE_CONFIG);
  const server = new WebpackDevServer(compiler, { ...devServer });
  server.listen(devServer.port);
};
// build
const webpack = require('webpack');
const { getProdConfig } = require('./config');
const logSymbols = require('log-symbols');

module.exports = () => {
  const PROD_CONFIG = getProdConfig();
  const compiler = webpack(PROD_CONFIG);

  compiler.run((err, stats) => {
    if (err) {
      // 回调中接收错误信息。
      console.error(err);
    } else {
      console.log(logSymbols.success, '打包成功!');
    }
  });
};

template

template主要就是通过传入的参数,来判断是否要copy对应的文件,同时根据options来去修改对应文件内容和后缀。代码过于无趣,就不贴了。

plugin-xx

plugin的话,可以做的事情比较多,我这边目前就只是来链式修改webpack配置信息。这只是其中一种功能,还有很多例如:自己写一个webpack plugin / loader去传入,去做一些其他事情。

// example
module.exports = (config) => {
  ['.tsx', '.ts'].forEach((item) => config.resolve.extensions.add(item));

  config.module
    .rule('js')
    .test(/\.(js|ts|tsx|jsx)$/)
    .use('babel-loader')
    .tap((options) => {
      options.presets.push('@babel/preset-typescript');
      return options;
    });
};