【若川视野 x 源码共读】第7期 | validate-npm-package-name 检测 npm 包是否符合标准

140 阅读2分钟

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

源码:github.com/npm/validat…

链接:juejin.cn/post/708498…

1、源码

'use strict'
//?: 匹配 pattern 但不获取匹配结果
var scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$')
var builtins = require('builtins')
var blacklist = [  'node_modules',  'favicon.ico',]

function validate (name) {
  var warnings = []
  var errors = []

  //是否为空
  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)
  }

 //是否长度为0
  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(/^_/)) {
    errors.push('name cannot start with an underscore')
  }

  //是否有空格
  if (name.trim() !== name) {
    errors.push('name cannot contain leading or trailing spaces')
  }
  
  // 是否与保留/列入黑名单的名称相同
  blacklist.forEach(function (blacklistedName) {
    if (name.toLowerCase() === blacklistedName) {
      errors.push(blacklistedName + ' is a blacklisted name')
    }
  })

  //是否与节点.js/io.js核心模块相同
  builtins({ version: '*' }).forEach(function (builtin) {
    if (name.toLowerCase() === builtin) {
      warnings.push(builtin + ' is a core module name')
    }
  })

  //是否长度大于214
  if (name.length > 214) {
    warnings.push('name can no longer contain more than 214 characters')
  }

  // 是否小写
  if (name.toLowerCase() !== name) {
    warnings.push('name can no longer contain capital letters')
  }

  //是否包含~'!()*
  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 @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')
  }

  return done(warnings, errors)
}

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
}

module.exports = validate

builtin核心模块:

'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'

2、注意

encodeURIComponent() 函数通过将一个,两个,三个或四个表示字符的 UTF-8 编码的转义序列替换某些字符的每个实例来编码 URI

var set1 = ";,/?:@&=+$";  // 保留字符
var set2 = "-_.!~*'()";   // 不转义字符
var set3 = "#";           // 数字标志
var set4 = "ABC abc 123"; // 字母数字字符和空格

console.log(encodeURI(set1)); // ;,/?:@&=+$
console.log(encodeURI(set2)); // -_.!~*'()
console.log(encodeURI(set3)); // #
console.log(encodeURI(set4)); // ABC%20abc%20123 (空格被编码为 %20)

console.log(encodeURIComponent(set1)); // %3B%2C%2F%3F%3A%40%26%3D%2B%24
console.log(encodeURIComponent(set2)); // -_.!~*'()
console.log(encodeURIComponent(set3)); // %23
console.log(encodeURIComponent(set4)); // ABC%20abc%20123 (the space gets encoded as %20)

3、测试

test('validate-npm-package-name', function (t) {
  // Traditional

  t.same(validate('some-package'), { validForNewPackages: true, validForOldPackages: true })
  t.same(validate('example.com'), { validForNewPackages: true, validForOldPackages: true })
  t.same(validate('under_score'), { validForNewPackages: true, validForOldPackages: true })
  t.same(validate('period.js'), { validForNewPackages: true, validForOldPackages: true })
  t.same(validate('123numeric'), { validForNewPackages: true, validForOldPackages: true })
  t.same(validate('crazy!'), {
    validForNewPackages: false,
    validForOldPackages: true,
    warnings: ['name can no longer contain special characters ("~'!()*")'],
  })

  // Scoped (npm 2+)

  t.same(validate('@npm/thingy'), { validForNewPackages: true, validForOldPackages: true })
  t.same(validate('@npm-zors/money!time.js'), {
    validForNewPackages: false,
    validForOldPackages: true,
    warnings: ['name can no longer contain special characters ("~'!()*")'],
  })

  // Invalid

  t.same(validate(''), {
    validForNewPackages: false,
    validForOldPackages: false,
    errors: ['name length must be greater than zero'] })

  t.same(validate(''), {
    validForNewPackages: false,
    validForOldPackages: false,
    errors: ['name length must be greater than zero'] })

  t.same(validate('.start-with-period'), {
    validForNewPackages: false,
    validForOldPackages: false,
    errors: ['name cannot start with a period'] })

  t.same(validate('_start-with-underscore'), {
    validForNewPackages: false,
    validForOldPackages: false,
    errors: ['name cannot start with an underscore'] })

  t.same(validate('contain:colons'), {
    validForNewPackages: false,
    validForOldPackages: false,
    errors: ['name can only contain URL-friendly characters'] })

  t.same(validate(' leading-space'), {
    validForNewPackages: false,
    validForOldPackages: false,
    /* eslint-disable-next-line max-len */
    errors: ['name cannot contain leading or trailing spaces', 'name can only contain URL-friendly characters'] })

  t.same(validate('trailing-space '), {
    validForNewPackages: false,
    validForOldPackages: false,
    /* eslint-disable-next-line max-len */
    errors: ['name cannot contain leading or trailing spaces', 'name can only contain URL-friendly characters'] })

  t.same(validate('s/l/a/s/h/e/s'), {
    validForNewPackages: false,
    validForOldPackages: false,
    errors: ['name can only contain URL-friendly characters'] })

  t.same(validate('node_modules'), {
    validForNewPackages: false,
    validForOldPackages: false,
    errors: ['node_modules is a blacklisted name'] })

  t.same(validate('favicon.ico'), {
    validForNewPackages: false,
    validForOldPackages: false,
    errors: ['favicon.ico is a blacklisted name'] })

  // Node/IO Core

  t.same(validate('http'), {
    validForNewPackages: false,
    validForOldPackages: true,
    warnings: ['http is a core module name'] })

  t.deepEqual(validate('process'), {
    validForNewPackages: false,
    validForOldPackages: true,
    warnings: ['process is a core module name'] })

  // Long Package Names

  /* eslint-disable-next-line max-len */
  t.same(validate('ifyouwanttogetthesumoftwonumberswherethosetwonumbersarechosenbyfindingthelargestoftwooutofthreenumbersandsquaringthemwhichismultiplyingthembyitselfthenyoushouldinputthreenumbersintothisfunctionanditwilldothatforyou-'), {
    validForNewPackages: false,
    validForOldPackages: true,
    warnings: ['name can no longer contain more than 214 characters'],
  })

  /* eslint-disable-next-line max-len */
  t.same(validate('ifyouwanttogetthesumoftwonumberswherethosetwonumbersarechosenbyfindingthelargestoftwooutofthreenumbersandsquaringthemwhichismultiplyingthembyitselfthenyoushouldinputthreenumbersintothisfunctionanditwilldothatforyou'), {
    validForNewPackages: true,
    validForOldPackages: true,
  })

  // Legacy Mixed-Case

  t.same(validate('CAPITAL-LETTERS'), {
    validForNewPackages: false,
    validForOldPackages: true,
    warnings: ['name can no longer contain capital letters'] })

  t.end()
})