源码地址(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 businessblacklist.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, etcbuiltins.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 nAMEsif (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 nameif (encodeURIComponent(name) !== name) {// Maybe it's a scoped package name, like @user/packagevar nameMatch = name.match(scopedPackagePattern)if (nameMatch) {// 如果是 @babel/core,则会 user = babel,pkg = corevar 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@dsafdsaerrors.push('name can only contain URL-friendly characters')}// 经过上面一系列判断之后,整体返回 warnings 数组 和 errors 数组return done(warnings, errors)}// 隐藏功能,提供给私有域定制 npm 设置包名校验规则validate.scopedPackagePattern = scopedPackagePatternvar 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 messagewarnings: warnings,// errors messageerrors: errors}if (!result.warnings.length) delete result.warningsif (!result.errors.length) delete result.errorsreturn 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) : projectNameconst 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/
