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 🙏

© 2025 – Pkg Stats / Ryan Hefner

lqsimooc-cli-dev

v1.0.0

Published

- .js => module.exports/exports - .json => 内部会通过JSON.parse()返回一个对象 - .node => c++插件(一般用不到) - any => .js解析

Readme

脚手架开发全流程记录

1. require能加载资源类型

  • .js => module.exports/exports
  • .json => 内部会通过JSON.parse()返回一个对象
  • .node => c++插件(一般用不到)
  • any => .js解析

2. 封装@lqsimooc-cli-dev/log工具库

在utils/log中新建npm项目,在cli中引入这个包 pnpm i @lqsimooc-cli-dev/log -r --filter @lqsimooc-cli-dev/core-cli

然后就能使用了

const log = require("@lqsimooc-cli-dev/log");
log()

3. 使用semver进行版本号的比对

4. 使用colors来输出彩色文字

5. 使用root-check来root用户降级

root用户的操作一般都没有权限相关问题,为了规避一些文件权限问题,所以不让使用root用户 使用1.0.0版本支持require引入

6. 使用user-home获取用户家目录

7. 使用path-exists判断路径是否存在

4.0.0版本

const pathExists = require("path-exists").sync;
const userHome = require("user-home");
pathExists(userHome)

8. 使用[email protected]检查命令参数

function checkInputArgs() {
    const minimist = require("minimist");
    const args = minimist(process.argv.slice(2));
    console.log(args);
}
imooc-cli -wd
{ _: [], w: true, d: true }

imooc-cli --wd
{ _: [], wd: true }

9. 使用dotenv从.env文件中加载环境变量

该文件中的配置会自动挂载到process.env上

function checkEnv() {
    const dotenv = require("dotenv");
    const dotenvPath = path.resolve(userHome, ".env");
    if (pathExists(dotenvPath)) {
        config = dotenv.config({
            path: dotenvPath
        });
    }
    log.verbose("环境变量", config, process.env.DB_PWD);
}

10. 开发get-npm-info 工具库

用来获取最新的版本号

10-1。 获取npm包信息

https://registry.npmjs.org/@lqsimooc-cli-dev/core-cli

10-2. 使用[email protected]来做url的拼接

然后用axios访问获取数据,拿到最新版本信息,如果有最新的版本就提示更新

11. 让Node支持ES Module

  • 使用webpack
    const path = require('path');
      module.exports = {
      entry: './bin/core.js',
      output: {
          path: path.join(__dirname, '/dist'),
          filename: 'core.js',
      },
      mode: 'development',
      target: 'node',
      module: {
          rules: [
          {
              test: /\.js$/,
              exclude: /node_modules/,
              use: {
              loader: 'babel-loader',
              options: {
                  presets: [ '@babel/preset-env' ],
                  plugins: [
                  [
                      '@babel/plugin-transform-runtime',
                      {
                      corejs: 3,
                      regenerator: true,
                      useESModules: true,
                      helpers: true,
                      },
                  ],
                  ],
              },
              },
          },
          ],
      },
      };
    
  • nodejs原生 后缀名.mjs 里面全都都用es modules nodejs 14以前需要 node --experimental-modules xxx.mjs

12.使用[email protected]来注册命令

12.1 在commands包中创建第一个命令 init

12.2 动态加载init命令

不同团队的init命令可能不同,为了增加灵活性,需要考虑动态加载init命令

12.2.1 全局指定本地调试文件路径

  • 定义全局option:program.option("-tp, --targetPath <targetPath>", "指定本地调试文件路径");
  • 监听 targetPath 把值放到环境变量
    program.on("option:targetPath", function () {
          process.env.CLI_TARGET_PATH = program.targetPath;
    });
  • 外部init命令库读取环境变量
    function init(projectName, cmdObj) {
      console.log("init123123", projectName, cmdObj.force, process.env.CLI_TARGET_PATH);
      }

12.2.2 开发exec包用来执行js文件

    // 1.targetPath => modulePath
    // 2.modulePath => Package(npm模块)
    // 3.package.getRootFile(获取入口文件)
    // 4.package.update()  package.install()

12.2.3 在models中开发Package类,来统一管理操作package

12.2.3.1 使用pkg-dir来获取package的根路径

 getRootFilePath() {
        // 1. 获取package.json所在目录 - pkg-dir
        const dir = pkgDir(this.targetPath);
        if (dir) {
            // 2. 读取package.json
            const pkgFile = require(path.resolve(dir, "package.json"));
            // 3. main/lib
            if (pkgFile && pkgFile.main) {
                return path.resolve(dir, pkgFile.main);
            }
            // 4. 路径兼容
        }
        return null;
    }

