乐趣区

veecli脚手架实践上

前言

随着前端的发展,工程化已经成为每一个前端都应该重视和熟悉的方面,而作为工程化之一的脚手架更是在日常开发中起到了十分重要的作用,而能够根据业务和项目需要为自己的前端团队打造个性化的属于自己团队的脚手架也会成为工程师项目简历中的一道亮点,本文主要是借鉴 vue-cli 脚手架的源码,练习自己搭建脚手架过程中遇到的问题及自己的思考。(ps: 一款成熟的脚手架通常要包括以下功能:1、命令行;2、模板拉取;3、本地服务;4、打包构建;5、集成部署;6、周边其他,本文主要是对脚手架大致的功能进行一定的探究,手写一版粗浅的脚手架,后续如果有项目实践,会持续更新分享。)

命令行

大框架结构

[目录结构]

  • bin

    • www (全局命令执行入口连接,用 npm link 进行连接)
  • src

    • main.js (入口文件,对命令进行分发)
    • create.js (创建的命令,所有创建的逻辑在此文件中书写)
    • config.js (配置的命令,所有的配置的命令在此文件中书写)
    • constants.js (常量文件,所有常量配置在此文件中)
  • package.json

[目录描述] bin 下的 www 脚本获取全局的执行环境,可引入入口文件,我们在此将其导入到 main.js 中,所有的命令都可在 main.js 进行分发,为了更好的使用,可以将不同命令分到不同的 js 文件下,这里用到了 commander 的包对命令进行分发,具体业务可在对应文件中进行后续扩展

命令配置

入口

bin 目录下的可执行脚本 www,

! /usr/bin/env node

package.json 中配置:

“bin”: {

"vee-cli": "./bin/www"

},

其可对全局下的命令进行分发,由 npm link 进行连接,如果权限不够可能需要使用 sudo 等,具体的爬坑可以参考这篇文章 npm link 没有效果的问题,在入口文件中可先打印一个命令 ”hello vee” 进行测试,如图所示。

命令

完成测试后,就是命令分发的主要逻辑了,这里主要用到了 commander 的包,源码部分在下节中解读,mapAction 主要是映射命令的行为,其中主要是 alias、description、examples,然后就是每个行为的显示执行了,这里有一个关键代码是 require(path.resolve(__dirname,action))(...process.argv.slice(3)) 获取对应的文件名来进行解析,这样就可以把各个功能抽离到对应文件里去做具体的逻辑

const program = require('commander');
const {version} = require('./constants');
const path = require('path');

const mapAction = {
    create: {
        alias: 'c', 
        description: 'create a project', 
        examples: ['vee-cli create <project-name>'],
    },
    config: {
        alias: 'conf',
        description: 'config project variable',
        examples: [
            'vee-cli config set <k> <v>',
            'vee-cli config get <k>',
        ],
    },
    '*': { 
        alias: '',
        description: 'command not found',
        examples: [],}
}

// 可以使用 Reflect,好处是可以遍历 Symbol
Object.keys(mapAction).forEach((action) => {program.command(action) 
        .alias(mapAction[action].alias) 
        .description(mapAction[action].description) 
        .action(() => {if (action === '*') {console.log(mapAction[action].description)
            } else {console.log(action);
                require(path.resolve(__dirname,action))(...process.argv.slice(3))
            }
        })
});

program.on('--help',()=>{console.log('\r\nExamples:');
    Object.keys(mapAction).forEach((action)=>{mapAction[action].examples.forEach(example=>{console.log(' '+example)
        })
    })
});

program.version(version).parse(process.argv);

相关包源码分析

commander 是 tj 大佬写的,包主要依赖着 event、child_process、fs、path、Error 五个 node 核心模块,其中 EventEmitter 的实现更是前端面试中常见的笔试题,具体请看编程题 - 方法库 - 实现 EventEmitter,因此搭建脚手架,node 是基础!

  • 依赖
const EventEmitter = require('events').EventEmitter; // 事件触发与事件监听器功能的封装
const spawn = require('child_process').spawn; // 利用给定的命令以及参数执行一个新的进程
const path = require('path'); // 路径模块
const fs = require('fs'); // 文件模块
  • Option 类
