源码地址(npm 官方出品)

https://github.com/npm/validate-npm-package-name#naming-rules
检查 npm 包名是否正确合法(Give me a string and I’ll tell you if it’s a valid npm package name.)

源码注释

  1. 'use strict'
  2. var scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$')
  3. var builtins = require('builtins')
  4. var blacklist = [
  5. 'node_modules',
  6. 'favicon.ico'
  7. ]
  8. var validate = module.exports = function (name) {
  9. var warnings = []
  10. var errors = []
  11. // 判断是否为空,如果为空则为 error 级别错误,直接 return 退出返回,无需继续以下检查
  12. if (name === null) {
  13. errors.push('name cannot be null')
  14. return done(warnings, errors)
  15. }
  16. // 判断是否 undefined,error 级别错误,直接 return 返回
  17. if (name === undefined) {
  18. errors.push('name cannot be undefined')
  19. return done(warnings, errors)
  20. }
  21. // 判断是否为 string 类型,error 级别错误,直接 return 返回
  22. // 123 是错误的,'123' 是正确的
  23. if (typeof name !== 'string') {
  24. errors.push('name must be a string')
  25. return done(warnings, errors)
  26. }
  27. // 判断长度,如果是 '' 则为 error 级别错误,这里其实也可以直接 return 返回?
  28. if (!name.length) {
  29. errors.push('name length must be greater than zero')
  30. }
  31. // 判断是否为 . 开头的,error 级别错误,不需要返回,因为还有别的错误要一起检查
  32. if (name.match(/^\./)) {
  33. errors.push('name cannot start with a period')
  34. }
  35. // 判断是否为 _ 开头的,error 级别错误,不需要返回
  36. if (name.match(/^_/)) {
  37. errors.push('name cannot start with an underscore')
  38. }
  39. // 判断开头结尾是不是有空格的,error 级别错误,不需要返回
  40. if (name.trim() !== name) {
  41. errors.push('name cannot contain leading or trailing spaces')
  42. }
  43. // 判断是不是黑名单的关键字,大小写不敏感,统一变成小写来判断,error 级别错误
  44. // No funny business
  45. blacklist.forEach(function (blacklistedName) {
  46. if (name.toLowerCase() === blacklistedName) {
  47. errors.push(blacklistedName + ' is a blacklisted name')
  48. }
  49. })
  50. // Generate warnings for stuff that used to be allowed
  51. // 使用 builtins 库,拿到 node core module names 列表
  52. // 逐个判断是不是有一致的,如果是,则为 warning 级别错误
  53. // core module names like http, events, util, etc
  54. builtins.forEach(function (builtin) {
  55. if (name.toLowerCase() === builtin) {
  56. warnings.push(builtin + ' is a core module name')
  57. }
  58. })
  59. // 长度不能超过 214 个字符,warning 级别错误
  60. // really-long-package-names-------------------------------such--length-----many---wow
  61. // the thisisareallyreallylongpackagenameitshouldpublishdowenowhavealimittothelengthofpackagenames-poch.
  62. if (name.length > 214) {
  63. warnings.push('name can no longer contain more than 214 characters')
  64. }
  65. // 不能包含大写字符,warning 级别错误
  66. // mIxeD CaSe nAMEs
  67. if (name.toLowerCase() !== name) {
  68. warnings.push('name can no longer contain capital letters')
  69. }
  70. // 判断是否包含 ~'!()* 这几个特殊字符,但只是判断 / 后面的
  71. // 比如 @test/abc 是正确的
  72. // 比如 @test/ab!!!c 是错误的
  73. // 比如 @test!!!/abcabfd 是正确的
  74. if (/[~'!()*]/.test(name.split('/').slice(-1)[0])) {
  75. warnings.push('name can no longer contain special characters ("~\'!()*")')
  76. }
  77. // 判断是否为 scoped package name
  78. if (encodeURIComponent(name) !== name) {
  79. // Maybe it's a scoped package name, like @user/package
  80. var nameMatch = name.match(scopedPackagePattern)
  81. if (nameMatch) {
  82. // 如果是 @babel/core,则会 user = babel,pkg = core
  83. var user = nameMatch[1]
  84. var pkg = nameMatch[2]
  85. // 然后使用 encodeURIComponent,判断这俩里面有没有 non-url-safe 字符
  86. if (encodeURIComponent(user) === user && encodeURIComponent(pkg) === pkg) {
  87. // 注意这里是全等才进入,说明没有包含 non-url-safe 字符
  88. // 说明最后的检查已经结束
  89. return done(warnings, errors)
  90. }
  91. }
  92. // 有可能是正则 mathch 不出来的,就会标为非法,比如 @abc/sfdsa/dsafdsa, @abc/sfdsa@dsafdsa/sdafdsa
  93. // 也有可能是 match 解析出来之后,user 和 pkg 内包含特殊字符的,比如 @abc/sfdsa@dsafdsa
  94. errors.push('name can only contain URL-friendly characters')
  95. }
  96. // 经过上面一系列判断之后,整体返回 warnings 数组 和 errors 数组
  97. return done(warnings, errors)
  98. }
  99. // 隐藏功能,提供给私有域定制 npm 设置包名校验规则
  100. validate.scopedPackagePattern = scopedPackagePattern
  101. var done = function (warnings, errors) {
  102. var result = {
  103. // 新版本 pkg name 不允许有 errors 和 warnings 错误
  104. validForNewPackages: errors.length === 0 && warnings.length === 0,
  105. // 旧版本 pkg name 不允许有 errors 错误,可以忽略 warnings 错误
  106. validForOldPackages: errors.length === 0,
  107. // warnings message
  108. warnings: warnings,
  109. // errors message
  110. errors: errors
  111. }
  112. if (!result.warnings.length) delete result.warnings
  113. if (!result.errors.length) delete result.errors
  114. return result
  115. }

深究细节

1. 判断逻辑

  • 比较明显能直接返回的错误,会写在前面,比如 null,undefined,非 string 判断这种
  • 除了直接 return 的下面所有 if 校验,会一条条叠加判断,有可能会命中多条,比如有可能字符会超过 214,同时又包含特殊字符,又包含大写字符等。这样一次性就能返回给开发者所有错误信息。
  • if (encodeURIComponent(user) === user && encodeURIComponent(pkg) === pkg) 这段的判断可能会有点点绕,是不包含 non-url-safe 字符才进入

    2. module exports 暴露的 validate

    最后一句有句 validate.scopedPackagePattern = scopedPackagePattern,具体作用是?

    3. encodeURIComponent 和 encodeURI 区别

    encodeURIComponent 比 encodeURI 编码范围更大,比如 http://,encodeURIComponent 会编码成 http%3A%2F%2F,encodeURI 不会

    4. 如何判断 node core module names

    使用 builtins 这个包,https://www.npmjs.com/package/builtins,里面逻辑也比较简单,返回的是 string array,https://github.com/juliangruber/builtins/blob/master/index.js,包含的是 core module name

5. 正则

new RegExp(‘^(?:@([^/]+?)[/])?([^/]+?)$’),@ 和 / 之间的是 group1,/ 之后的是 group2
截屏2021-10-10 上午12.24.27.png
这里有个特殊字符,?:@,可以对比看看没有 ?: 是怎么样的,new RegExp(‘^(@([^/]+?)[/])?([^/]+?)$’),如下图,会多匹配出一个 group
截屏2021-10-10 上午12.27.59.png
截屏2021-10-10 上午12.29.49.png
test 一下几个 case(对着图示分析,总的来说,能根据字符串能完整走一个通路,就说明能匹配,否则为 null):

  • ‘@demo/test’.match(new RegExp(‘^(?:@([^/]+?)[/])?([^/]+?)$’)),匹配出 user(group1) = ‘demo’,pkg(group2) = ‘test’
  • ‘/d’.match(new RegExp(‘^(?:@([^/]+?)[/])?([^/]+?)$’)),调用 match 返回 null
  • ‘@user’.match(new RegExp(‘^(?:@([^/]+?)[/])?([^/]+?)$’)),匹配出 user(group1) = undefined, pkg(group2) = ‘@user’

应用场景

https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli/lib/create.js#L20
比如 vue-cli create,Error 错误看出来是 validate-npm-package-name 透传出来的错误信息,拼接之后直接展示:
截屏2021-10-09 下午7.06.23.png
对应代码:

  1. // ...
  2. // 引用包
  3. const validateProjectName = require('validate-npm-package-name')
  4. async function create (projectName, options) {
  5. if (options.proxy) {
  6. process.env.HTTP_PROXY = options.proxy
  7. }
  8. const cwd = options.cwd || process.cwd()
  9. const inCurrent = projectName === '.'
  10. const name = inCurrent ? path.relative('../', cwd) : projectName
  11. const targetDir = path.resolve(cwd, projectName || '.')
  12. // 调用库校验
  13. const result = validateProjectName(name)
  14. // 如果检验不通过,逐行显示错误信息,最后 exit(1) 退出程序
  15. if (!result.validForNewPackages) {
  16. console.error(chalk.red(`Invalid project name: "${name}"`))
  17. result.errors && result.errors.forEach(err => {
  18. console.error(chalk.red.dim('Error: ' + err))
  19. })
  20. result.warnings && result.warnings.forEach(warn => {
  21. console.error(chalk.red.dim('Warning: ' + warn))
  22. })
  23. exit(1)
  24. }
  25. // ...

附录