12.2.3.2 在utils中开发utils包

 isObject(o) {
        return Object.prototype.toString.call(o) === "[object Object]";
    }

12.2.3.3 在utils中开发format-path包

用来兼容各平台路径,

function formatPath(p) {
    if (p && typeof p === "string") {
        const sep = path.sep;
        if (sep === "/") return p;
        else { //windows
            return p.replace("/\\/g", "/");
        }
    }
    return p;
}

12.2.3.4 使用npminstall 来安装package

12.2.3.5 使用fs-extra 来进行文件相关操作

12.2.4 动态加载init 总结

  • 之前init的命令直接执行init方法,现在通过执行一个中间层 exec方法
  • exec中通过一个对象映射来找到对应方法的对应packageName
      const SETTINGS = {
          init: "@lqsimooc-cli-dev/init"
      };
  • 如果传入了targetPath imooc-cli init -tp /Users/ashui/Desktop/架构师/01.脚手架/lqsimooc-cli-dev/commands/init/lib/index.js 那么获取rootFilePath就会执行对应的文件
  • 如果没有传入就会走默认缓存路径
    • 相关路径:targetPath:path.resolve(homePath, CACHE_DIR) storeDir: path.resolve(targetPath, "node_modules");
    • 对应逻辑:
      • 如果包存在就更新包
      • 如果不存在就安装包
    • 执行rootFilePath 中的文件

13 node多进程

将之前的rootFilePath的执行(require执行)

 if (rootFilePath) require(rootFilePath).apply(null, arguments);

改为:子进程调用

13.1 child_process 用法

具体查看 testDemo/process/child1.js

  • 异步:
    • exec
    • execFile
    • fork
    • spawn
  • 同步:
    • execSync
    • execFileSync
    • spawnSync
  • 区别:
    • exec & execFile: exec主要用来执行shell命令,execFile用来执行shell文件, 通过回调机制返回信息(适合开销较小的任务,一次性把结果返回回调中)
    • spawn 没有回调,返回一个子进程,默认通过事件监听处理信息 适合耗时任务(比如:npm install)等 需要不断日志的任务 可以通过options中 stdio: "inherit" //实时在主进程打印
    • fork 使用node(开启一个子进程)去执行一个文件,返回一个子进程对象 可以和子进程双向通信
      const child1 = cp.fork(path.resolve(__dirname, "fork.js"));
      //异步
      //child1.send("hello fork");
      child1.send("hello fork child process", () => {
          //向子进程发送消息
          console.log("向子进程发送消息");
      });
    
      child1.on("message", (msg) => {
          console.log(msg);
          //断开和子进程的连接
          child1.disconnect();
      });

13.1.1 给文件添加可执行权限

chmod +x ./test.shell

13.1.2 抹平操作系统通过cp.spawn执行node的差异

// other: cp.spawn("node", ["-e", code],{})
// windows:  cp.spawn("cmd",["/c",'node','-e',code],{})
function spawn(command, args, options = {}) {
    const isWin32 = process.platform === "win32";
    const cmd = isWin32 ? "cmd" : command;
    const cmdArgs = isWin32 ? ["/c"].concat(command, args) : args;
    return cp.spawn(cmd, cmdArgs, options);
}

13.2 在models中新建command命令类

  • 检查node版本 checkNodeVersion
  • 格式化入参 initArgs
  • 让子类实现 init
  • 让子类实现 exec

week5 脚手架创建项目流程设计和开发

week5-1 主要内容

  • 脚手架项目创建功能的架构设计
  • 通过命令行交互获取项目基本信息
  • eggjs+云MongoDB的集成
  • 开发前端项目模板
  • eggjs获取项目模板Api开发
  • 项目模板下载功能开发
  • 加餐:自己实现一个命令行交互(放弃)

week5-2 prepare阶段

主要就是通过命令行交互收集项目信息,生成项目配置,为下一步模板的下载和安装做铺垫

prepare阶段

week5-2-1 判断当前目录是否为空

node判断目录是否为空

  isCwdEmpty() {
        const currentpath = process.cwd();
        // console.log(currentpath);
        // console.log(path.resolve("."));
        let files = fs.readdirSync(currentpath);
        if (!files) return true;
        //过滤出 不以.开头 和 不是node_modules的文件或目录
        files = files.filter((file) => !file.startsWith(".") && ["node_modules"].indexOf(file) < 0);
        return files.length === 0;
    }