class Option {constructor(flags, description) {
    this.flags = flags;
    this.required = flags.indexOf('<') >= 0; 
    this.optional = flags.indexOf('[') >= 0; 
    this.mandatory = false; 
    this.negate = flags.indexOf('-no-') !== -1;
    const flagParts = flags.split(/[ ,|]+/);
    if (flagParts.length > 1 && !/^[[<]/.test(flagParts[1])) this.short = flagParts.shift();
    this.long = flagParts.shift();
    this.description = description || '';
    this.defaultValue = undefined;
  }

  name() {return this.long.replace(/^--/, '');
  };

 
  attributeName() {return camelcase(this.name().replace(/^no-/, ''));
  };

  is(arg) {return this.short === arg || this.long === arg;};
}
  • CommandError 类
class CommanderError extends Error {constructor(exitCode, code, message) {super(message);
    // properly capture stack trace in Node.js
    Error.captureStackTrace(this, this.constructor);
    this.name = this.constructor.name;
    this.code = code;
    this.exitCode = exitCode;
    this.nestedError = undefined;
  }
}
  • Command 类
class Command extends EventEmitter {constructor(name) {super();
    this.commands = [];
    this.options = [];
    this.parent = null;
    this._allowUnknownOption = false;
    this._args = [];
    this.rawArgs = null;
    this._scriptPath = null;
    this._name = name || '';
    this._optionValues = {};
    this._storeOptionsAsProperties = true; 
    this._passCommandToAction = true;
    this._actionResults = [];
    this._actionHandler = null;
    this._executableHandler = false;
    this._executableFile = null; 
    this._defaultCommandName = null;
    this._exitCallback = null;
    this._aliases = [];

    this._hidden = false;
    this._helpFlags = '-h, --help';
    this._helpDescription = 'display help for command';
    this._helpShortFlag = '-h';
    this._helpLongFlag = '--help';
    this._hasImplicitHelpCommand = undefined; 
    this._helpCommandName = 'help';
    this._helpCommandnameAndArgs = 'help [command]';
    this._helpCommandDescription = 'display help for command';
  }

  command(nameAndArgs, actionOptsOrExecDesc, execOpts) {
    let desc = actionOptsOrExecDesc;
    let opts = execOpts;
    if (typeof desc === 'object' && desc !== null) {
      opts = desc;
      desc = null;
    }
    opts = opts || {};
    const args = nameAndArgs.split(/ +/);
    const cmd = this.createCommand(args.shift());

    if (desc) {cmd.description(desc);
      cmd._executableHandler = true;
    }
    if (opts.isDefault) this._defaultCommandName = cmd._name;

    cmd._hidden = !!(opts.noHelp || opts.hidden);
    cmd._helpFlags = this._helpFlags;
    cmd._helpDescription = this._helpDescription;
    cmd._helpShortFlag = this._helpShortFlag;
    cmd._helpLongFlag = this._helpLongFlag;
    cmd._helpCommandName = this._helpCommandName;
    cmd._helpCommandnameAndArgs = this._helpCommandnameAndArgs;
    cmd._helpCommandDescription = this._helpCommandDescription;
    cmd._exitCallback = this._exitCallback;
    cmd._storeOptionsAsProperties = this._storeOptionsAsProperties;
    cmd._passCommandToAction = this._passCommandToAction;

    cmd._executableFile = opts.executableFile || null; 
    this.commands.push(cmd);
    cmd._parseExpectedArgs(args);
    cmd.parent = this;

    if (desc) return this;
    return cmd;
  };


  createCommand(name) {return new Command(name);
  };

  addCommand(cmd, opts) {if (!cmd._name) throw new Error('Command passed to .addCommand() must have a name');

    function checkExplicitNames(commandArray) {commandArray.forEach((cmd) => {if (cmd._executableHandler && !cmd._executableFile) {throw new Error(`Must specify executableFile for deeply nested executable: ${cmd.name()}`);
        }
        checkExplicitNames(cmd.commands);
      });
    }
    checkExplicitNames(cmd.commands);

    opts = opts || {};
    if (opts.isDefault) this._defaultCommandName = cmd._name;
    if (opts.noHelp || opts.hidden) cmd._hidden = true; 
    this.commands.push(cmd);
    cmd.parent = this;
    return this;
  };

