remote-git-tags callback promisify 化的 Node.js 源码实现参考好文:
Get tags from a remote Git repo
从远程仓库获取所有标签。
使用
import remoteGitTags from 'remote-git-tags';
console.log(await remoteGitTags('https://github.com/lxchuan12/blog.git'));
//=> Map {'3.0.5' => 'c39343e7e81d898150191d744efbdfe6df395119', …}
原理:通过执行 git ls-remote --tags repoUrl (仓库路径)获取 tags
应用场景:可以看有哪些包依赖的这个包。 npm 包描述信息
其中一个比较熟悉的是 npm-check-updates
npm-check-updates将您的package.json依赖项升级到最新版本,忽略指定的版本。
还有场景可能是 github 中获取所有 tags 信息,切换 tags 或者选定 tags 发布版本等,比如微信小程序版本。
看源码前先看 package.json 文件。
{
  "name": "remote-git-tags",
  "version": "4.0.0",
  "description": "Get tags from a remote Git repo",
  // 指定 Node 以什么模块加载,缺省时默认是 commonjs
  "type": "module",
  "exports": "./index.js",
  // 指定 nodejs 的版本
  "engines": {
    "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
  },
  // xo 是个 ESLint 包装器
  "scripts": {
    "test": "xo && ava"
  },
  "files": [
    "index.js"
  ]
}
众所周知,Node 之前一直是 CommonJS 模块机制。 Node 13 添加了对标准 ES6 模块的支持。
告诉 Node 它要加载的是什么模块的最简单的方式,就是将信息编码到不同的扩展名中。
关于 Node 模块加载方式,在《JavaScript权威指南第7版》16.1.4 Node 模块 小节,有更加详细的讲述。此书第16章都是讲述Node,感兴趣的读者可以进行查阅。
还有个配置 .npmrc
# 该配置会导致 npm i 不生成 package-lock.json 文件
package-lock=false
代码比较少,直接贴源码
import { promisify } from 'node:util';
import childProcess from 'node:child_process';
const execFile = promisify(childProcess.execFile);
export default async function remoteGitTags(repoUrl) {
  const {stdout} = await execFile('git', ['ls-remote', '--tags', repoUrl]);
  const tags = new Map();
  for (const line of stdout.trim().split('\n')) {
    const [hash, tagReference] = line.split('\t');
    // Strip off the indicator of dereferenced tags so we can override the
    // previous entry which points at the tag hash and not the commit hash
    // `refs/tags/v9.6.0^{}` → `v9.6.0`
    const tagName = tagReference.replace(/^refs\/tags\//, '').replace(/\^{}$/, '');
    tags.set(tagName, hash);
  }
  return tags;
}
child_process.execFile() 函数与 child_process.exec() 类似,不同之处在于它默认不衍生 shell。 而是,指定的可执行文件 file 直接作为新进程衍生,使其比 child_process.exec() 略有效率。由于未衍生 shell,因此不支持 I/O 重定向和文件通配等行为。
一句话简述 remote-git-tags 原理:使用Node.js的子进程 child_process 模块的 execFile 方法执行 git ls-remote --tags repoUrl 获取所有 tags 及对应的 hash 值, 存放在 Map 对象中。
git ls-remote --tags支持远程仓库链接。
# 示例
git ls-remote --tags https://github.com/vuejs/vue-next.git
把所有 tags 及对应的 hash 值, 存放在 Map 对象中。
node:utilnode:util Node 模块文档说明
Core modules can also be identified using the node: prefix, in which case it bypasses the require cache. For instance, require(‘node:http’) will always return the built in HTTP module, even if there is require.cache entry by that name.
也就是说引用 node 原生库可以加 node: 前缀,比如 import util from 'node:util', 这样会绕过 require.cache
// 学习源码,需要边调试边学习
// https://github.com/nodejs/node/blob/ccb8aae3932c13f33622203b2ffc5a33120e9d40/lib/internal/util.js#L324
const kCustomPromisifiedSymbol = SymbolFor('nodejs.util.promisify.custom');
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs');
let validateFunction;
function promisify(original) {
  // Lazy-load to avoid a circular dependency.
  if (validateFunction === undefined)
    ({ validateFunction } = require('internal/validators'));
  validateFunction(original, 'original');
  if (original[kCustomPromisifiedSymbol]) {
    const fn = original[kCustomPromisifiedSymbol];
    validateFunction(fn, 'util.promisify.custom');
    return ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
      value: fn, enumerable: false, writable: false, configurable: true
    });
  }
  // Names to create an object from in case the callback receives multiple
  // arguments, e.g. ['bytesRead', 'buffer'] for fs.read.
  const argumentNames = original[kCustomPromisifyArgsSymbol];
  function fn(...args) {
    return new Promise((resolve, reject) => {
      ArrayPrototypePush(args, (err, ...values) => {
        if (err) {
          return reject(err);
        }
        if (argumentNames !== undefined && values.length > 1) {
          const obj = {};
          for (let i = 0; i < argumentNames.length; i++)
            obj[argumentNames[i]] = values[i];
          resolve(obj);
        } else {
          resolve(values[0]);
        }
      });
      ReflectApply(original, this, args);
    });
  }
  ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original));
  ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
    value: fn, enumerable: false, writable: false, configurable: true
  });
  return ObjectDefineProperties(
    fn,
    ObjectGetOwnPropertyDescriptors(original)
  );
}
promisify.custom = kCustomPromisifiedSymbol;
node:utilgit ls-remote --tagsutil.promisify
    Mapfor...ofSymbolSymbolForReflectApplyObjectSetPrototypeOfObjectDefinePropertyObjectGetOwnPropertyDescriptors这些知识可以查看esma规范,或者阮一峰老师的《ES6 入门教程》 等书籍。
还有经典的 MDN 文档,我在这里学习 learn-javascript
promisify 也可以看 es6-promisify 这个库学习