week5-2-2 使用inquirer库做命令行交互

import inquirer from "inquirer";

inquirer
    .prompt([
        {
            type: "input", //input, number, confirm, list, rawlist, expand, checkbox, password, editor
            name: "yourName",
            message: "请输入你的名字",
            prefix: "ashuiweb:",
            suffix: "哎哟不错",
            //输入时的展现形式,不会改变结果
            transformer: (name) => `我的名字叫:${name}。`,
            //改变输入结果
            filter: (name) => (name += "!"),
            //validate: (v) => !!v
            //提示信息
            validate: function (v) {
                const done = this.async();
                if (!/^\d{1,2}\.\d{1,2}\.\d{1,2}$/.test(v)) {
                    done("参考格式:1.0.0");
                    return;
                }
                done(null, true);
            }
        },
        //when 根据用户之前输入来提问
        {
            type: "confirm",
            message: "你真的叫ashui吗",
            name: "confirm",
            when: (answers) => answers.yourName === "ashui"
        },
        {
            type: "list",
            name: "language",
            message: "your language",
            choices: [
                { value: 1, name: "php" },
                { value: 2, name: "node" },
                { value: 3, name: "js" }
            ]
        },
        //和list就是交互形式不同,基本差不多
        {
            type: "rawlist",
            name: "hobby",
            message: "your hobby",
            choices: [
                { value: 1, name: "php" },
                { value: 2, name: "node" },
                { value: 3, name: "js" }
            ]
        },
        //提供简写的形式 需要用户手动输入key key不能省
        {
            type: "expand",
            name: "color",
            message: "your color",
            default: "red",
            choices: [
                { key: "R", name: "红", value: "red" },
                { key: "G", name: "绿", value: "green" },
                { key: "B", name: "蓝", value: "blue" }
            ]
        },
        {
            type: "checkbox",
            name: "ball",
            message: "your ball",
            default: 0,
            choices: [
                { name: "足球", value: 1 },
                { name: "篮球", value: 2 },
                { name: "铅球", value: 3 }
            ]
        },
        {
            type: "editor",
            name: "message",
            message: "your message"
        }
    ])
    .then((answers) => {
        console.log(answers);
        // Use user feedback for... whatever!!
    })
    .catch((error) => {
        if (error.isTtyError) {
            // Prompt couldn't be rendered in the current environment
        } else {
            // Something else went wrong
        }
    });

week5-3 eggjs项目创建

流程:

  • 云mongoDb中插入数据
  • eggjs中调用mongo库读取数据
  • 控制器中返回数据

week5-3-1 安装 egg

npm init egg --type=simple

week5-3-2 执行npm init packageName

会下载执行 create-packageName

week5-3-3 通过egg.js创建api

week5-3-4 使用@pick-star/cli-mongodb 来快速使用momgodb

week5-3-5 在app目录下新建utils/mongo.js

week5-4 utils中新建request包用来发请求

week5-5 获取项目模板并添加选择项目模板流程

  • 获取项目模板
const request = require("@lqsimooc-cli-dev/request");

module.exports = () =>
    request({
        url: "/project/template"
    });
  • 在inquire.propmt中插入
{
    type: 'list',
    name: 'projectTemplateInfo',
    message: '请选择项目模板',
    choices: this.template.map((it) => ({ value: it.name, name: it.title })),
    //通过inquire中options的filter会修改最终返回结果 //最终返回
    /**
             {
                title: 'vue-element-admin模板',
                name: 'lqsimooc-cli-dev-tempate-vue-element-admin',
                version: '1.0.0'
            }
                */
    filter: (v) => this.template.find((it) => it.name === v),
    },
       

week5-6 获取模板信息后,下载或更新模板包

使用我们之前创建的package类型来安装或更新包

 async downloadTemplate() {
    //根据projectTemplateInfo下载
    const { projectTemplateInfo } = this.projectInfo;
    const { name, version } = projectTemplateInfo;
    const targetPath = path.resolve(process.env.HOME_PATH, process.env.CLI_HOME_PATH, 'template');
    const storeDir = path.resolve(targetPath, 'node_modules');
    const templatePkg = new Package({
      targetPath,
      storeDir,
      packageName: name,
      packageVersion: version,
    });
    if (!(await templatePkg.exists())) {
      log.info('模板下载中:', 'loading...');
      await templatePkg.install();
    } else {
      log.info('更新模板中:', 'loading...');
      await templatePkg.update();
    }
  }

