UNPKG

8.21 kBPlain TextView Raw
1/**
2 * @license
3 * Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
4 * This code may only be used under the BSD style license found at
5 * http://polymer.github.io/LICENSE.txt
6 * The complete set of authors may be found at
7 * http://polymer.github.io/AUTHORS.txt
8 * The complete set of contributors may be found at
9 * http://polymer.github.io/CONTRIBUTORS.txt
10 * Code distributed by Google as part of the polymer project is also
11 * subject to an additional IP rights grant found at
12 * http://polymer.github.io/PATENTS.txt
13 */
14
15// Be mindful of adding imports here, as this is on the hot path of all
16// commands.
17
18import * as commandLineArgs from 'command-line-args';
19import {sep as pathSeperator} from 'path';
20import * as logging from 'plylog';
21import {ProjectConfig, ProjectOptions} from 'polymer-project-config';
22
23import {globalArguments, mergeArguments} from './args';
24import {AnalyzeCommand} from './commands/analyze';
25import {BuildCommand} from './commands/build';
26import {Command} from './commands/command';
27import {HelpCommand} from './commands/help';
28import {InitCommand} from './commands/init';
29import {InstallCommand} from './commands/install';
30import {LintCommand} from './commands/lint';
31import {ServeCommand} from './commands/serve';
32import {TestCommand} from './commands/test';
33import {dashToCamelCase} from './util';
34
35import commandLineCommands = require('command-line-commands');
36import {ParsedCommand} from 'command-line-commands';
37
38const logger = logging.getLogger('cli.main');
39
40process.on('uncaughtException', (error: null|undefined|Partial<Error>) => {
41 logger.error(`Uncaught exception: ${error}`);
42 if (error && error.stack)
43 logger.error(error.stack);
44 process.exit(1);
45});
46
47process.on('unhandledRejection', (error: null|undefined|Partial<Error>) => {
48 logger.error(`Promise rejection: ${error}`);
49 if (error && error.stack)
50 logger.error(error.stack);
51 process.exit(1);
52});
53
54/**
55 * CLI arguments are in "hyphen-case" format, but our configuration is in
56 * "lowerCamelCase". This helper function converts the special
57 * `command-line-args` data format (with its hyphen-case flags) to an easier to
58 * use options object with lowerCamelCase properties.
59 */
60// tslint:disable-next-line: no-any Super hacky scary code.
61function parseCLIArgs(commandOptions: any): {[name: string]: string} {
62 commandOptions = commandOptions && commandOptions['_all'];
63 const parsedOptions = Object.assign({}, commandOptions);
64
65 if (commandOptions['extra-dependencies']) {
66 parsedOptions.extraDependencies = commandOptions['extra-dependencies'];
67 }
68 if (commandOptions.fragment) {
69 parsedOptions.fragments = commandOptions.fragment;
70 }
71
72 return parsedOptions;
73}
74
75/**
76 * Shallowly copies an object, converting keys from dash-case to camelCase.
77 */
78function objectDashToCamelCase<V>(input: {[key: string]: V}) {
79 const output: {[key: string]: V} = {};
80 for (const key of Object.keys(input)) {
81 output[dashToCamelCase(key)] = input[key];
82 }
83 return output;
84}
85
86
87export class PolymerCli {
88 commands: Map<string, Command> = new Map();
89 args: string[];
90 defaultConfigOptions: ProjectOptions;
91
92 constructor(args: string[], configOptions?: ProjectOptions) {
93 // If the "--quiet"/"-q" flag is ever present, set our global logging
94 // to quiet mode. Also set the level on the logger we've already created.
95 if (args.indexOf('--quiet') > -1 || args.indexOf('-q') > -1) {
96 logging.setQuiet();
97 }
98
99 // If the "--verbose"/"-v" flag is ever present, set our global logging
100 // to verbose mode. Also set the level on the logger we've already created.
101 if (args.indexOf('--verbose') > -1 || args.indexOf('-v') > -1) {
102 logging.setVerbose();
103 }
104
105 this.args = args;
106 logger.debug('got args:', {args: args});
107
108 if (typeof configOptions !== 'undefined') {
109 this.defaultConfigOptions = configOptions;
110 logger.debug(
111 'got default config from constructor argument:',
112 {config: this.defaultConfigOptions});
113 } else {
114 this.defaultConfigOptions =
115 ProjectConfig.loadOptionsFromFile('polymer.json')!;
116 if (this.defaultConfigOptions) {
117 logger.debug(
118 'got default config from polymer.json file:',
119 {config: this.defaultConfigOptions});
120 } else {
121 logger.debug('no polymer.json file found, no config loaded');
122 }
123 }
124
125 // This is a quick fix to make sure that "webcomponentsjs" files are
126 // included in every build, since some are imported dynamically in a way
127 // that our analyzer cannot detect.
128 // TODO(fks) 03-07-2017: Remove/refactor when we have a better plan for
129 // support (either here or inside of polymer-project-config).
130 this.defaultConfigOptions = this.defaultConfigOptions || {};
131 this.defaultConfigOptions.extraDependencies =
132 this.defaultConfigOptions.extraDependencies || [];
133 this.defaultConfigOptions.extraDependencies.unshift(
134 `bower_components${pathSeperator}webcomponentsjs${pathSeperator}*.js`);
135
136 this.addCommand(new AnalyzeCommand());
137 this.addCommand(new BuildCommand());
138 this.addCommand(new HelpCommand(this.commands));
139 this.addCommand(new InitCommand());
140 this.addCommand(new InstallCommand());
141 this.addCommand(new LintCommand());
142 this.addCommand(new ServeCommand());
143 this.addCommand(new TestCommand());
144 }
145
146 addCommand(command: Command) {
147 logger.debug('adding command', command.name);
148 this.commands.set(command.name, command);
149
150 command.aliases.forEach((alias) => {
151 logger.debug('adding alias', alias);
152 this.commands.set(alias, command);
153 });
154 }
155
156 async run() {
157 const helpCommand = this.commands.get('help')!;
158 const commandNames = Array.from(this.commands.keys());
159 let parsedArgs: ParsedCommand;
160 logger.debug('running...');
161
162 // If the "--version" flag is ever present, just print
163 // the current version. Useful for globally installed CLIs.
164 if (this.args.indexOf('--version') > -1) {
165 console.log(require('../package.json').version);
166 return Promise.resolve();
167 }
168
169 try {
170 parsedArgs = commandLineCommands(commandNames, this.args);
171 } catch (error) {
172 // Polymer CLI needs a valid command name to do anything. If the given
173 // command is invalid, run the generalized help command with default
174 // config. This should print the general usage information.
175 if (error.name === 'INVALID_COMMAND') {
176 if (error.command) {
177 logger.warn(`'${error.command}' is not an available command.`);
178 }
179 return helpCommand.run(
180 {command: error.command},
181 new ProjectConfig(this.defaultConfigOptions));
182 }
183 // If an unexpected error occurred, propagate it
184 throw error;
185 }
186
187 const commandName = parsedArgs.command;
188 const commandArgs = parsedArgs.argv;
189 const command = this.commands.get(commandName)!;
190 if (command == null)
191 throw new TypeError('command is null');
192
193 logger.debug(
194 `command '${commandName}' found, parsing command args:`,
195 {args: commandArgs});
196
197 const commandDefinitions = mergeArguments([command.args, globalArguments]);
198 const commandOptionsRaw =
199 commandLineArgs(commandDefinitions, {argv: commandArgs});
200 const commandOptions = parseCLIArgs(commandOptionsRaw);
201 logger.debug(`command options parsed from args:`, commandOptions);
202
203 const mergedConfigOptions = {
204 ...this.defaultConfigOptions,
205 ...objectDashToCamelCase(commandOptions),
206 };
207 logger.debug(`final config options:`, mergedConfigOptions);
208
209 const config = new ProjectConfig(mergedConfigOptions);
210 logger.debug(`final project configuration generated:`, config);
211
212 // Help is a special argument for displaying help for the given command.
213 // If found, run the help command instead, with the given command name as
214 // an option.
215 if (commandOptions['help']) {
216 logger.debug(
217 `'--help' option found, running 'help' for given command...`);
218 return helpCommand.run({command: commandName}, config);
219 }
220
221 logger.debug('Running command...');
222 return command.run(commandOptions, config);
223 }
224}