/** * @license * Copyright (c) 2016 The Polymer Project Authors. All rights reserved. * This code may only be used under the BSD style license found at * http://polymer.github.io/LICENSE.txt * The complete set of authors may be found at * http://polymer.github.io/AUTHORS.txt * The complete set of contributors may be found at * http://polymer.github.io/CONTRIBUTORS.txt * Code distributed by Google as part of the polymer project is also * subject to an additional IP rights grant found at * http://polymer.github.io/PATENTS.txt */ // Be mindful of adding imports here, as this is on the hot path of all // commands. import * as commandLineArgs from 'command-line-args'; import {sep as pathSeperator} from 'path'; import * as logging from 'plylog'; import {ProjectConfig, ProjectOptions} from 'polymer-project-config'; import {globalArguments, mergeArguments} from './args'; import {AnalyzeCommand} from './commands/analyze'; import {BuildCommand} from './commands/build'; import {Command} from './commands/command'; import {HelpCommand} from './commands/help'; import {InitCommand} from './commands/init'; import {InstallCommand} from './commands/install'; import {LintCommand} from './commands/lint'; import {ServeCommand} from './commands/serve'; import {TestCommand} from './commands/test'; import {dashToCamelCase} from './util'; import commandLineCommands = require('command-line-commands'); import {ParsedCommand} from 'command-line-commands'; const logger = logging.getLogger('cli.main'); process.on('uncaughtException', (error: null|undefined|Partial) => { logger.error(`Uncaught exception: ${error}`); if (error && error.stack) logger.error(error.stack); process.exit(1); }); process.on('unhandledRejection', (error: null|undefined|Partial) => { logger.error(`Promise rejection: ${error}`); if (error && error.stack) logger.error(error.stack); process.exit(1); }); /** * CLI arguments are in "hyphen-case" format, but our configuration is in * "lowerCamelCase". This helper function converts the special * `command-line-args` data format (with its hyphen-case flags) to an easier to * use options object with lowerCamelCase properties. */ // tslint:disable-next-line: no-any Super hacky scary code. function parseCLIArgs(commandOptions: any): {[name: string]: string} { commandOptions = commandOptions && commandOptions['_all']; const parsedOptions = Object.assign({}, commandOptions); if (commandOptions['extra-dependencies']) { parsedOptions.extraDependencies = commandOptions['extra-dependencies']; } if (commandOptions.fragment) { parsedOptions.fragments = commandOptions.fragment; } return parsedOptions; } /** * Shallowly copies an object, converting keys from dash-case to camelCase. */ function objectDashToCamelCase(input: {[key: string]: V}) { const output: {[key: string]: V} = {}; for (const key of Object.keys(input)) { output[dashToCamelCase(key)] = input[key]; } return output; } export class PolymerCli { commands: Map = new Map(); args: string[]; defaultConfigOptions: ProjectOptions; constructor(args: string[], configOptions?: ProjectOptions) { // If the "--quiet"/"-q" flag is ever present, set our global logging // to quiet mode. Also set the level on the logger we've already created. if (args.indexOf('--quiet') > -1 || args.indexOf('-q') > -1) { logging.setQuiet(); } // If the "--verbose"/"-v" flag is ever present, set our global logging // to verbose mode. Also set the level on the logger we've already created. if (args.indexOf('--verbose') > -1 || args.indexOf('-v') > -1) { logging.setVerbose(); } this.args = args; logger.debug('got args:', {args: args}); if (typeof configOptions !== 'undefined') { this.defaultConfigOptions = configOptions; logger.debug( 'got default config from constructor argument:', {config: this.defaultConfigOptions}); } else { this.defaultConfigOptions = ProjectConfig.loadOptionsFromFile('polymer.json')!; if (this.defaultConfigOptions) { logger.debug( 'got default config from polymer.json file:', {config: this.defaultConfigOptions}); } else { logger.debug('no polymer.json file found, no config loaded'); } } // This is a quick fix to make sure that "webcomponentsjs" files are // included in every build, since some are imported dynamically in a way // that our analyzer cannot detect. // TODO(fks) 03-07-2017: Remove/refactor when we have a better plan for // support (either here or inside of polymer-project-config). this.defaultConfigOptions = this.defaultConfigOptions || {}; this.defaultConfigOptions.extraDependencies = this.defaultConfigOptions.extraDependencies || []; this.defaultConfigOptions.extraDependencies.unshift( `bower_components${pathSeperator}webcomponentsjs${pathSeperator}*.js`); this.addCommand(new AnalyzeCommand()); this.addCommand(new BuildCommand()); this.addCommand(new HelpCommand(this.commands)); this.addCommand(new InitCommand()); this.addCommand(new InstallCommand()); this.addCommand(new LintCommand()); this.addCommand(new ServeCommand()); this.addCommand(new TestCommand()); } addCommand(command: Command) { logger.debug('adding command', command.name); this.commands.set(command.name, command); command.aliases.forEach((alias) => { logger.debug('adding alias', alias); this.commands.set(alias, command); }); } async run() { const helpCommand = this.commands.get('help')!; const commandNames = Array.from(this.commands.keys()); let parsedArgs: ParsedCommand; logger.debug('running...'); // If the "--version" flag is ever present, just print // the current version. Useful for globally installed CLIs. if (this.args.indexOf('--version') > -1) { console.log(require('../package.json').version); return Promise.resolve(); } try { parsedArgs = commandLineCommands(commandNames, this.args); } catch (error) { // Polymer CLI needs a valid command name to do anything. If the given // command is invalid, run the generalized help command with default // config. This should print the general usage information. if (error.name === 'INVALID_COMMAND') { if (error.command) { logger.warn(`'${error.command}' is not an available command.`); } return helpCommand.run( {command: error.command}, new ProjectConfig(this.defaultConfigOptions)); } // If an unexpected error occurred, propagate it throw error; } const commandName = parsedArgs.command; const commandArgs = parsedArgs.argv; const command = this.commands.get(commandName)!; if (command == null) throw new TypeError('command is null'); logger.debug( `command '${commandName}' found, parsing command args:`, {args: commandArgs}); const commandDefinitions = mergeArguments([command.args, globalArguments]); const commandOptionsRaw = commandLineArgs(commandDefinitions, {argv: commandArgs}); const commandOptions = parseCLIArgs(commandOptionsRaw); logger.debug(`command options parsed from args:`, commandOptions); const mergedConfigOptions = { ...this.defaultConfigOptions, ...objectDashToCamelCase(commandOptions), }; logger.debug(`final config options:`, mergedConfigOptions); const config = new ProjectConfig(mergedConfigOptions); logger.debug(`final project configuration generated:`, config); // Help is a special argument for displaying help for the given command. // If found, run the help command instead, with the given command name as // an option. if (commandOptions['help']) { logger.debug( `'--help' option found, running 'help' for given command...`); return helpCommand.run({command: commandName}, config); } logger.debug('Running command...'); return command.run(commandOptions, config); } }