一、介绍
1.1 概念
Give me a string and I'll tell you if it's a valid npm package name.
This package exports a single synchronous function that takes a string as input and returns an object with two properties:
validForNewPackages :: Boolean
validForOldPackages :: Boolean
谷歌翻译:
接受一个字符串,判断它是否是一个有效的npm包名。
这个包导出一个同步函数,它接受一个字符串作为输入参数并返回一个具如下有两个属性的对象:
validForNewPackages :: Boolean
validForOldPackages :: Boolean
从概念中可以看出,validate-npm-package-name是用来校验npm包命名是否合法的一个工具。
ps:只有在包名合法有效的时候,返回的对象只有validForNewPackages、validForOldPackages两个属性。当包名存在问题时,返回的对象中还会有erros或者warnings。
1.2 命名规则
Below is a list of rules that valid npm package name should conform to.
package name length should be greater than zero
all the characters in the package name must be lowercase i.e., no uppercase or mixed case names are allowed
package name can consist of hyphens
package name must not contain any non-url-safe characters (since name ends up being part of a URL)
package name should not start with . or _
package name should not contain any leading or trailing spaces
package name should not contain any of the following characters: ~)('!*
package name cannot be the same as a node.js/io.js core module nor a reserved/blacklisted name. For example, the following names are invalid:
http
stream
node_modules
favicon.ico
package name length cannot exceed 214
以下是npm包命名规则列表:
1. 包名长度应大于零
2. 包名中的所有字符都必须是小写,即:不允许大写或混合大小写的名称
3. 包名可以由连字符“-”组成
4. 包名不得包含任何非 url 安全字符(因为名称最终成为 URL 的一部分)
5. 包名不应以 . 或者 _ 开始
6. 包名不应包含任何首尾空格
7. 包名不应包含以下字符:~)('!*
8. 包名不能与 node.js/io.js 核心模块相同,也不能与保留/黑名单名称相同。例如,以下名称无效:
http stream node_modules favicon.ico
9. 包名长度不能超过214
1.2.1 有效包名
var validate = require("validate-npm-package-name")
validate("some-package")
validate("example.com")
validate("under_score")
validate("123numeric")
validate("@npm/thingy")
validate("@jane/foo.js")
以上包名都是有效的,所以会返回以下这个对象:
{
validForNewPackages: true,
validForOldPackages: true
}
1.2.2 无效包名
validate("excited!")
validate(" leading-space:and:weirdchars")
以上包名无效,所以会返回以下对象:
{
validForNewPackages: false,
validForOldPackages: false,
errors: [
'name cannot contain leading or trailing spaces',
'name can only contain URL-friendly characters'
]
}
1.3 以前的包名
在以前,npm包命名随意,可以有大写字母,包名可能很长,也有可能是已经存在的包名。
在现在,如果给validate输入一个在以前算是有效的包名,会看到 validForNewPackages 属性的值发生变化,并且会出现一个警告数组:
validate("eLaBorAtE-paCkAgE-with-mixed-case-and-more-than-214-characters-----------------------------------------------------------------------------------------------------------------------------------------------------------")
以上函数返回:
{
validForNewPackages: false,
validForOldPackages: true,
warnings: [
"name can no longer contain capital letters",
"name can no longer contain more than 214 characters"
]
}
二、源码分析
2.1 源码
源码总共105行,算是偏少的,易于学习~
'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 = []
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)
}
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')
}
// 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
// core module names like http, events, util, etc
builtins.forEach(function (builtin) {
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) {
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')
}
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)
}
validate.scopedPackagePattern = scopedPackagePattern
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
}
2.2 分析
2.2.1
var scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$')
// node所有内置模块
var builtins = require('builtins')
// 黑名单
var blacklist = [
'node_modules',
'favicon.ico'
]
源码中用到了builtins,builtins在之后的源码中,用来判断包名是否与当前 Node.js 版本的核心模块相同。
在此罗列一下核心模块:
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'
]
2.2.2
注释“校验代码”部分主要是判断包名是否有效的代码,暂时先省略,在之后再分析,先分析其他部分。
var validate = module.exports = function (name) {
// 警告数组
var warnings = []
// 报错数组
var errors = []
/***** 校验代码 *****/
return done(warnings, errors)
}
// 赋值正则对象
validate.scopedPackagePattern = scopedPackagePattern
var done = function (warnings, errors) {
var result = {
// 该属性来判断一个包名是否合法
validForNewPackages: errors.length === 0 && warnings.length === 0,
// 这个属性是用于兼容最开始node package name带来的遗留问题,那个时候有些包名不规范
validForOldPackages: errors.length === 0,
// 警告数组
warnings: warnings,
// 报错数组
errors: errors
}
if (!result.warnings.length) delete result.warnings
if (!result.errors.length) delete result.errors
return result
}
在介绍概念的时候提到过,validate函数返回的对象有两个属性,值均为布尔值:validForNewPackages和validForOldPackages。warnings和errors两个数组根据其长度是否为0,确定是否存在。
在return的done函数中,对以上几个属性赋值并返回对象。其中validForNewPackages用来判断一个包名是否合法,validForOldPackages主要是为了兼容以前包名不规范时定下的包名。
2.2.3 errors
在执行到if判断里的“return done(warnings, errors) ”时,valid函数便会执行结束,不再执行之后的if判断,提高效率。
1、校验包名是否是字符串
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)
}
2、校验包名是否是合法的字符串
// 不能是空字符串
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')
}
// 不能存在黑名单中
// No funny business
blacklist.forEach(function (blacklistedName) {
if (name.toLowerCase() === blacklistedName) {
errors.push(blacklistedName + ' is a blacklisted name')
}
})
2.2.4 warnings
// Generate warnings for stuff that used to be allowed
// 包名不能与node.js/io.js 的核心模块名相同
// 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
// 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')
}
// 包名需要为小写
// mIxeD CaSe nAMEs
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 ("~'!()*")')
}
2.2.5 包名只能包含URL安全字符
var scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$')
// 包名只能包含URL安全字符
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')
}
三、心得
虽然知道源码学习很重要,但平时因为工作忙或者懈怠,源码看的比较少,刚好碰到了“源码共读”这个活动,开始系统地看源码,希望能坚持下去,coding能力up up up!
根据推荐的学习顺序, validate-npm-package-name的源码还是十分友好的,学习到了一些知识还有写代码风格,是个好的开始。
学习文章: