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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@stellastella/xj-cli

v1.1.0

Published

Command Line Interface for personal instant project creation

Downloads

2

Readme

实现简易的 CLI

CLI 全称 command-line interface 命令行界面,通常不支持鼠标,用户通过键盘输入指令,计算机收到指令,予以执行。

本文结合 vue-clicreate-react-app 即脚手架,定制个人项目脚手架,快速创建初始化项目文件。

定制脚手架初衷:

  1. 快速搭建项目初始化文件;
  2. 统一代码规范。

Tip: 文章旨在记录个人开发 cli 的经验。后续文章提及的 xj-cli 为个人开发脚手架示例,可在 npm 上 download 之后,就能快速生成项目。

xj-cli

开发之前,需创建一个 Github 组织 organization 账户(如果你有,即可跳过)。先创建一个普通 Github 账户,并升级为 organization 组织账户,后续会将个人初始化后的项目放入该组织账户下,供脚手架逻辑使用。

下载依赖包

  • axios: http 库
  • commander: 命令行参数解析
  • consolidate: 统一模板引擎
  • download-git-repo: 下载并提取出 Git 仓库代码
  • ejs: 模板引擎
  • inquirer: 交互式命令行,实现命令行选择功能
  • metalsmith: 极简、插件化的静态站点生成器

Metalsmith (译自官网) 为什么 Metalsmith 是一个插件化的静态站点生成器?

  • 从事源码目录中读取源文件,抽取信息;
  • 可操作抽取的信息;
  • 将操作后的信息写入文件,最后移至目标目录。

初始化文件

目录

├── bin
│   └── www  
├── package.json
├── src
│   ├── main.js 
│   ├── create.js   
│   ├── config.js   
│   └── constants.js  

配置项

package.json

package.json 是整个项目的配置文件,通过命令行 npm init -y 快速生成。

"bin": {
  "xj-cli": "./bin/www"
},
""

Tip: bin 属性是为包配置执行环境与入口文件,在 shell 下执行 xj-cli 时就能调用 bin 目录下的 www 文件

bin/www

#! /usr/bin/env node
require('../src/main.js');

Tip: 顶部需添加 #! /usr/bin/env node,标识命令行输入 xj-cli 时,以 node 环境执行此文件

链接全局包

该步骤是将当前 xj-cli 临时配置到执行环境变量中,实现在任意目录 shell 下都能执行 xj-cli 命令。

npm link

核心代码

功能

  • xj-cli --version 查看版本号
  • xj-cli --help 查看帮助
  • xj-cli config xxx 设置配置项
  • xj-cli create xxx 创建项目

constants.js

const { version } = require('../package.json');

const downloadDirectory = `${process.env[process.platform === 'darwin' ? 'HOME' : 'USERPROFILE']}/.template`

module.exports = {
  version,
  downloadDirectory,
};

导出版本号,下载创建项目缓存目录

const program = require('commander');
const path = require('path');
const { version } = require('./constants');

const mapActions = {
  create: {
    alias: 'cre',
    description: 'create a project',
    examples: [
      'xj-cli create <project-name>',
    ],
  },
  config: {
    alias: 'conf',
    description: 'config project variable',
    examples: [
      'xj-cli config set <k> <v>',
      'xj-cli config get <k>',
    ],
  },
  '*': {
    alias: '',
    description: 'command not found',
    examples: [],
  },
};

Reflect
  .ownKeys(mapActions)
  .forEach(
    (action) => {
      program
        .command(action)
        .alias(mapActions[action].alias)
        .description(mapActions[action].description)
        .action(() => {
          if (action === '*') {
            console.log(mapActions[action].description);
          } else {
            require(path.resolve(__dirname, action))(...process.argv.slice(3));
          }
        });
    },
  );

program.on('--help', () => {
  console.log('\nExamples:');
  Reflect.ownKeys(mapActions).forEach((action) => {
    mapActions[action].examples.forEach((example) => console.log(`  ${example}`));
  });
});

program
  .version(version)
  .parse(process.argv);

create.js 引入依赖文件

const axios = require('axios');
const ora = require('ora');
const Inquirer = require('inquirer');
let downloadGitRepo = require('download-git-repo');
const fs = require('fs');
const { promisify } = require('util');
const path = require('path');
const MetalSmith = require('metalsmith');
let { render } = require('consolidate').ejs;
const ncp = require('ncp');
const { downloadDirectory } = require('./constants');