  arguments(desc) {return this._parseExpectedArgs(desc.split(/ +/));
  };

  addHelpCommand(enableOrNameAndArgs, description) {if (enableOrNameAndArgs === false) {this._hasImplicitHelpCommand = false;} else {
      this._hasImplicitHelpCommand = true;
      if (typeof enableOrNameAndArgs === 'string') {this._helpCommandName = enableOrNameAndArgs.split(' ')[0];
        this._helpCommandnameAndArgs = enableOrNameAndArgs;
      }
      this._helpCommandDescription = description || this._helpCommandDescription;
    }
    return this;
  };

  _lazyHasImplicitHelpCommand() {if (this._hasImplicitHelpCommand === undefined) {this._hasImplicitHelpCommand = this.commands.length && !this._actionHandler && !this._findCommand('help');
    }
    return this._hasImplicitHelpCommand;
  };

  _parseExpectedArgs(args) {if (!args.length) return;
    args.forEach((arg) => {
      const argDetails = {
        required: false,
        name: '',
        variadic: false
      };

      switch (arg[0]) {
        case '<':
          argDetails.required = true;
          argDetails.name = arg.slice(1, -1);
          break;
        case '[':
          argDetails.name = arg.slice(1, -1);
          break;
      }

      if (argDetails.name.length > 3 && argDetails.name.slice(-3) === '...') {
        argDetails.variadic = true;
        argDetails.name = argDetails.name.slice(0, -3);
      }
      if (argDetails.name) {this._args.push(argDetails);
      }
    });
    this._args.forEach((arg, i) => {if (arg.variadic && i < this._args.length - 1) {throw new Error(`only the last argument can be variadic '${arg.name}'`);
      }
    });
    return this;
  };

  exitOverride(fn) {if (fn) {this._exitCallback = fn;} else {this._exitCallback = (err) => {if (err.code !== 'commander.executeSubCommandAsync') {throw err;} else {}};
    }
    return this;
  };

  _exit(exitCode, code, message) {if (this._exitCallback) {this._exitCallback(new CommanderError(exitCode, code, message));
    }
    process.exit(exitCode);
  };

  action(fn) {const listener = (args) => {
      const expectedArgsCount = this._args.length;
      const actionArgs = args.slice(0, expectedArgsCount);
      if (this._passCommandToAction) {actionArgs[expectedArgsCount] = this;
      } else {actionArgs[expectedArgsCount] = this.opts();}
      if (args.length > expectedArgsCount) {actionArgs.push(args.slice(expectedArgsCount));
      }

      const actionResult = fn.apply(this, actionArgs);
      let rootCommand = this;
      while (rootCommand.parent) {rootCommand = rootCommand.parent;}
      rootCommand._actionResults.push(actionResult);
    };
    this._actionHandler = listener;
    return this;
  };

  _optionEx(config, flags, description, fn, defaultValue) {const option = new Option(flags, description);
    const oname = option.name();
    const name = option.attributeName();
    option.mandatory = !!config.mandatory;

    if (typeof fn !== 'function') {if (fn instanceof RegExp) {
        const regex = fn;
        fn = (val, def) => {const m = regex.exec(val);
          return m ? m[0] : def;
        };
      } else {
        defaultValue = fn;
        fn = null;
      }
    }

    if (option.negate || option.optional || option.required || typeof defaultValue === 'boolean') {if (option.negate) {const positiveLongFlag = option.long.replace(/^--no-/, '--');
        defaultValue = this._findOption(positiveLongFlag) ? this._getOptionValue(name) : true;
      }
      if (defaultValue !== undefined) {this._setOptionValue(name, defaultValue);
        option.defaultValue = defaultValue;
      }
    }

    this.options.push(option);

    this.on('option:' + oname, (val) => {if (val !== null && fn) {val = fn(val, this._getOptionValue(name) === undefined ? defaultValue : this._getOptionValue(name));
      }

      if (typeof this._getOptionValue(name) === 'boolean' || typeof this._getOptionValue(name) === 'undefined') {if (val == null) {
          this._setOptionValue(name, option.negate
            ? false
            : defaultValue || true);
        } else {this._setOptionValue(name, val);
        }
      } else if (val !== null) {this._setOptionValue(name, option.negate ? false : val);
      }
    });

    return this;
  };

