UNPKG

8.23 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 }
45 process.exit(1);
46});
47
48process.on('unhandledRejection', (error: null|undefined|Partial<Error>) => {
49 logger.error(`Promise rejection: ${error}`);
50 if (error && error.stack) {
51 logger.error(error.stack);
52 }
53 process.exit(1);
54});
55
56/**
57 * CLI arguments are in "hyphen-case" format, but our configuration is in
58 * "lowerCamelCase". This helper function converts the special
59 * `command-line-args` data format (with its hyphen-case flags) to an easier to
60 * use options object with lowerCamelCase properties.
61 */
62// tslint:disable-next-line: no-any Super hacky scary code.
63function parseCLIArgs(commandOptions: any): {[name: string]: string} {
64 commandOptions = commandOptions && commandOptions['_all'];
65 const parsedOptions = Object.assign({}, commandOptions);
66
67 if (commandOptions['extra-dependencies']) {
68 parsedOptions.extraDependencies = commandOptions['extra-dependencies'];
69 }
70 if (commandOptions.fragment) {
71 parsedOptions.fragments = commandOptions.fragment;
72 }
73
74 return parsedOptions;
75}
76
77/**
78 * Shallowly copies an object, converting keys from dash-case to camelCase.
79 */
80function objectDashToCamelCase<V>(input: {[key: string]: V}) {
81 const output: {[key: string]: V} = {};
82 for (const key of Object.keys(input)) {
83 output[dashToCamelCase(key)] = input[key];
84 }
85 return output;
86}
87
88
89export class PolymerCli {
90 commands: Map<string, Command> = new Map();
91 args: string[];
92 defaultConfigOptions: ProjectOptions;
93
94 constructor(args: string[], configOptions?: ProjectOptions) {
95 // If the "--quiet"/"-q" flag is ever present, set our global logging
96 // to quiet mode. Also set the level on the logger we've already created.
97 if (args.indexOf('--quiet') > -1 || args.indexOf('-q') > -1) {
98 logging.setQuiet();
99 }
100
101 // If the "--verbose"/"-v" flag is ever present, set our global logging
102 // to verbose mode. Also set the level on the logger we've already created.
103 if (args.indexOf('--verbose') > -1 || args.indexOf('-v') > -1) {
104 logging.setVerbose();
105 }
106
107 this.args = args;
108 logger.debug('got args:', {args: args});
109
110 if (typeof configOptions !== 'undefined') {
111 this.defaultConfigOptions = configOptions;
112 logger.debug(
113 'got default config from constructor argument:',
114 {config: this.defaultConfigOptions});
115 } else {
116 this.defaultConfigOptions =
117 ProjectConfig.loadOptionsFromFile('polymer.json')!;
118 if (this.defaultConfigOptions) {
119 logger.debug(
120 'got default config from polymer.json file:',
121 {config: this.defaultConfigOptions});
122 } else {
123 logger.debug('no polymer.json file found, no config loaded');
124 }
125 }
126
127 // This is a quick fix to make sure that "webcomponentsjs" files are
128 // included in every build, since some are imported dynamically in a way
129 // that our analyzer cannot detect.
130 // TODO(fks) 03-07-2017: Remove/refactor when we have a better plan for
131 // support (either here or inside of polymer-project-config).
132 this.defaultConfigOptions = this.defaultConfigOptions || {};
133 this.defaultConfigOptions.extraDependencies =
134 this.defaultConfigOptions.extraDependencies || [];
135 this.defaultConfigOptions.extraDependencies.unshift(
136 `bower_components${pathSeperator}webcomponentsjs${pathSeperator}*.js`);
137
138 this.addCommand(new AnalyzeCommand());
139 this.addCommand(new BuildCommand());
140 this.addCommand(new HelpCommand(this.commands));
141 this.addCommand(new InitCommand());
142 this.addCommand(new InstallCommand());
143 this.addCommand(new LintCommand());
144 this.addCommand(new ServeCommand());
145 this.addCommand(new TestCommand());
146 }
147
148 addCommand(command: Command) {
149 logger.debug('adding command', command.name);
150 this.commands.set(command.name, command);
151
152 command.aliases.forEach((alias) => {
153 logger.debug('adding alias', alias);
154 this.commands.set(alias, command);
155 });
156 }
157
158 async run() {
159 const helpCommand = this.commands.get('help')!;
160 const commandNames = Array.from(this.commands.keys());
161 let parsedArgs: ParsedCommand;
162 logger.debug('running...');
163
164 // If the "--version" flag is ever present, just print
165 // the current version. Useful for globally installed CLIs.
166 if (this.args.indexOf('--version') > -1) {
167 console.log(require('../package.json').version);
168 return Promise.resolve();
169 }
170
171 try {
172 parsedArgs = commandLineCommands(commandNames, this.args);
173 } catch (error) {
174 // Polymer CLI needs a valid command name to do anything. If the given
175 // command is invalid, run the generalized help command with default
176 // config. This should print the general usage information.
177 if (error.name === 'INVALID_COMMAND') {
178 if (error.command) {
179 logger.warn(`'${error.command}' is not an available command.`);
180 }
181 return helpCommand.run(
182 {command: error.command},
183 new ProjectConfig(this.defaultConfigOptions));
184 }
185 // If an unexpected error occurred, propagate it
186 throw error;
187 }
188
189 const commandName = parsedArgs.command;
190 const commandArgs = parsedArgs.argv;
191 const command = this.commands.get(commandName)!;
192 if (command == null) {
193 throw new TypeError('command is null');
194 }
195
196 logger.debug(
197 `command '${commandName}' found, parsing command args:`,
198 {args: commandArgs});
199
200 const commandDefinitions = mergeArguments([command.args, globalArguments]);
201 const commandOptionsRaw =
202 commandLineArgs(commandDefinitions, {argv: commandArgs});
203 const commandOptions = parseCLIArgs(commandOptionsRaw);
204 logger.debug(`command options parsed from args:`, commandOptions);
205
206 const mergedConfigOptions = {
207 ...this.defaultConfigOptions,
208 ...objectDashToCamelCase(commandOptions),
209 };
210 logger.debug(`final config options:`, mergedConfigOptions);
211
212 const config = new ProjectConfig(mergedConfigOptions);
213 logger.debug(`final project configuration generated:`, config);
214
215 // Help is a special argument for displaying help for the given command.
216 // If found, run the help command instead, with the given command name as
217 // an option.
218 if (commandOptions['help']) {
219 logger.debug(
220 `'--help' option found, running 'help' for given command...`);
221 return helpCommand.run({command: commandName}, config);
222 }
223
224 logger.debug('Running command...');
225 return command.run(commandOptions, config);
226 }
227}