1. 场景
在使用如脚手架 创建项目 的时候,为了 规范包名,会对包名进行校验,此时就用到了题目中的 validate-npm-package-name
包。
2. 使用
打开 cmd,创建 vue 项目试一下(以 @vue/cli 4.5.9 为例)
1)使用 保留字
作为项目名
2)使用 _
下划线作为开头
3)使用 大写字母
作为项目名
细心的人可能会发现,报错信息中,有的是 Error
,有的是 Warning
,区别在于:
Warning
:用于表示过去 package name 允许,而如今不允许的Error
:不符合规范的包名
3. 实现
源码较简单,主要过程是对各种情况进行 校验,并分别将错误信息添加进 Errors 和 Warnings 数组,最终将数组返回进行报错提示。
于我个人而言,该源码也帮助我复习了几个知识点,见 第四点。
'use strict'
var scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$')
// 这个包是包括node内置 module的列表
var builtins = require('builtins')
// 保留名(黑名单)
var blacklist = [
'node_modules',
'favicon.ico'
]
var validate = module.exports = function (name) {
// 警告:用于表示过去package name允许、如今不允许的兼容error
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')
}
// 校验包名不能以_开头
// '.'.match(/^_/) === null
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')
}
})
// 校验包名是否是node 内置module名、给予警告
builtins.forEach(function (builtin) {
if (name.toLowerCase() === builtin) {
warnings.push(builtin + ' is a core module name')
}
})
// 校验包名最大长度
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')
}
// 校验包名不能包含特殊字段 ~'!()*
// name.split('/').slice(-1)[0] => 获取包名、之所以要这样处理是因为
// name.split('/') 处理 npm package scope场景
// slice(-1)[0] 保证永远截取包名正确
// 'koa'.split('/').slice(-1)[0] // 'koa'
// '@babel/core'.split('/').slice(-1)[0] // 'core'
// /[~'!()*]/.test('@babel/core'.split('/').slice(-1)) // false
// /[~'!()*]/.test('@babel/co*re'.split('/').slice(-1)) // true
if (/[~'!()*]/.test(name.split('/').slice(-1)[0])) {
warnings.push('name can no longer contain special characters ("~\'!()*")')
}
// 包名不能包含non-url-safe字符
// 关于encodeURIComponent不转义哪些字符
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
if (encodeURIComponent(name) !== name) {
// 这里主要处理 scope package name 比如 @babel/core
var nameMatch = name.match(scopedPackagePattern)
if (nameMatch) {
var user = nameMatch[1] // 比如 bebel
var pkg = nameMatch[2] // 比如得到 core
// 如果没有异常 直接返回
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
// 返回结果的util方法
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
}
4. 补充
1)slice(-1)
slice(-1)
表示从尾部开始,如下
let arr = [1, 2, 3, 4]
arr.slice(-1) // [4]
arr.slice(-2) // [3, 4]
2)match(reg)
match(/^_/)
表示匹配下划线开头
- 匹配到:返回
_
- 匹配不到:返回
null
3)delete result.warnings
联想到 【源码阅读 | 01】Promisify 函数 中 Promisify
提到的 Reflect
,可以用 Reflect 来对该语句替换为
// use Object method
delete result.warnings
// use Reflect
Reflect.deleteProperty(result, "warnings")