  option(flags, description, fn, defaultValue) {return this._optionEx({}, flags, description, fn, defaultValue);
  };

  requiredOption(flags, description, fn, defaultValue) {return this._optionEx({ mandatory: true}, flags, description, fn, defaultValue);
  };

  allowUnknownOption(arg) {this._allowUnknownOption = (arg === undefined) || arg;
    return this;
  };

  storeOptionsAsProperties(value) {this._storeOptionsAsProperties = (value === undefined) || value;
    if (this.options.length) {throw new Error('call .storeOptionsAsProperties() before adding options');
    }
    return this;
  };

  passCommandToAction(value) {this._passCommandToAction = (value === undefined) || value;
    return this;
  };

  _setOptionValue(key, value) {if (this._storeOptionsAsProperties) {this[key] = value;
    } else {this._optionValues[key] = value;
    }
  };

  _getOptionValue(key) {if (this._storeOptionsAsProperties) {return this[key];
    }
    return this._optionValues[key];
  };

  parse(argv, parseOptions) {if (argv !== undefined && !Array.isArray(argv)) {throw new Error('first parameter to parse must be array or undefined');
    }
    parseOptions = parseOptions || {};

    if (argv === undefined) {
      argv = process.argv;
      if (process.versions && process.versions.electron) {parseOptions.from = 'electron';}
    }
    this.rawArgs = argv.slice();

    let userArgs;
    switch (parseOptions.from) {
      case undefined:
      case 'node':
        this._scriptPath = argv[1];
        userArgs = argv.slice(2);
        break;
      case 'electron':
        if (process.defaultApp) {this._scriptPath = argv[1];
          userArgs = argv.slice(2);
        } else {userArgs = argv.slice(1);
        }
        break;
      case 'user':
        userArgs = argv.slice(0);
        break;
      default:
        throw new Error(`unexpected parse option { from: '${parseOptions.from}' }`);
    }
    if (!this._scriptPath && process.mainModule) {this._scriptPath = process.mainModule.filename;}

    this._name = this._name || (this._scriptPath && path.basename(this._scriptPath, path.extname(this._scriptPath)));

    this._parseCommand([], userArgs);

    return this;
  };

  parseAsync(argv, parseOptions) {this.parse(argv, parseOptions);
    return Promise.all(this._actionResults).then(() => this);
  };

  _executeSubCommand(subcommand, args) {args = args.slice();
    let launchWithNode = false;
    const sourceExt = ['.js', '.ts', '.mjs'];

    this._checkForMissingMandatoryOptions();

    const scriptPath = this._scriptPath;

    let baseDir;
    try {const resolvedLink = fs.realpathSync(scriptPath);
      baseDir = path.dirname(resolvedLink);
    } catch (e) {baseDir = '.';}

    let bin = path.basename(scriptPath, path.extname(scriptPath)) + '-' + subcommand._name;
    if (subcommand._executableFile) {bin = subcommand._executableFile;}

    const localBin = path.join(baseDir, bin);
    if (fs.existsSync(localBin)) {bin = localBin;} else {sourceExt.forEach((ext) => {if (fs.existsSync(`${localBin}${ext}`)) {bin = `${localBin}${ext}`;
        }
      });
    }
    launchWithNode = sourceExt.includes(path.extname(bin));

    let proc;
    if (process.platform !== 'win32') {if (launchWithNode) {args.unshift(bin);
        args = incrementNodeInspectorPort(process.execArgv).concat(args);

        proc = spawn(process.argv[0], args, {stdio: 'inherit'});
      } else {proc = spawn(bin, args, { stdio: 'inherit'});
      }
    } else {args.unshift(bin);
      args = incrementNodeInspectorPort(process.execArgv).concat(args);
      proc = spawn(process.execPath, args, { stdio: 'inherit'});
    }

    const signals = ['SIGUSR1', 'SIGUSR2', 'SIGTERM', 'SIGINT', 'SIGHUP'];
    signals.forEach((signal) => {process.on(signal, () => {if (proc.killed === false && proc.exitCode === null) {proc.kill(signal);
        }
      });
    });

    const exitCallback = this._exitCallback;
    if (!exitCallback) {proc.on('close', process.exit.bind(process));
    } else {proc.on('close', () => {exitCallback(new CommanderError(process.exitCode || 0, 'commander.executeSubCommandAsync', '(close)'));
      });
    }
    proc.on('error', (err) => {if (err.code === 'ENOENT') {const executableMissing = `'${bin}' does not exist
 - if '${subcommand._name}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
 - if the default executable name is not suitable, use the executableFile option to supply a custom name`;
        throw new Error(executableMissing);
      } else if (err.code === 'EACCES') {throw new Error(`'${bin}' not executable`);
      }
      if (!exitCallback) {process.exit(1);
      } else {const wrappedError = new CommanderError(1, 'commander.executeSubCommandAsync', '(error)');
        wrappedError.nestedError = err;
        exitCallback(wrappedError);
      }
    });

    this.runningCommand = proc;
  };

