《若川视野x源码共读》第7期 源码学习系列之validate-npm-pageage-name

104 阅读4分钟

前言

本文参加了由公众号@若川视野 发起的每周源码共读活动。从简单到进阶学习源码中的巧妙之处,旨在于将学习的东西应用到实际的开发中,同时借鉴源码中的思想,规范自我开发,以及锻炼自己的开发思维。也欢迎大家加入大佬的源码共读活动。一起卷起来。

疑问

我们尝尝通过vue create 项目名称 去创建项目,有时候会因为项目名称的不规范导致 创建不成功,比如 项目名称中文 项目名称大小写问题等,比如下图:

image.png

那这是为什么呢,项目名都有哪些规则呢,带着问题我们进入接下来的学习

正文

笔者用的是vue的技术栈 因此我们从vuecli中去查找问题。

vuecli是如何进行包名的验证

我们进入vuecli 的源码,因为我们是通过vue create 的方式去创建的。

//pageage.json 
{
....
"bin":{
 "vue":"bin/vue.js" //可以看出我们的入口在这里
}
}

bin/vue.js文件我们可以查找到create 指令,在create指令中加载了一个验证包名的方法

program
    .command('create <app-name>')
    .description('create a new project powered by vue-cli-service')
    .option('-p, --preset <presetName>', 'Skip prompts and use saved or remote preset')
    .option('-d, --default', 'Skip prompts and use default preset')
    .option('-i, --inlinePreset <json>', 'Skip prompts and use inline JSON string as preset')
    .option('-m, --packageManager <command>', 'Use specified npm client when installing dependencies')
    .option('-r, --registry <url>', 'Use specified npm registry when installing dependencies (only for npm)')
    .option('-g, --git [message]', 'Force git initialization with initial commit message')
    .option('-n, --no-git', 'Skip git initialization')
    .option('-f, --force', 'Overwrite target directory if it exists')
    .option('--merge', 'Merge target directory if it exists')
    .option('-c, --clone', 'Use git clone when fetching remote preset')
    .option('-x, --proxy <proxyUrl>', 'Use specified proxy when creating project')
    .option('-b, --bare', 'Scaffold project without beginner instructions')
    .option('--skipGetStarted', 'Skip displaying "Get started" instructions')
    .action((name, options) => {
        if (minimist(process.argv.slice(3))._.length > 1) {
            console.log(chalk.yellow('\n Info: You provided more than one argument. The first one will be used as the app\'s name, the rest are ignored.'))
        }
        // --git makes commander to default git to true
        if (process.argv.includes('-g') || process.argv.includes('--git')) {
            options.forceGit = true
        }
        //这里引入了create 方法,也就是包名的验证方法
        require('../lib/create')(name, options)
    })

create方法它是怎样的验证包名,带着疑问继续往下

image.png 在create中我们可以看出vuecli中包名验证用到了 validate-npm-package-name 的插件进行验证的。那我们继续 validate-npm-package-name 它又是如何验证的呢?

validate-npm-package-name源码和原理

接着上述的问题我们进入validate-npm-package-name源码查看它是如何进行包名验证的.

'use strict'
//用于匹配@babel/core 格式的包
var scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$')
//引入node中内置的关键字
var builtins = require('builtins')
//黑名单  
var blacklist = [
  'node_modules',
  'favicon.ico',
]

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 
  //包名不能是黑名单里面的名称也就是node_modules或者favicon.ico
  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
  //包名不能是node 中的关键字
  builtins({ version: '*' }).forEach(function (builtin) {
    if (name.toLowerCase() === builtin) {
      warnings.push(builtin + ' is a core module name')
    }
  })
 //报的长度不能大于214
  if (name.length > 214) {
    warnings.push('name can no longer contain more than 214 characters')
  }

 // 包名必须小写
  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 ("~\'!()*")')
  }
 //包名不能包含任何非url 安全字符
 //也就是说我们的包名只能由A-Z a-z 0-9 - 等组成并且 不能有空格存在
  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)
}

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
}

module.exports = validate

builtins 模块的源码

'use strict'

var semver = require('semver')

module.exports = function (version) {
  // 获取node版本
  version = version || process.version

  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'
  ]

  if (semver.lt(version, '6.0.0')) coreModules.push('freelist')
  if (semver.gte(version, '1.0.0')) coreModules.push('v8')
  if (semver.gte(version, '1.1.0')) coreModules.push('process')
  if (semver.gte(version, '8.1.0')) coreModules.push('async_hooks')
  if (semver.gte(version, '8.4.0')) coreModules.push('http2')
  if (semver.gte(version, '8.5.0')) coreModules.push('perf_hooks')

  return coreModules
}

以上就是我们validate-npm-pageage-name 源码的学习

总结

我们带着疑问从vuecli为入口 通过create指令查找到了 我们验证包的插件validate-npm-pageage-name 这个插件里面我们了解到了 包名是一个不能为空,且不能是node的保留字 以及一个安全url,长度不超过214的字符串。验证结果则通过validate-npm-pageage-name返回,通过返回结果中的 validForNewPackages属性去判断 包名验证是否通过