week5-7 下载时间有点长?使用cli-spinner给命令行加个loading效果吧

week6 安装模板到项目

week6-1 ejs的用法

  • ejs.compile返回渲染器,通过给渲染器传入数据来渲染结果
    const compileHtml = ejs.compile(template, options);
    const compiledHtml = compileHtml(data);
  • ejs.render
    const renderedTHtml = ejs.render(template, data, options);
    console.log(renderedTHtml);
  • ejs.renderFile
    • promise
    const renderFile = ejs.renderFile(path.resolve(__dirname, 'temp.html'), data, options);
    renderFile.then(console.log);
    • 回调
    ejs.renderFile(path.resolve(__dirname, 'temp.html'), data, options, function (err, data) {
    if (err) {
      console.log(err);
      return;
    }
    console.log(data);
    });

week6-2 ejs的标签含义

  • <% '脚本' 标签,用于流程控制,无输出。
  • <%= 输出数据到模板(输出是转义 HTML 标签)
  • <%_ 删除其前面的空格符
  • <%- 输出非转义的数据到模板
  • <%# 注释标签,不执行、不输出内容
  • <%% 输出字符串 '<%'
  • %> 一般结束标签
  • -%> 删除紧随其后的换行符
  • _%> 将结束标签后面的空格符删除

week6-3 ejs其他功能

  • 引入其他模板
    <%# include 其他模板 _%> 
    <#- include('header.html', {user}); #>
  • 配置自定义分隔符
    const renderFile = ejs.renderFile(path.resolve(__dirname, 'temp.html'), data, {
       delimiter: '?', //自定义分隔符 默认 '#'
      }); 
     <h1>版权所有:<?= user.copyRight ?> </h1>
  • 自定义加载器 使用此功能,您可以在读取模板之前对其进行预处理。
    let myFileLoader = function (filePath) {
    return 'myFileLoader: ' + fs.readFileSync(filePath);
    };
    ejs.fileLoader = myFileLoader;

week6-4 使用glob库来匹配文件路径

const glob = require('glob');
//异步使用
(async function () {
  const jsfiles = await glob('**/*.js', { ignore: 'node_modules/**' });
  console.log(jsfiles);
})();
//同步使用
const jsfiles = glob.globSync('**/*.js', { ignore: 'node_modules/**' });
console.log(jsfiles);
//读取流
const filesStream = glob.globStream('**/*.js', { ignore: 'node_modules/**' });
const jsFilesArr = [];
filesStream.on('data', (data) => {
  //console.log(data);
  jsFilesArr.push(data);
});
filesStream.on('end', function () {
  console.log(jsFilesArr);
});

week6-5 安装模板

如果模板type是normal的就是普通安装

    spinnerStart('模板安装中');
    //拷贝模板代码至当前目录
    const sourcePath = path.resolve(tempath, 'template');
    const targetPath = process.cwd(); //当前目录
    try {
      fse.copySync(sourcePath, targetPath);
    } catch (error) {
      throw error;
    } finally {
      spinnerEnd();
      log.success('模板安装成功');
    }

week6-6 安装依赖和执行

模板数据中配置installCommand 和 startCommand,将spawn抽离出来用来执行命令

const {
      projectTemplateInfo: { installCommand, startCommand },
    } = this.projectInfo;
    //拿到命令和命令参数
    if (installCommand) {
      await this.execCommand(installCommand, '依赖安装过程失败');
    }
    if (startCommand) {
      await this.execCommand(startCommand, '启动命令失败');
    }

execAsync 抽离的spawn方法

async execCommand(command, errMsg) {
    const [cmd, ...args] = command.split(' ');
    if (!WHITE_CMDS.includes(cmd)) throw new Error('远程模板存在非法命令');
    //执行命令
    const res = await execAsync(cmd, args, {
      stdio: 'inherit',
      cwd: process.cwd(),
    });
    if (res !== 0) throw new Error('errMsg');
  }

week6-7 使用kebab-case库来转化驼峰到classname形式

var kebabCase = require('kebab-case');

console.log(kebabCase('webkitTransformDemo'));
// "webkit-transform-demo
console.log(kebabCase.reverse('-webkit-transform'));
// "WebkitTransform"

week6-8 重点 ejs替换模板内容

  1. 先用glob获取项目文件
     async ejsRender(_options = {}, data = {}) {
     const dir = process.cwd();
     const options = { ignore: ['node_modules/**'], cwd: dir, nodir: true, ..._options };
     const files = await require('glob')('**', options);

}

