validate-npm-package-name 监测 npm 包名是否符合标准

473 阅读3分钟

1. 前言

有幸在若川大佬的公众号《若川视野》中看到源码共读的活动,于是果断加入了这个活动。希望通过参加这个活动,一方面让自己坚持去看源码,另一方面让自己养成写文章的习惯。本篇文章是参加第七期活动后写的。

2. 整体分析

2.1 功能

用户输入一个字符串,然后这个库就会判断该名称是否为一个有效的 npm 包名

2.2 输入和输出示例

如下所示,当用户引用该库,然后输入有效的包名,最后会得出该包名是否符合标准的结果

var validate = require("validate-npm-package-name")
validate("some-package")
{
  validForNewPackages: true,
  validForOldPackages: true
}

2.3 运行官方测试用例

按照官方步骤,下载源码后,安装依赖,运行测试命令,即可看到测试结果。

npm install
npm test

通过测试结果可以看到,官方的test/index.js中含有22条测试用例且全部验证通过

3. 源码分析

3.1 整体结构

主要是有 validate 和 done 这两个函数。前一个函数是用来判断包名的主函数,还附带了相关的正则表达式 scopedPackagePattern,而 done 函数是辅助函数,可以通过传入的 warnings 和 errors 来决定最后返回的 result

var validate = module.exports = function (name) {
  ...
  return done(warnings, errors)
}
validate.scopedPackagePattern = scopedPackagePattern

var done = function (warnings, errors) {
    ...
}

3.2 包名的长度需要大于0

包名不应该是 null 或者是 undefined,必须是一个长度不为0的字符串,

if (name === null) {
  errors.push("name cannot be null");
  return done(warnings, errors);
}

if (name === undefined) {
  errors.push("name cannot be undefined");
  return done(warnings, errors);
}

if (typeof name !== "string") {
  errors.push("name must be a string");
  return done(warnings, errors);
}

if (!name.length) {
  errors.push("name length must be greater than zero");
}

3.3 包名不应该是.或者_开始

if (name.match(/^\./)) {
  errors.push("name cannot start with a period");
}

if (name.match(/^_/)) {
  errors.push("name cannot start with an underscore");
}

3.4 包名前后不能含有空格

if (name.trim() !== name) {
  errors.push("name cannot contain leading or trailing spaces");
}

3.5 包名不能和 node.js 的核心模块名称相同,也不能是黑名单里面的名称

blacklist 包含两个名称,而引入的 builtins 依赖包则是包含所有 node.js 内嵌模块的清单

var builtins = require('builtins')
var blacklist = [
  'node_modules',
  'favicon.ico'
]

blacklist.forEach(function (blacklistedName) {
  if (name.toLowerCase() === blacklistedName) {
    errors.push(blacklistedName + " is a blacklisted name");
  }
});

builtins.forEach(function (builtin) {
  if (name.toLowerCase() === builtin) {
    warnings.push(builtin + " is a core module name");
  }
});

3.6 包名的长度不能超过214

if (name.length > 214) {
  warnings.push("name can no longer contain more than 214 characters");
}

3.7 包名必须是小写字母

if (name.toLowerCase() !== name) {
  warnings.push("name can no longer contain capital letters");
}

3.8 包名不能含有字符~)('!*,不能包含任何非 URL 安全的字符

这个两个校验放在了一起进行分析,前一个校验说明在官方文档上的介绍是不严谨的,像@!~npm/thing这种特殊的包名包含了是可以通过校验的,因为它符合最后一个/后面不含~)('!*的规则,而且符合不包含非 URL 安全字符这个规则。 正则表达式 scopedPackagePattern 可以参考网站 进行分析,不过觉得自己还是没有能够完整理解,所以准备看书《正则表达式必知必会》

image.png

if (/[~'!()*]/.test(name.split("/").slice(-1)[0])) {
  warnings.push('name can no longer contain special characters ("~\'!()*")');
}

var scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$')

if (encodeURIComponent(name) !== name) {
  // Maybe it's a scoped package name, like @user/package
  var nameMatch = name.match(scopedPackagePattern);
  if (nameMatch) {
    var user = nameMatch[1];
    var pkg = nameMatch[2];
    if (encodeURIComponent(user) === user && encodeURIComponent(pkg) === pkg) {
      return done(warnings, errors);
    }
  }

  errors.push("name can only contain URL-friendly characters");
}

4. 总结

  1. 正则表达式在做校验的时候方便且常用,所以学好这个技巧很重要
  2. 单单一个包名校验就有9条校验规则,所以写代码和文档的时候需要严谨
  3. 分析源代码的时候需要提高效率,现在看源码加上写文章超过了3个小时