【若川视野 x 源码共读】第7期 | validate-npm-package-name

203 阅读2分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

源码地址:github1s.com/npm/validat…

  1. validate-npm-package-name依赖了builtins用来判断是否是node原生模块的名字。是其中的一个判断条件。
  2. builtins依赖了semver用来判断当前的node版本里面都有哪些模块。

Builtins

'use strict'

var semver = require('semver')

module.exports = function ({
  version = process.version,
  experimental = false
} = {}) {
  var coreModules = [
    'assert',
    'buffer',
    'child_process',
    'cluster',
    'console',
    'constants',
    'crypto',
    'dgram',
    'dns',
    'domain',
    'events',
    'fs',
    'http',
    'https',
    'module',
    'net',
    'os',
    'path',
    'punycode',
    'querystring',
    'readline',
    'repl',
    'stream',
    'string_decoder',
    'sys',
    'timers',
    'tls',
    'tty',
    'url',
    'util',
    'vm',
    'zlib'
  ]

  if (semver.lt(version, '6.0.0')) coreModules.push('freelist')  // 小于某版本才有,应该是被node删掉了
  // 大于等于某版本
  if (semver.gte(version, '1.0.0')) coreModules.push('v8')
  if (semver.gte(version, '1.1.0')) coreModules.push('process')
  if (semver.gte(version, '8.0.0')) coreModules.push('inspector')
  if (semver.gte(version, '8.1.0')) coreModules.push('async_hooks')
  if (semver.gte(version, '8.4.0')) coreModules.push('http2')
  if (semver.gte(version, '8.5.0')) coreModules.push('perf_hooks')
  if (semver.gte(version, '10.0.0')) coreModules.push('trace_events')

  if (
    semver.gte(version, '10.5.0') &&
    (experimental || semver.gte(version, '12.0.0'))
  ) {
    coreModules.push('worker_threads')
  }
  if (semver.gte(version, '12.16.0') && experimental) {
    coreModules.push('wasi')
  }
  
  return coreModules
}
// lt 和 gte 的含义
/*
gt(v1, v2): v1 > v2
gte(v1, v2): v1 >= v2
lt(v1, v2): v1 < v2
lte(v1, v2): v1 <= v2
*/

validate-npm-package-name

'use strict'

// 这个正则可以分为^(?:@([^/]+?)[/])?   和   ([^/]+?)$  两部分看
// ^(?:@([^/]+?)[/])? "@antd/"匹配这个部分,但是捕获的只有([^/]+?),是antd; 最后的?代表可选。没有匹配到返回undefined
// ([^/]+?)$  "icons" 没有/的结尾部分
var scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$')
var builtins = require('builtins')  // 判断是否是node原生包
// 黑名单
var blacklist = [
  'node_modules',
  'favicon.ico'
]

var validate = module.exports = function (name) {
  var warnings = []  // 存warning的数组
  var errors = []   // 存error的数组

  if (name === null) {  // 没传,肯定不行
    errors.push('name cannot be null')
    return done(warnings, errors)
  }

  if (name === undefined) {   // 传了个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')
  }

  if (name.match(/^./)) {  // 不能以./开头
    errors.push('name cannot start with a period')
  }

  if (name.match(/^_/)) {  不能以_/开头, why?
    errors.push('name cannot start with an underscore')
  }

  if (name.trim() !== name) {  // 前后去空格 和 name不符,也就是多加空格不行
    errors.push('name cannot contain leading or trailing spaces')
  }

  blacklist.forEach(function (blacklistedName) {
    // 传入的name小写之后 和 黑名单里面的名字一样了,不行
    /*
    	好像也可以写成:blacklist.includes(name.toLowerCase())
    */
    if (name.toLowerCase() === blacklistedName) {
      errors.push(blacklistedName + ' is a blacklisted name')
    }
  })

  // Generate warnings for stuff that used to be allowed

  // core module names like http, events, util, etc
  builtins.forEach(function (builtin) {
    // 如果是node原来就有的包名字,不行
    /*
    	好像也可以写成:builtins.includes(name.toLowerCase())
    */
    if (name.toLowerCase() === builtin) {
      warnings.push(builtin + ' is a core module name')
    }
  })

  // really-long-package-names-------------------------------such--length-----many---wow
  // the thisisareallyreallylongpackagenameitshouldpublishdowenowhavealimittothelengthofpackagenames-poch.
  if (name.length > 214) {  // 文件名不能大于214个字符
    warnings.push('name can no longer contain more than 214 characters')
  }

  // mIxeD CaSe nAMEs
  if (name.toLowerCase() !== name) {  // 不能用大写
    warnings.push('name can no longer contain capital letters')
  }
	// "@antd/icons".split('/').slice(-1)[0] 是 icons,也就是文件名不能有特殊符号。
  if (/[~'!()*]/.test(name.split('/').slice(-1)[0])) {
    warnings.push('name can no longer contain special characters ("~'!()*")')
  }

  if (encodeURIComponent(name) !== name) {
    // Maybe it's a scoped package name, like "@antd/icons"
    var nameMatch = name.match(scopedPackagePattern)
    if (nameMatch) {
      var user = nameMatch[1]  // antd
      var pkg = nameMatch[2]   // icons
      // 如果解码之后和原来一致可以,不一致,就可能有中文空格什么之类的吧,数字和字母不会有问题
      if (encodeURIComponent(user) === user && encodeURIComponent(pkg) === pkg) {
        return done(warnings, errors)
      }
    }

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

  return done(warnings, errors)  // 前面很多内容都是只push没有返回,在这里统一返回
}

// 在暴露出的函数上绑了个属性,不知道干嘛用的。
validate.scopedPackagePattern = scopedPackagePattern

/**
 * @param {Array<string>} warnings 警告数组
 * @param {Array<string>} errors 错误数组
 * @returns {} boolean:新的包规范  boolean: 老的包规范  传入的两个数组?可选,如果没有就不返回数组
 */
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
}

这个包就是检查起的名字对不对的。一般用在脚手架里面,我们会create-react-app xxx, 这个xxx就是文件的名字,需要校验的对象,用户传入的总是不让人相信,就需要校验。

一些想说的话

一开始我也很抵触看源码,感觉非常难,有幸遇到了川哥。川哥举办的这个活动,我也是在群里混了半年之后,才看了一个源码。其实就像川哥讲的,真的不难,读好的源码,就像读一本书一样,你在跟作者对话,学习和看到更好的实现方式。如果大家不急着背八股文,就来一起读源码吧!

给源码写注释,大概就和看书的时候写写画画一样的吧。