源码地址(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.)
源码注释
'use strict'
var scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$')
var builtins = require('builtins')
var blacklist = [
'node_modules',
'favicon.ico'
]
var validate = module.exports = function (name) {
var warnings = []
var errors = []
// 判断是否为空,如果为空则为 error 级别错误,直接 return 退出返回,无需继续以下检查
if (name === null) {
errors.push('name cannot be null')
return done(warnings, errors)
}
// 判断是否 undefined,error 级别错误,直接 return 返回
if (name === undefined) {
errors.push('name cannot be undefined')
return done(warnings, errors)
}
// 判断是否为 string 类型,error 级别错误,直接 return 返回
// 123 是错误的,'123' 是正确的
if (typeof name !== 'string') {
errors.push('name must be a string')
return done(warnings, errors)
}
// 判断长度,如果是 '' 则为 error 级别错误,这里其实也可以直接 return 返回?
if (!name.length) {
errors.push('name length must be greater than zero')
}
// 判断是否为 . 开头的,error 级别错误,不需要返回,因为还有别的错误要一起检查
if (name.match(/^\./)) {
errors.push('name cannot start with a period')
}
// 判断是否为 _ 开头的,error 级别错误,不需要返回
if (name.match(/^_/)) {
errors.push('name cannot start with an underscore')
}
// 判断开头结尾是不是有空格的,error 级别错误,不需要返回
if (name.trim() !== name) {
errors.push('name cannot contain leading or trailing spaces')
}
// 判断是不是黑名单的关键字,大小写不敏感,统一变成小写来判断,error 级别错误
// No funny business
blacklist.forEach(function (blacklistedName) {
if (name.toLowerCase() === blacklistedName) {
errors.push(blacklistedName + ' is a blacklisted name')
}
})
// Generate warnings for stuff that used to be allowed
// 使用 builtins 库,拿到 node core module names 列表
// 逐个判断是不是有一致的,如果是,则为 warning 级别错误
// core module names like http, events, util, etc
builtins.forEach(function (builtin) {
if (name.toLowerCase() === builtin) {
warnings.push(builtin + ' is a core module name')
}
})
// 长度不能超过 214 个字符,warning 级别错误
// 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')
}
// 不能包含大写字符,warning 级别错误
// mIxeD CaSe nAMEs
if (name.toLowerCase() !== name) {
warnings.push('name can no longer contain capital letters')
}
// 判断是否包含 ~'!()* 这几个特殊字符,但只是判断 / 后面的
// 比如 @test/abc 是正确的
// 比如 @test/ab!!!c 是错误的
// 比如 @test!!!/abcabfd 是正确的
if (/[~'!()*]/.test(name.split('/').slice(-1)[0])) {
warnings.push('name can no longer contain special characters ("~\'!()*")')
}
// 判断是否为 scoped package name
if (encodeURIComponent(name) !== name) {
// Maybe it's a scoped package name, like @user/package
var nameMatch = name.match(scopedPackagePattern)
if (nameMatch) {
// 如果是 @babel/core,则会 user = babel,pkg = core
var user = nameMatch[1]
var pkg = nameMatch[2]
// 然后使用 encodeURIComponent,判断这俩里面有没有 non-url-safe 字符
if (encodeURIComponent(user) === user && encodeURIComponent(pkg) === pkg) {
// 注意这里是全等才进入,说明没有包含 non-url-safe 字符
// 说明最后的检查已经结束
return done(warnings, errors)
}
}
// 有可能是正则 mathch 不出来的,就会标为非法,比如 @abc/sfdsa/dsafdsa, @abc/sfdsa@dsafdsa/sdafdsa
// 也有可能是 match 解析出来之后,user 和 pkg 内包含特殊字符的,比如 @abc/sfdsa@dsafdsa
errors.push('name can only contain URL-friendly characters')
}
// 经过上面一系列判断之后,整体返回 warnings 数组 和 errors 数组
return done(warnings, errors)
}
// 隐藏功能,提供给私有域定制 npm 设置包名校验规则
validate.scopedPackagePattern = scopedPackagePattern
var done = function (warnings, errors) {
var result = {
// 新版本 pkg name 不允许有 errors 和 warnings 错误
validForNewPackages: errors.length === 0 && warnings.length === 0,
// 旧版本 pkg name 不允许有 errors 错误,可以忽略 warnings 错误
validForOldPackages: errors.length === 0,
// warnings message
warnings: warnings,
// errors message
errors: errors
}
if (!result.warnings.length) delete result.warnings
if (!result.errors.length) delete result.errors
return result
}
深究细节
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
这里有个特殊字符,?:@,可以对比看看没有 ?: 是怎么样的,new RegExp(‘^(@([^/]+?)[/])?([^/]+?)$’),如下图,会多匹配出一个 group
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 透传出来的错误信息,拼接之后直接展示:
对应代码:
// ...
// 引用包
const validateProjectName = require('validate-npm-package-name')
async function create (projectName, options) {
if (options.proxy) {
process.env.HTTP_PROXY = options.proxy
}
const cwd = options.cwd || process.cwd()
const inCurrent = projectName === '.'
const name = inCurrent ? path.relative('../', cwd) : projectName
const targetDir = path.resolve(cwd, projectName || '.')
// 调用库校验
const result = validateProjectName(name)
// 如果检验不通过,逐行显示错误信息,最后 exit(1) 退出程序
if (!result.validForNewPackages) {
console.error(chalk.red(`Invalid project name: "${name}"`))
result.errors && result.errors.forEach(err => {
console.error(chalk.red.dim('Error: ' + err))
})
result.warnings && result.warnings.forEach(warn => {
console.error(chalk.red.dim('Warning: ' + warn))
})
exit(1)
}
// ...
附录
- 正则表达式:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions
- encodeURIComponent:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
- encodeURI:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
- 正则表达可视化:https://regexper.com/