polymer-cli
Version:
A commandline tool for Polymer projects
228 lines (196 loc) • 8.23 kB
text/typescript
/**
* @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<Error>) => {
logger.error(`Uncaught exception: ${error}`);
if (error && error.stack) {
logger.error(error.stack);
}
process.exit(1);
});
process.on('unhandledRejection', (error: null|undefined|Partial<Error>) => {
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<V>(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<string, Command> = 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);
}
}