validate-npm-package-name 源码阅读

1,930 阅读3分钟

1. 前言

首先, 很感谢「 若川 」 大佬 提供的 一个「 源码共读 」 平台, 其实我很早就进入了,但是因为做为一个奶爸, 下班后马不停蹄地回家带娃, 所以很少有时间参与 源码共读 的活动来。

今天正好抽出时间,参与一下大佬的新一期的活动, 这一期项目是 validate-npm-package-name, 它是检验一个字符串是否是一个 有效的 包命名

2. 学习目标

  • 了解 validate-npm-package-name 的作用和使用场景

3. 工具介绍

Give me a string and I'll tell you if it's a valid npm package name.
This package exports a single synchronous function that takes a string as input and returns an object with two properties:
validForNewPackages :: Boolean
validForOldPackages :: Boolean

接受一个字符串参数, 检验该字符串是否是一个有效的包命名,
该工具 提供一个 接受字符串的函数, 并且返回 一个拥有2个属性的对象
validForNewPackages :: Boolean
validForOldPackages :: Boolean

4. 包命名规则

  1. 包名不能是空字符串;
  2. 所有的字符串必须小写;
  3. 可以包含 连字符 - ;
  4. 包名不得包含任何非 url 安全字符;
  5. 包名不得以 . 或者 _ 开头;
  6. 包名首尾不得包含空格;
  7. 包名不得包含 ~)('!* 任意一个字符串;
  8. 包名不得与node.js/io.js 的核心模块 或者 保留名 以及 黑名单相同;
  9. 包名的长度不得超过 214;

5. 示例

5.1 有效的包名

var validate = require("validate-npm-package-name")
validate("some-package")
validate("example.com")
validate("under_score")
validate("123numeric")
validate("@npm/thingy")
validate("@jane/foo.js")

5.2 无效的包名

// 包含 不符合 第6条 规则
validate("excited!") 
validate(" leading-space:and:weirdchars")

6. 源码阅读

6.1 代码结构

var scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$') 
var builtins = require('builtins')
var blacklist = ['node_modules','favicon.ico']
var validate = module.exports = function(name) {
    // .... 检验 参数 name 是否 规范
}
validate.scopedPackagePattern = scopedPackagePattern
var done = function (warning, erros) {
    // ....  返回 处理结果
}

6.1.1 scopedPackagePattern 正则表达式

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

正则表达式可视化, 链接地址:jex.im/regulex/

下载.png 匹配以下字符串

  • @user 以@开头
  • @user/test
  • 非‘/’的字符串

image.png

builtins:列出了 node 所有的内置模块

6.1.2 validate函数, 结合 [包命名规则]

(1)检测传入的参数是否是字符串;
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)
}
(2)包名不能是空字符串
if (!name.length) {
    errors.push('name length must be greater than zero')
}
(3)包名不得以 . 或者 _ 开头
if (name.match(/^\./)) {
    errors.push('name cannot start with a period')
}
if (name.match(/^_/)) {
    errors.push('name cannot start with an underscore')
}
(4) 包名首尾不得包含空格
// trim 方法 可以去掉 字符串 两边的 空白
if (name.trim() !== name) {
    errors.push('name cannot contain leading or trailing spaces')
}
(5) 包名不得与node.js/io.js 的核心模块 或者 保留名 以及 黑名单相同
// No funny business
blacklist.forEach(function (blacklistedName) {
    if (name.toLowerCase() === blacklistedName) {
          errors.push(blacklistedName + ' is a blacklisted name')
    }
})

// core module names like http, events, util, etc
builtins.forEach(function (builtin) {
    if (name.toLowerCase() === builtin) {
      warnings.push(builtin + ' is a core module name')
    }
})
(6)包名的长度不得超过 214
// really-long-package-names-------------------------------such--length-----many---wow
// the thisisareallyreallylongpackagenameitshouldpublishdowenowhavealimittothelengthofpackagenames-poch.
if (name.length > 214) {
    warnings.push('name can no longer contain more than 214 characters')
}
(7)所有的字符串必须小写
// mIxeD CaSe nAMEs
if (name.toLowerCase() !== name) {
    warnings.push('name can no longer contain capital letters')
}
(8)包名不得包含 ~)('!* 任意一个字符串
if (/[~'!()*]/.test(name.split('/').slice(-1)[0])) {
    warnings.push('name can no longer contain special characters ("~\'!()*")')
}
(9)包名不得包含任何非 url 安全字符
const regx = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$') 
const name = '@user/package'
//  ?: 会忽略分组 
// ['@user/package', 'user', 'package', index: 0, input: '@user/package', groups: undefined]
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')
}

6.2 done 函数

var done = function (warnings, errors) {
  var result = {
    validForNewPackages: errors.length === 0 && warnings.length === 0,
    validForOldPackages: errors.length === 0,
    warnings: warnings,
    errors: errors
  }
  if (!result.warnings.length) delete result.warnings
  if (!result.errors.length) delete result.errors
  return result
}

7. 总结

整个项目并不是太难, 都是一些基本的字符串判断,以及正则匹配,但是值得每个人去学习, 在编写一个工具的时候, 保证代码的逻辑清晰, 代码的书写规范。