【若川视野 x 源码共读】第7期 | validate-npm 检测包是否符合标准

173 阅读2分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

在我们使用脚手架快速创建项目时,不管是 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
 * }
*/

那它是从哪些方面来进行包名的规则校验呢?

  1. 非空判断
  2. 必须为字符串
  3. 长度必须大于0
  4. 不能以.开头
  5. 不能以_ 开头
  6. 前后不能有空格
  7. 不能包含黑名单中的名称
  8. 不能包含 node 内置的包名
  9. 命名长度限制
  10. 必须是小写
  11. 不可以包含特殊字段 ~'!()*
  12. 不能包含非 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
}

总的来说,有一种表单校验那种味儿,只不过不是每一个表单都对应一个校验规则,而是一个包名对应多种规则这样。