简介
本文档记录了我参加 validate-npm-package-name 检测 npm 包是否符合标准
共读的过程中的学习和思考。
- 本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
- 这是源码共读的第7期,链接: 【若川视野 x 源码共读】第7期 | validate-npm-package-name 检测 npm 包是否符合标准
参与目的:
- 探索源码,理解工作原理
- 学习和成长
- 提升技术水平
作为一名前端开发者,参与这次源码共读活动。这是一个很好的机会,与志同道合的开发者一起深入探索源码、理解工作原理,并在过程中学习和成长。
本文参与学习的目标:
- 了解 validate-npm-package-name 作用和使用场景
- vue create xxx 新建项目时,其实就用了这个包。
- vue-cli/packages/@vue/cli/lib/create.js...
- create-react-app 也用了
- create-react-app/packages/create-react-app/createReactApp.js ...
源码分析
validate-npm-package-name
是一个用于验证npm包名称是否符合规范的npm包。 它的源代码主要包含以下几个部分:
- 包名称验证逻辑:这部分代码主要负责解析npm包名称,并检查其是否符合规范。它使用了正则表达式来匹配包名称的格式,例如以字母或数字开头,后面可以包含字母、数字和下划线,但不能以点号开头或结尾等。
- 错误处理逻辑:当验证失败时,
validate-npm-package-name
会抛出异常或返回错误信息。这部分代码包括异常处理、错误信息输出和日志记录等功能。 - 依赖库:
validate-npm-package-name
使用了多个第三方库,如lodash
、axios
等,用于辅助实现其功能。这些库的作用包括字符串处理、网络请求等。
源码准备
Bash
git clone https://github.com/npm/validate-npm-package-name
有效名称
JavaScript
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")
上述所有名称都有效,因此您将取回此对象:
JavaScript
{
validForNewPackages: true,
validForOldPackages: true
}
无效名称
JavaScript
validate("excited!")
validate(" leading-space:and:weirdchars")
这从来都不是一个有效的软件包名称,所以你会得到这个:
JavaScript
{
validForNewPackages: false,
validForOldPackages: false,
errors: [
'name cannot contain leading or trailing spaces',
'name can only contain URL-friendly characters'
]
}
源码详情
代码结构
Javascript
'use strict'
const { builtinModules: builtins } = require('module')
// 正则表达式
var scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$')
// 黑名单数组
var blacklist = [
'node_modules',
'favicon.ico',
]
// 用于验证输入的名称是否符合特定的规则
function validate (name) {...}
// 对传入的警告和错误进行检查,并返回一个包含一些属性的对象
var done = function (warnings, errors) {...}
// 导出
module.exports = validate
正则表达式
javascript
var scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$')
这个正则表达式的模式是:
^
:表示匹配字符串的开始。(?:@([^/]+?)[/])?
:这部分用于匹配命名空间。@
是一个特殊的字符,通常用于标识包的名称中的命名空间。括号中的([^/]+?)
匹配任意数量的非斜杠字符,尽可能少的匹配。这部分还可以捕获命名空间部分,如果包名后面没有斜杠,这部分会匹配到包名的结束。?
:表示前面的部分是可选的。也就是说,包名可以没有命名空间,直接是另一个部分(这部分被([^/]+?)
捕获)。([^/]+?)
:这部分用于匹配包名的主要部分,尽可能少的匹配。$
:表示匹配字符串的结束。
所以,这个正则表达式可以匹配像 @John/Awesome
这样的包名,其中 John
是命名空间,Awesome
是包名的主要部分。如果包名后面没有斜杠,那么命名空间部分会匹配到 John/
结束。 然后,var blacklist = ['node_modules', 'favicon.ico']
这行代码创建了一个数组,其中包含两个字符串值:'node_modules' 和 'favicon.ico'。这个数组可能用于过滤不应该被正则表达式匹配到的包名
总的来说,这段代码可能是在处理JavaScript项目中的包管理问题,例如解析npm包名称或处理scoped包。
validate 函数
javascript
function validate (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
if (builtins.includes(name.toLowerCase())) {
warnings.push(name + ' 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')
}
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)
}
用于验证输入的 name
参数是否满足一系列特定的条件,条件如下:
name
不能为null
或undefined
。name
必须是一个字符串。name
的长度必须大于零。name
不能以句点 (.
)、下划线 (_
) 或其他特定字符开始。name
不能有首尾空格。name
不能包含前后的斜杠 (/
)。name
不能是禁止的名称(由一个黑名单列表定义)。- 旧有的标准模块名可能仍然是合法的,但可能会收到警告。
- 如果
name
的长度超过 214 个字符,将收到警告。 - 如果
name
中的大写字母或特殊字符(如~'!()*
)存在,将收到警告。 - 如果
name
不符合 URL 友好的字符规则(即编码后的name
与原始的name
不匹配),则可能是一个特定位符号或者类似 "@user/package" 这样的 scoped package 名称,这也会导致警告。
done 函数
它接受两个参数 warnings
和 errors
,并返回一个对象。这个对象包含了一些关于输入参数的验证结果,以及一些警告和错误信息。
Javascript
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
}
- 首先,它检查
warnings
和errors
数组的长度。如果它们都为空(即长度为 0),那么函数会返回一个对象,该对象表示所有输入都是有效的。 - 如果
warnings
数组中有元素,那么这些元素会被存储在result.warnings
中。 - 如果
errors
数组中有元素,那么这些元素会被存储在result.errors
中。 - 如果
result.warnings
或result.errors
的长度为 0,那么它们将被从result
对象中删除。这是为了减少不必要的存储空间,防止出现大量的警告和错误信息而影响性能。 - 最后,这个函数会返回包含所有有效性的验证结果和警告和错误信息的对象。
总结
通过阅读这段代码,我学习到了 JavaScript 中函数的基本结构和逻辑,同时也了解了如何在 JavaScript 中处理输入参数的验证。
总的来说,这段代码相对来说不难