  _dispatchSubcommand(commandName, operands, unknown) {const subCommand = this._findCommand(commandName);
    if (!subCommand) this._helpAndError();

    if (subCommand._executableHandler) {this._executeSubCommand(subCommand, operands.concat(unknown));
    } else {subCommand._parseCommand(operands, unknown);
    }
  };

  _parseCommand(operands, unknown) {const parsed = this.parseOptions(unknown);
    operands = operands.concat(parsed.operands);
    unknown = parsed.unknown;
    this.args = operands.concat(unknown);

    if (operands && this._findCommand(operands[0])) {this._dispatchSubcommand(operands[0], operands.slice(1), unknown);
    } else if (this._lazyHasImplicitHelpCommand() && operands[0] === this._helpCommandName) {if (operands.length === 1) {this.help();
      } else {this._dispatchSubcommand(operands[1], [], [this._helpLongFlag]);
      }
    } else if (this._defaultCommandName) {outputHelpIfRequested(this, unknown); 
      this._dispatchSubcommand(this._defaultCommandName, operands, unknown);
    } else {if (this.commands.length && this.args.length === 0 && !this._actionHandler && !this._defaultCommandName) {this._helpAndError();
      }

      outputHelpIfRequested(this, parsed.unknown);
      this._checkForMissingMandatoryOptions();
      if (parsed.unknown.length > 0) {this.unknownOption(parsed.unknown[0]);
      }

      if (this._actionHandler) {const args = this.args.slice();
        this._args.forEach((arg, i) => {if (arg.required && args[i] == null) {this.missingArgument(arg.name);
          } else if (arg.variadic) {args[i] = args.splice(i);
          }
        });

        this._actionHandler(args);
        this.emit('command:' + this.name(), operands, unknown);
      } else if (operands.length) {if (this._findCommand('*')) {this._dispatchSubcommand('*', operands, unknown);
        } else if (this.listenerCount('command:*')) {this.emit('command:*', operands, unknown);
        } else if (this.commands.length) {this.unknownCommand();
        }
      } else if (this.commands.length) {this._helpAndError();
      } else {}}
  };

  _findCommand(name) {if (!name) return undefined;
    return this.commands.find(cmd => cmd._name === name || cmd._aliases.includes(name));
  };

  _findOption(arg) {return this.options.find(option => option.is(arg));
  };

  _checkForMissingMandatoryOptions() {for (let cmd = this; cmd; cmd = cmd.parent) {cmd.options.forEach((anOption) => {if (anOption.mandatory && (cmd._getOptionValue(anOption.attributeName()) === undefined)) {cmd.missingMandatoryOptionValue(anOption);
        }
      });
    }
  };

