本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
在我们使用脚手架快速创建项目时,不管是 Vue 的 vue create xxx 和 React 的 create-react-app 都会使用 validate-npm-package-name 来检测 npm 包名是否符合标准,比如在 vue-cli 中:
// https://github1s.com/vuejs/vue-cli/blob/HEAD/packages/@vue/cli/lib/create.js#L8
const fs = require('fs-extra')
const path = require('path')
const inquirer = require('inquirer')
const Creator = require('./Creator')
const { clearConsole } = require('./util/clearConsole')
const { getPromptModules } = require('./util/createTools')
const { chalk, error, stopSpinner, exit } = require('@vue/cli-shared-utils')
const validateProjectName = require('validate-npm-package-name')
最后一行就引入了这个包,它的作用就是来检测我们安装的 npm 包名是否都是合格的,可以正常在项目中使用的。其使用也是相当的简单,还是刚刚那个文件:
const result = validateProjectName(name)
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)
}
可以看到在传入一个包名后,会得到一个 result 对象,我们读取这个对象的 validForNewPackages 属性,当它返回 false 时,表示这个包名是不合法的,此时会在控制台中输出错误信息提示给用户。
语法上:
/**
* @param: <string> package_name
* @return {
* validForNewPackages: Boolean
* validForOldPackages: Boolean
* }
*/
那它是从哪些方面来进行包名的规则校验呢?
- 非空判断
- 必须为字符串
- 长度必须大于0
- 不能以
.开头 - 不能以
_开头 - 前后不能有空格
- 不能包含黑名单中的名称
- 不能包含 node 内置的包名
- 命名长度限制
- 必须是小写
- 不可以包含特殊字段
~'!()* - 不能包含非 url 安全字符
接下来我们一起看看源码:
// validate-npm-package-name/index.js
'use strict'
var scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$')
// node 包内的内置包名
var builtins = require('builtins') // 有兴趣可以看下
// 黑名单
var blacklist = [
'node_modules',
'favicon.ico'
]
var validate = module.exports = function (name) {
var warnings = []
var errors = []
// 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)
}
// 2. 必须为字符串
if (typeof name !== 'string') {
errors.push('name must be a string')
return done(warnings, errors)
}
// 3. 长度必须大于0
if (!name.length) {
errors.push('name length must be greater than zero')
}
// 4. 不能以.开头
if (name.match(/^./)) {
errors.push('name cannot start with a period')
}
// 5. 不能以_ 开头
if (name.match(/^_/)) {
errors.push('name cannot start with an underscore')
}
// 6. 前后不能有空格
if (name.trim() !== name) {
errors.push('name cannot contain leading or trailing spaces')
}
// 7. 不能包含黑名单中的名称
blacklist.forEach(function (blacklistedName) {
if (name.toLowerCase() === blacklistedName) {
errors.push(blacklistedName + ' is a blacklisted name')
}
})
// 8. 不能包含 node 内置的包名
builtins.forEach(function (builtin) {
if (name.toLowerCase() === builtin) {
warnings.push(builtin + ' is a core module name')
}
})
// 9. 命名长度限制
if (name.length > 214) {
warnings.push('name can no longer contain more than 214 characters')
}
// 10. 必须是小写
if (name.toLowerCase() !== name) {
warnings.push('name can no longer contain capital letters')
}
// 11. 不可以包含特殊字段 ~'!()*
// name.split('/').slice(-1)[0] 这个是获取包名用的,这样写的原因是可以兼容
// scope package name 的场景
/**
* express => express
* @babel/core => core
*/
if (/[~'!()*]/.test(name.split('/').slice(-1)[0]) {
warnings.push('name can no longer contain special characters ("~\'!()*")')
}
// 12. 不能包含非 url 安全字符
if (encodeURIComponent(name) !== name) {
// 也可能是一个内置的包名,比如 @vue/cli
var nameMatch = name.match(scopedPackagePattern)
if (nameMatch) {
var user = nameMatch[1] // vue
var pkg = nameMatch[2] // cli
// 如果符合规范,则直接返回
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,
// 遗留的包名问题,比如最开始 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
}
总的来说,有一种表单校验那种味儿,只不过不是每一个表单都对应一个校验规则,而是一个包名对应多种规则这样。