包装原函数为返回 promise 对象的新函数,避免回调函数嵌套

render = promisify(render);
downloadGitRepo = promisify(downloadGitRepo);

异步获取仓库项目列表,tag 列表

const fetchRepoList = async () => {
  const { data } = await axios.get('https://api.github.com/orgs/xj-cli/repos');
  return data;
};

const fetchTagList = async (repo) => {
  const { data } = await axios.get(`https://api.github.com/repos/xj-cli/${repo}/tags`);
  return data;
};

基于函数柯里化设计模式,封装异步获取前后的 loading 图标显示

const waitFnLoading = (fn, message) => async (...args) => {
  const spinner = ora(message);
  spinner.start();
  const result = await fn(...args);
  spinner.succeed();
  return result;
};

const download = async (repo, tag) => {
  let api = `xj-cli/${repo}`;
  if (tag) {
    api += `#${tag}`;
  }
  const dest = `${downloadDirectory}/${repo}`;
  await downloadGitRepo(api, dest);
  return dest;
};

function checkProjectName(projectName) {
  if (!projectName) {
    console.error('请输入要创建的项目名称');
    return false;
  }
  return true;
}

导出一个函数,接收项目名,开始远程拉取项目,进行创建

  • 拉取项目列表名,交互式问答得出用户想要下载的项目名
  • 拉取用户选择的项目 tag 版本号
  • 下载代码缓存在本地,并在当前目录下生成项目
module.exports = async (projectName) => {
  if (!checkProjectName(projectName)) return;

  let repos = await waitFnLoading(fetchRepoList, 'fetching template...')();
  repos = repos.map((item) => item.name);
  const { repo } = await Inquirer.prompt({
    name: 'repo',
    type: 'list',
    message: '请选择要下载的模板',
    choices: repos,
  });
  let tags = await waitFnLoading(fetchTagList, 'fetching tags...')(repo);
  tags = tags.map((item) => item.name);

  const { tag } = await Inquirer.prompt({
    name: 'tag',
    type: 'list',
    message: '请选择要下载的版本',
    choices: tags,
  });

  const result = await waitFnLoading(download, 'downloading template')(repo, tag);
  if (!fs.existsSync(path.join(result, 'ask.js'))) {
    await ncp(result, path.resolve(projectName));
  } else {
    await new Promise((resolve, reject) => {
      MetalSmith(__dirname)
        .source(result)
        .destination(path.resolve(projectName))
        .use(async (files, metal, done) => {
          const args = require(path.join(result, 'ask.js'));
          const obj = await Inquirer.prompt(args);
          const meta = metal.metadata();
          Object.assign(meta, obj);
          Reflect.deleteProperty(files, 'ask.js');
          done();
        })
        .use((files, metal, done) => {
          const obj = metal.metadata();
          Reflect.ownKeys(files).forEach(async (file) => {
            if (file.includes('js') || file.includes('json')) {
              let content = files[file].contents.toString();
              if (content.includes('<%')) {
                content = await render(content, obj);
                files[file].contents = Buffer.from(content);
              }
            }
          });
          done();
        })
        .build((err) => {
          if (err) {
            reject();
          } else {
            resolve();
          }
        })
    });
  }
};

发包

nrm use npm
npm login
npm publish

Tip: 登录 npm 账号前,切换到npm 源下

nrmList

使用

全局安装 xj-cli 包

nmp i xj-cli -g

下载包

xj-cli create app

fetchingRepo

selectRepo

tag

downloading

到这里核心功能实现了,后续有其他命令可在此基础上扩展即可。

补充

如果发布 scoped packages 作用域包到 NPM 包管理器上,点击 创建scoped packages,通过以下步骤即可把当前的 user 转成 org 组织(如图):

create an org

  • 输入要新创建的 org 名,如:stellastella
  • 勾选 FREE 选项 -(可选)CONVERT
    • 勾选,将当前的个人账号 @stella2 转为 org,新创建的 stellastella 为个人账号

修改配置文件信息

{
  "name": "@stellastella/xj-cli"
}

这一步也能通过 NPM 命令行实现:

npm publish --access public

至此,scoped packages 完成发布。