  parseOptions(argv) {const operands = []; 
    const unknown = []; 
    let dest = operands;
    const args = argv.slice();

    function maybeOption(arg) {return arg.length > 1 && arg[0] === '-';
    }

    while (args.length) {const arg = args.shift();

      if (arg === '--') {if (dest === unknown) dest.push(arg);
        dest.push(...args);
        break;
      }

      if (maybeOption(arg)) {const option = this._findOption(arg);
        if (option) {if (option.required) {const value = args.shift();
            if (value === undefined) this.optionMissingArgument(option);
            this.emit(`option:${option.name()}`, value);
          } else if (option.optional) {
            let value = null;
            if (args.length > 0 && !maybeOption(args[0])) {value = args.shift();
            }
            this.emit(`option:${option.name()}`, value);
          } else {this.emit(`option:${option.name()}`);
          }
          continue;
        }
      }

      if (arg.length > 2 && arg[0] === '-' && arg[1] !== '-') {const option = this._findOption(`-${arg[1]}`);
        if (option) {if (option.required || option.optional) {this.emit(`option:${option.name()}`, arg.slice(2));
          } else {this.emit(`option:${option.name()}`);
            args.unshift(`-${arg.slice(2)}`);
          }
          continue;
        }
      }

      if (/^--[^=]+=/.test(arg)) {const index = arg.indexOf('=');
        const option = this._findOption(arg.slice(0, index));
        if (option && (option.required || option.optional)) {this.emit(`option:${option.name()}`, arg.slice(index + 1));
          continue;
        }
      }

      if (arg.length > 1 && arg[0] === '-') {dest = unknown;}

      dest.push(arg);
    }

    return {operands, unknown};
  };

  opts() {if (this._storeOptionsAsProperties) {const result = {};
      const len = this.options.length;

      for (let i = 0; i < len; i++) {const key = this.options[i].attributeName();
        result[key] = key === this._versionOptionName ? this._version : this[key];
      }
      return result;
    }

    return this._optionValues;
  };

  missingArgument(name) {const message = `error: missing required argument '${name}'`;
    console.error(message);
    this._exit(1, 'commander.missingArgument', message);
  };

  optionMissingArgument(option, flag) {
    let message;
    if (flag) {message = `error: option '${option.flags}' argument missing, got '${flag}'`;
    } else {message = `error: option '${option.flags}' argument missing`;
    }
    console.error(message);
    this._exit(1, 'commander.optionMissingArgument', message);
  };

  missingMandatoryOptionValue(option) {const message = `error: required option '${option.flags}' not specified`;
    console.error(message);
    this._exit(1, 'commander.missingMandatoryOptionValue', message);
  };

  unknownOption(flag) {if (this._allowUnknownOption) return;
    const message = `error: unknown option '${flag}'`;
    console.error(message);
    this._exit(1, 'commander.unknownOption', message);
  };

  unknownCommand() {const partCommands = [this.name()];
    for (let parentCmd = this.parent; parentCmd; parentCmd = parentCmd.parent) {partCommands.unshift(parentCmd.name());
    }
    const fullCommand = partCommands.join(' ');
    const message = `error: unknown command '${this.args[0]}'. See '${fullCommand} ${this._helpLongFlag}'.`;
    console.error(message);
    this._exit(1, 'commander.unknownCommand', message);
  };

  version(str, flags, description) {if (str === undefined) return this._version;
    this._version = str;
    flags = flags || '-V, --version';
    description = description || 'output the version number';
    const versionOption = new Option(flags, description);
    this._versionOptionName = versionOption.long.substr(2) || 'version';
    this.options.push(versionOption);
    this.on('option:' + this._versionOptionName, () => {process.stdout.write(str + '\n');
      this._exit(0, 'commander.version', str);
    });
    return this;
  };

  description(str, argsDescription) {if (str === undefined && argsDescription === undefined) return this._description;
    this._description = str;
    this._argsDescription = argsDescription;
    return this;
  };

  alias(alias) {if (alias === undefined) return this._aliases[0];
    let command = this;
    if (this.commands.length !== 0 && this.commands[this.commands.length - 1]._executableHandler) {command = this.commands[this.commands.length - 1];
    }

    if (alias === command._name) throw new Error('Command alias can\'t be the same as its name');

    command._aliases.push(alias);
    return this;
  };

  aliases(aliases) {if (aliases === undefined) return this._aliases;

    aliases.forEach((alias) => this.alias(alias));
    return this;
  };

  usage(str) {if (str === undefined) {if (this._usage) return this._usage;

      const args = this._args.map((arg) => {return humanReadableArgName(arg);
      });
      return '[options]' +
        (this.commands.length ? '[command]' : '') +
        (this._args.length ? '' + args.join(' ') :'');
    }

    this._usage = str;
    return this;
  };

  name(str) {if (str === undefined) return this._name;
    this._name = str;
    return this;
  };