2. 再用ejs处理对应的文件,处理好后重新写入
```JS
async ejsRender(_options = {}, data = {}) {
 const dir = process.cwd();
 const options = { ignore: ['node_modules/**'], cwd: dir, nodir: true, ..._options };
 const files = await require('glob')('**', options);
  await Promise.all(
   files.map((file) => {
     const filePath = path.resolve(dir, file);
     //后缀名白名单
     if (EJS_RENDER_FILE_TYPE.includes(path.extname(filePath).substring(1))) {
       return ejs.renderFile(filePath, data).then((result) => {
         //重新写入
         return fse.writeFile(filePath, result);
       });
     }
   })
 );
}

week6-9 处理选择模板是组件以及是自定义模板

如果创建的时候选择是组件,那么组件比项目多一个描述信息,需要用户手动去填写。

什么是自定义模板,自定义模板就是我们只管下载,至于安装逻辑交由模板中的index.js去处理

const code = `require("${rootFilePath}")(${JSON.stringify(options)})`;
log.verbose('code', code, options);
await execAsync('node', ['-e', code], { stdio: 'inherit', cwd: process.cwd() });

直接执行

require("/Users/ashui/.imooc-cli/template/node_modules/[email protected]@lqsimooc-cli-dev-tempate-custom-vue3/index.js")
({"type":"project","projectName":"asd","version":"1.0.0","projectTemplateInfo":{"title":"vue3自定义模板","name":"lqsimooc-cli-dev-tempate-custom-vue3","version":"1.0.3","type":"custom","tag":["project"],"ejsIgnore":["public/**"],"path":"/Users/ashui/.imooc-cli/template/node_modules/[email protected]@lqsimooc-cli-dev-tempate-custom-vue3"},"className":"asd","sourcePath":"/Users/ashui/.imooc-cli/template/node_modules/[email protected]@lqsimooc-cli-dev-tempate-custom-vue3/template","targetPath":"/Users/ashui/Desktop/test/imooc-cli-test"})

week6-10 当前小结

目前为止,我们从0到1开发了一个脚手架,实现的功能如下:

imooc-cli init projectName -tp /Users/ashui/Desktop/架构师/01.脚手架/lqsimooc-cli-dev/commands/init -f -c -d

  • imooc-cli 是整体命令
  • init 是 imooc-cli的一个子命令
  • -tp 和 -d 是immoc-cli的option 分别指代 本地调试路径和开启debug模式
  • -f 和 -c 是init的option 分别指代是否强制初始化项目和是否读取上次缓存的远程模板数据。
  • 如果传了tp 那么就用指定的包来执行init命令,否则就会从npm中下载@lqsimooc-cli-dev/init包进行inti命令

执行流程如下:

  • prepare阶段
    • 检查版本号
    • root降级
    • 检查用户主目录
    • 检查环境变量
    • 检查更新
  • 注册命令
    • 监听到init命令执行exec方法
  • exec方法
    • 获取执行init的是哪个包
      • 如果传了targetPath就用这个包
      • 如果没有则需要安装或更新@lqsimooc-cli-dev/init
    • 子进程去执行这个包
      const code = `require('${rootFilePath}').call(null, ${JSON.stringify(args)})`;
        //新的进程
        const child = spawn('node', ['-e', code], {
          cwd: process.cwd(),
          //将子进程的输出实时输出到父进程
          stdio: 'inherit',
        });
    • 执行init包
      • 初始化
        • 检查node版本
        • 检查参命令参数
      • 准备阶段
        • 判断项目模板是否存在(加入是否开启模板缓存判断的逻辑)
        • 判断目录是否是空(加入是否开启强制清空目录逻辑)
        • 再次提醒是否要清空目录(高危操作)
      • 收集项目信息
        • 项目类型:项目还是组件
        • 版本信息
        • 拉取对应类型的模板(前面已经在判断项目模板存在时已经做了,这里只要显示对应类型的模板即可)
        • 如果是组件还要填写项目描述
      • 下载模板
      • 安装模板
        • 普通模板
          • 将下载下来的模板拷贝到当前项目目录
          • 进行ejs模板渲染(替换掉需要ejs渲染的地方)
          • 执行install(如果有installCommand)
          • 执行start(如果有startCommand)
        • 自定义模板
          • 执行下载下来的模板的主文件(传入已经获取到项目信息等)