  prepareCommands() {const commandDetails = this.commands.filter((cmd) => {return !cmd._hidden;}).map((cmd) => {const args = cmd._args.map((arg) => {return humanReadableArgName(arg);
      }).join(' ');

      return [
        cmd._name +
          (cmd._aliases[0] ? '|' + cmd._aliases[0] : '') +
          (cmd.options.length ? '[options]' : '') +
          (args ? '' + args :''),
        cmd._description
      ];
    });

    if (this._lazyHasImplicitHelpCommand()) {commandDetails.push([this._helpCommandnameAndArgs, this._helpCommandDescription]);
    }
    return commandDetails;
  };

  largestCommandLength() {const commands = this.prepareCommands();
    return commands.reduce((max, command) => {return Math.max(max, command[0].length);
    }, 0);
  };

  largestOptionLength() {const options = [].slice.call(this.options);
    options.push({flags: this._helpFlags});

    return options.reduce((max, option) => {return Math.max(max, option.flags.length);
    }, 0);
  };

  largestArgLength() {return this._args.reduce((max, arg) => {return Math.max(max, arg.name.length);
    }, 0);
  };

  padWidth() {let width = this.largestOptionLength();
    if (this._argsDescription && this._args.length) {if (this.largestArgLength() > width) {width = this.largestArgLength();
      }
    }

    if (this.commands && this.commands.length) {if (this.largestCommandLength() > width) {width = this.largestCommandLength();
      }
    }

    return width;
  };

  optionHelp() {const width = this.padWidth();
    const columns = process.stdout.columns || 80;
    const descriptionWidth = columns - width - 4;
    function padOptionDetails(flags, description) {return pad(flags, width) + ' ' + optionalWrap(description, descriptionWidth, width + 2);
    };

    const help = this.options.map((option) => {
      const fullDesc = option.description +
        ((!option.negate && option.defaultValue !== undefined) ? '(default:' + JSON.stringify(option.defaultValue) + ')' : '');
      return padOptionDetails(option.flags, fullDesc);
    });

    const showShortHelpFlag = this._helpShortFlag && !this._findOption(this._helpShortFlag);
    const showLongHelpFlag = !this._findOption(this._helpLongFlag);
    if (showShortHelpFlag || showLongHelpFlag) {
      let helpFlags = this._helpFlags;
      if (!showShortHelpFlag) {helpFlags = this._helpLongFlag;} else if (!showLongHelpFlag) {helpFlags = this._helpShortFlag;}
      help.push(padOptionDetails(helpFlags, this._helpDescription));
    }

    return help.join('\n');
  };

  commandHelp() {if (!this.commands.length && !this._lazyHasImplicitHelpCommand()) return '';

    const commands = this.prepareCommands();
    const width = this.padWidth();

    const columns = process.stdout.columns || 80;
    const descriptionWidth = columns - width - 4;

    return [
      'Commands:',
      commands.map((cmd) => {const desc = cmd[1] ? '' + cmd[1] :'';
        return (desc ? pad(cmd[0], width) : cmd[0]) + optionalWrap(desc, descriptionWidth, width + 2);
      }).join('\n').replace(/^/gm, ''),''
    ].join('\n');
  };

  helpInformation() {let desc = [];
    if (this._description) {
      desc = [
        this._description,
        ''
      ];

      const argsDescription = this._argsDescription;
      if (argsDescription && this._args.length) {const width = this.padWidth();
        const columns = process.stdout.columns || 80;
        const descriptionWidth = columns - width - 5;
        desc.push('Arguments:');
        desc.push('');
        this._args.forEach((arg) => {desc.push('' + pad(arg.name, width) +'  ' + wrap(argsDescription[arg.name], descriptionWidth, width + 4));
        });
        desc.push('');
      }
    }

    let cmdName = this._name;
    if (this._aliases[0]) {cmdName = cmdName + '|' + this._aliases[0];
    }
    let parentCmdNames = '';
    for (let parentCmd = this.parent; parentCmd; parentCmd = parentCmd.parent) {parentCmdNames = parentCmd.name() + ' ' + parentCmdNames;
    }
    const usage = ['Usage:' + parentCmdNames + cmdName + '' + this.usage(),''
    ];

    let cmds = [];
    const commandHelp = this.commandHelp();
    if (commandHelp) cmds = [commandHelp];

    const options = [
      'Options:',
      ''+ this.optionHelp().replace(/^/gm,'  '),''
    ];

    return usage
      .concat(desc)
      .concat(options)
      .concat(cmds)
      .join('\n');
  };

  outputHelp(cb) {if (!cb) {cb = (passthru) => {return passthru;};
    }
    const cbOutput = cb(this.helpInformation());
    if (typeof cbOutput !== 'string' && !Buffer.isBuffer(cbOutput)) {throw new Error('outputHelp callback must return a string or a Buffer');
    }
    process.stdout.write(cbOutput);
    this.emit(this._helpLongFlag);
  };

  helpOption(flags, description) {
    this._helpFlags = flags || this._helpFlags;
    this._helpDescription = description || this._helpDescription;

    const splitFlags = this._helpFlags.split(/[ ,|]+/);

    this._helpShortFlag = undefined;
    if (splitFlags.length > 1) this._helpShortFlag = splitFlags.shift();

    this._helpLongFlag = splitFlags.shift();

    return this;
  };

  help(cb) {this.outputHelp(cb);
    this._exit(process.exitCode || 0, 'commander.help', '(outputHelp)');
  };

  _helpAndError() {this.outputHelp();
    this._exit(1, 'commander.help', '(outputHelp)');
  };
};
  • 导出及函数方法
exports = module.exports = new Command();
exports.program = exports; 
exports.Command = Command;
exports.Option = Option;
exports.CommanderError = CommanderError;

function camelcase(flag) {return flag.split('-').reduce((str, word) => {return str + word[0].toUpperCase() + word.slice(1);
  });
}

function pad(str, width) {const len = Math.max(0, width - str.length);
  return str + Array(len + 1).join(' ');
}

function wrap(str, width, indent) {const regex = new RegExp('.{1,' + (width - 1) + '}([\\s\u200B]|$)|[^\\s\u200B]+?([\\s\u200B]|$)', 'g');
  const lines = str.match(regex) || [];
  return lines.map((line, i) => {if (line.slice(-1) === '\n') {line = line.slice(0, line.length - 1);
    }
    return ((i > 0 && indent) ? Array(indent + 1).join('') :'') + line.trimRight();}).join('\n');
}

function optionalWrap(str, width, indent) {if (str.match(/[\n]\s+/)) return str;
  const minWidth = 40;
  if (width < minWidth) return str;
  return wrap(str, width, indent);
}

function outputHelpIfRequested(cmd, args) {const helpOption = args.find(arg => arg === cmd._helpLongFlag || arg === cmd._helpShortFlag);
  if (helpOption) {cmd.outputHelp();
    cmd._exit(0, 'commander.helpDisplayed', '(outputHelp)');
  }
}

function humanReadableArgName(arg) {const nameOutput = arg.name + (arg.variadic === true ? '...' : '');
  return arg.required
    ? '<' + nameOutput + '>'
    : '[' + nameOutput + ']';
}

function incrementNodeInspectorPort(args) {return args.map((arg) => {
    let result = arg;
    if (arg.indexOf('--inspect') === 0) {
      let debugOption;
      let debugHost = '127.0.0.1';
      let debugPort = '9229';
      let match;
      if ((match = arg.match(/^(--inspect(-brk)?)$/)) !== null) {debugOption = match[1];
      } else if ((match = arg.match(/^(--inspect(-brk|-port)?)=([^:]+)$/)) !== null) {debugOption = match[1];
        if (/^\d+$/.test(match[3])) {debugPort = match[3];
        } else {debugHost = match[3];
        }
      } else if ((match = arg.match(/^(--inspect(-brk|-port)?)=([^:]+):(\d+)$/)) !== null) {debugOption = match[1];
        debugHost = match[3];
        debugPort = match[4];
      }

      if (debugOption && debugPort !== '0') {result = `${debugOption}=${debugHost}:${parseInt(debugPort) + 1}`;
      }
    }
    return result;
  });
}

总结

本篇主要描述的是自主搭建脚手架的基本配置以及基本命令的配置上,具体的各种命令配置后事如何,且听下回分解

未完待续 …

参考

  • vue-cli 官网
  • nodejs 官网
  • commander.js 源码
  • commander 源码解读
  • Node.js 中 spawn 与 exec 的异同比较
退出移动版