UNPKG

17.6 kBJavaScriptView Raw
1/**
2 Licensed to the Apache Software Foundation (ASF) under one
3 or more contributor license agreements. See the NOTICE file
4 distributed with this work for additional information
5 regarding copyright ownership. The ASF licenses this file
6 to you under the Apache License, Version 2.0 (the
7 "License"); you may not use this file except in compliance
8 with the License. You may obtain a copy of the License at
9 http://www.apache.org/licenses/LICENSE-2.0
10 Unless required by applicable law or agreed to in writing,
11 software distributed under the License is distributed on an
12 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13 KIND, either express or implied. See the License for the
14 specific language governing permissions and limitations
15 under the License.
16*/
17
18var path = require('path');
19var nopt = require('nopt');
20var updateNotifier = require('update-notifier');
21var pkg = require('../package.json');
22var telemetry = require('./telemetry');
23var help = require('./help');
24var cordova_lib = require('cordova-lib');
25var CordovaError = cordova_lib.CordovaError;
26var cordova = cordova_lib.cordova;
27var events = cordova_lib.events;
28var logger = require('cordova-common').CordovaLogger.get();
29var Configstore = require('configstore');
30var conf = new Configstore(pkg.name + '-config');
31var editor = require('editor');
32
33var knownOpts = {
34 'verbose': Boolean,
35 'version': Boolean,
36 'help': Boolean,
37 'silent': Boolean,
38 'experimental': Boolean,
39 'noregistry': Boolean,
40 'nohooks': Array,
41 'shrinkwrap': Boolean,
42 'copy-from': String,
43 'link-to': path,
44 'searchpath': String,
45 'variable': Array,
46 'link': Boolean,
47 'force': Boolean,
48 'save-exact': Boolean,
49 // Flags to be passed to `cordova build/run/emulate`
50 'debug': Boolean,
51 'release': Boolean,
52 'archs': String,
53 'device': Boolean,
54 'emulator': Boolean,
55 'target': String,
56 'noprepare': Boolean,
57 'nobuild': Boolean,
58 'list': Boolean,
59 'buildConfig': String,
60 'template': String,
61 'production': Boolean,
62 'noprod': Boolean
63};
64
65var shortHands = {
66 'd': '--verbose',
67 'v': '--version',
68 'h': '--help',
69 'src': '--copy-from',
70 't': '--template'
71};
72
73function checkForUpdates () {
74 try {
75 // Checks for available update and returns an instance
76 var notifier = updateNotifier({ pkg: pkg });
77
78 if (notifier.update &&
79 notifier.update.latest !== pkg.version) {
80 // Notify using the built-in convenience method
81 notifier.notify();
82 }
83 } catch (e) {
84 // https://issues.apache.org/jira/browse/CB-10062
85 if (e && e.message && /EACCES/.test(e.message)) {
86 console.log('Update notifier was not able to access the config file.\n' +
87 'You may grant permissions to the file: \'sudo chmod 744 ~/.config/configstore/update-notifier-cordova.json\'');
88 } else {
89 throw e;
90 }
91 }
92}
93
94var shouldCollectTelemetry = false;
95
96module.exports = function (inputArgs) {
97 // If no inputArgs given, use process.argv.
98 inputArgs = inputArgs || process.argv;
99 var cmd = inputArgs[2]; // e.g: inputArgs= 'node cordova run ios'
100 var subcommand = getSubCommand(inputArgs, cmd);
101 var isTelemetryCmd = (cmd === 'telemetry');
102 var isConfigCmd = (cmd === 'config');
103
104 // ToDO: Move nopt-based parsing of args up here
105 if (cmd === '--version' || cmd === '-v') {
106 cmd = 'version';
107 } else if (!cmd || cmd === '--help' || cmd === 'h') {
108 cmd = 'help';
109 }
110
111 // If "get" is called
112 if (isConfigCmd && inputArgs[3] === 'get') {
113 if (inputArgs[4]) {
114 logger.subscribe(events);
115 conf.get(inputArgs[4]);
116 if (conf.get(inputArgs[4]) !== undefined) {
117 events.emit('log', conf.get(inputArgs[4]).toString());
118 } else {
119 events.emit('log', 'undefined');
120 }
121 }
122 }
123
124 // If "set" is called
125 if (isConfigCmd && inputArgs[3] === 'set') {
126 if (inputArgs[5] === undefined) {
127 conf.set(inputArgs[4], true);
128 }
129
130 if (inputArgs[5]) {
131 conf.set(inputArgs[4], inputArgs[5]);
132 }
133 }
134
135 // If "delete" is called
136 if (isConfigCmd && inputArgs[3] === 'delete') {
137 if (inputArgs[4]) {
138 conf.del(inputArgs[4]);
139 }
140 }
141
142 // If "edit" is called
143 if (isConfigCmd && inputArgs[3] === 'edit') {
144 editor(conf.path, function (code, sig) {
145 logger.warn('Finished editing with code ' + code);
146 });
147 }
148
149 // If "ls" is called
150 if (isConfigCmd && (inputArgs[3] === 'ls' || inputArgs[3] === 'list')) {
151 logger.results(JSON.stringify(conf.all, null, 4));
152 }
153
154 return Promise.resolve().then(function () {
155 /**
156 * Skip telemetry prompt if:
157 * - CI environment variable is present
158 * - Command is run with `--no-telemetry` flag
159 * - Command ran is: `cordova telemetry on | off | ...`
160 */
161
162 if (telemetry.isCI(process.env) || telemetry.isNoTelemetryFlag(inputArgs)) {
163 return Promise.resolve(false);
164 }
165
166 /**
167 * We shouldn't prompt for telemetry if user issues a command of the form: `cordova telemetry on | off | ...x`
168 * Also, if the user has already been prompted and made a decision, use his saved answer
169 */
170 if (isTelemetryCmd) {
171 var isOptedIn = telemetry.isOptedIn();
172 return handleTelemetryCmd(subcommand, isOptedIn);
173 }
174
175 if (telemetry.hasUserOptedInOrOut()) {
176 return Promise.resolve(telemetry.isOptedIn());
177 }
178
179 /**
180 * Otherwise, prompt user to opt-in or out
181 * Note: the prompt is shown for 30 seconds. If no choice is made by that time, User is considered to have opted out.
182 */
183 return telemetry.showPrompt();
184 }).then(function (collectTelemetry) {
185 shouldCollectTelemetry = collectTelemetry;
186 if (isTelemetryCmd) {
187 return Promise.resolve();
188 }
189 return cli(inputArgs);
190 }).then(function () {
191 if (shouldCollectTelemetry && !isTelemetryCmd) {
192 telemetry.track(cmd, subcommand, 'successful');
193 }
194 }).catch(function (err) {
195 if (shouldCollectTelemetry && !isTelemetryCmd) {
196 telemetry.track(cmd, subcommand, 'unsuccessful');
197 }
198 throw err;
199 });
200};
201
202function getSubCommand (args, cmd) {
203 if (['platform', 'platforms', 'plugin', 'plugins', 'telemetry', 'config'].indexOf(cmd) > -1) {
204 return args[3]; // e.g: args='node cordova platform rm ios', 'node cordova telemetry on'
205 }
206 return null;
207}
208
209function printHelp (command) {
210 var result = help([command]);
211 cordova.emit('results', result);
212}
213
214function handleTelemetryCmd (subcommand, isOptedIn) {
215
216 if (subcommand !== 'on' && subcommand !== 'off') {
217 logger.subscribe(events);
218 printHelp('telemetry');
219 return;
220 }
221
222 var turnOn = subcommand === 'on';
223 var cmdSuccess = true;
224
225 // turn telemetry on or off
226 try {
227 if (turnOn) {
228 telemetry.turnOn();
229 console.log('Thanks for opting into telemetry to help us improve cordova.');
230 } else {
231 telemetry.turnOff();
232 console.log('You have been opted out of telemetry. To change this, run: cordova telemetry on.');
233 }
234 } catch (ex) {
235 cmdSuccess = false;
236 }
237
238 // track or not track ?, that is the question
239
240 if (!turnOn) {
241 // Always track telemetry opt-outs (whether user opted out or not!)
242 telemetry.track('telemetry', 'off', 'via-cordova-telemetry-cmd', cmdSuccess ? 'successful' : 'unsuccessful');
243 return Promise.resolve();
244 }
245
246 if (isOptedIn) {
247 telemetry.track('telemetry', 'on', 'via-cordova-telemetry-cmd', cmdSuccess ? 'successful' : 'unsuccessful');
248 }
249
250 return Promise.resolve();
251}
252
253function cli (inputArgs) {
254
255 checkForUpdates();
256
257 var args = nopt(knownOpts, shortHands, inputArgs);
258
259 process.on('uncaughtException', function (err) {
260 if (err.message) {
261 logger.error(err.message);
262 } else {
263 logger.error(err);
264 }
265 // Don't send exception details, just send that it happened
266 if (shouldCollectTelemetry) {
267 telemetry.track('uncaughtException');
268 }
269 process.exit(1);
270 });
271
272 logger.subscribe(events);
273
274 if (args.silent) {
275 logger.setLevel('error');
276 } else if (args.verbose) { // can't be both silent AND verbose, silent wins
277 logger.setLevel('verbose');
278 }
279
280 var cliVersion = require('../package').version;
281 // TODO: Use semver.prerelease when it gets released
282 var usingPrerelease = /-nightly|-dev$/.exec(cliVersion);
283 if (args.version || usingPrerelease) {
284 var libVersion = require('cordova-lib/package').version;
285 var toPrint = cliVersion;
286 if (cliVersion !== libVersion || usingPrerelease) {
287 toPrint += ' (cordova-lib@' + libVersion + ')';
288 }
289
290 if (args.version) {
291 logger.results(toPrint);
292 return Promise.resolve(); // Important! this will return and cease execution
293 } else { // must be usingPrerelease
294 // Show a warning and continue
295 logger.warn('Warning: using prerelease version ' + toPrint);
296 }
297 }
298
299 if (/^v0.\d+[.\d+]*/.exec(process.version)) { // matches v0.*
300 var msg1 = 'Warning: using node version ' + process.version +
301 ' which has been deprecated. Please upgrade to the latest Node.js version available (LTS version recommended).';
302 logger.warn(msg1);
303 }
304
305 // If there were arguments protected from nopt with a double dash, keep
306 // them in unparsedArgs. For example:
307 // cordova build ios -- --verbose --whatever
308 // In this case "--verbose" is not parsed by nopt and args.vergbose will be
309 // false, the unparsed args after -- are kept in unparsedArgs and can be
310 // passed downstream to some scripts invoked by Cordova.
311 var unparsedArgs = [];
312 var parseStopperIdx = args.argv.original.indexOf('--');
313 if (parseStopperIdx !== -1) {
314 unparsedArgs = args.argv.original.slice(parseStopperIdx + 1);
315 }
316
317 // args.argv.remain contains both the undashed args (like platform names)
318 // and whatever unparsed args that were protected by " -- ".
319 // "undashed" stores only the undashed args without those after " -- " .
320 var remain = args.argv.remain;
321 var undashed = remain.slice(0, remain.length - unparsedArgs.length);
322 var cmd = undashed[0];
323 var subcommand;
324
325 if (!cmd || cmd === 'help' || args.help) {
326 if (!args.help && remain[0] === 'help') {
327 remain.shift();
328 }
329 return printHelp(remain);
330 }
331
332 if (!cordova.hasOwnProperty(cmd)) {
333 var msg2 = 'Cordova does not know ' + cmd + '; try `' + cordova_lib.binname +
334 ' help` for a list of all the available commands.';
335 throw new CordovaError(msg2);
336 }
337
338 var opts = {
339 platforms: [],
340 options: [],
341 verbose: args.verbose || false,
342 silent: args.silent || false,
343 nohooks: args.nohooks || [],
344 searchpath: args.searchpath
345 };
346
347 var platformCommands = ['emulate', 'build', 'prepare', 'compile', 'run', 'clean'];
348 if (platformCommands.indexOf(cmd) !== -1) {
349
350 // All options without dashes are assumed to be platform names
351 opts.platforms = undashed.slice(1);
352
353 // Pass nopt-parsed args to PlatformApi through opts.options
354 opts.options = args;
355 opts.options.argv = unparsedArgs;
356 if (cmd === 'run' && args.list && cordova.targets) {
357 return cordova.targets.call(null, opts);
358 }
359 return cordova[cmd].call(null, opts);
360
361 } else if (cmd === 'requirements') {
362 // All options without dashes are assumed to be platform names
363 opts.platforms = undashed.slice(1);
364
365 return cordova[cmd].call(null, opts.platforms)
366 .then(function (platformChecks) {
367
368 var someChecksFailed = Object.keys(platformChecks).map(function (platformName) {
369 events.emit('log', '\nRequirements check results for ' + platformName + ':');
370 var platformCheck = platformChecks[platformName];
371 if (platformCheck instanceof CordovaError) {
372 events.emit('warn', 'Check failed for ' + platformName + ' due to ' + platformCheck);
373 return true;
374 }
375
376 var someChecksFailed = false;
377
378 // platformCheck is expected to be an array of conditions that must be met
379 // the browser platform currently returns nothing, which was breaking here.
380 if (platformCheck && platformCheck.forEach) {
381 platformCheck.forEach(function (checkItem) {
382 var checkSummary = checkItem.name + ': ' +
383 (checkItem.installed ? 'installed ' : 'not installed ') +
384 (checkItem.installed ? checkItem.metadata.version.version || checkItem.metadata.version : '');
385 events.emit('log', checkSummary);
386 if (!checkItem.installed) {
387 someChecksFailed = true;
388 events.emit('warn', checkItem.metadata.reason);
389 }
390 });
391 }
392 return someChecksFailed;
393 }).some(function (isCheckFailedForPlatform) {
394 return isCheckFailedForPlatform;
395 });
396
397 if (someChecksFailed) {
398 throw new CordovaError('Some of requirements check failed');
399 }
400 });
401 } else if (cmd === 'serve') {
402 var port = undashed[1];
403 return cordova.serve(port);
404 } else if (cmd === 'create') {
405 return create(undashed, args);
406 } else if (cmd === 'config') {
407 // Don't need to do anything with cordova-lib since config was handled above
408 return true;
409 } else {
410 // platform/plugins add/rm [target(s)]
411 subcommand = undashed[1]; // sub-command like "add", "ls", "rm" etc.
412 var targets = undashed.slice(2); // array of targets, either platforms or plugins
413 var cli_vars = {};
414 if (args.variable) {
415 args.variable.forEach(function (strVar) {
416 // CB-9171
417 var keyVal = strVar.split('=');
418 if (keyVal.length < 2) {
419 throw new CordovaError('invalid variable format: ' + strVar);
420 } else {
421 var key = keyVal.shift().toUpperCase();
422 var val = keyVal.join('=');
423 cli_vars[key] = val;
424 }
425 });
426 }
427
428 if (args.nosave) {
429 args.save = false;
430 } else {
431 args.save = true;
432 }
433
434 if (args.noprod) {
435 args.production = false;
436 } else {
437 args.production = true;
438 }
439
440 if (args.save === undefined) {
441 // User explicitly did not pass in save
442 args.save = conf.get('autosave');
443 }
444 if (args.searchpath === undefined) {
445 // User explicitly did not pass in searchpath
446 args.searchpath = conf.get('searchpath');
447 }
448 if (args.production === undefined) {
449 // User explicitly did not pass in noprod
450 args.production = conf.get('production');
451 }
452
453 if (args['save-exact'] === undefined) {
454 // User explicitly did not pass in save-exact
455 args['save-exact'] = conf.get('save-exact');
456 }
457
458 var download_opts = { searchpath: args.searchpath,
459 noregistry: args.noregistry,
460 nohooks: args.nohooks,
461 cli_variables: cli_vars,
462 link: args.link || false,
463 save: args.save,
464 save_exact: args['save-exact'] || false,
465 shrinkwrap: args.shrinkwrap || false,
466 force: args.force || false,
467 production: args.production
468 };
469 return cordova[cmd](subcommand, targets, download_opts);
470 }
471}
472
473function create ([_, dir, id, name, cfgJson], args) {
474 // If we got a fourth parameter, consider it to be JSON to init the config.
475 var cfg = JSON.parse(cfgJson || '{}');
476
477 // Template path
478 var customWww = args['link-to'] || args.template;
479
480 if (customWww) {
481 // TODO Handle in create
482 if (!args.template && customWww.indexOf('http') === 0) {
483 throw new CordovaError(
484 'Only local paths for custom www assets are supported for linking' + customWww
485 );
486 }
487
488 // Resolve tilda
489 // TODO: move to create and use sindresorhus/untildify
490 if (customWww.substr(0, 1) === '~') { customWww = path.join(process.env.HOME, customWww.substr(1)); }
491
492 // Template config
493 var wwwCfg = {
494 url: customWww,
495 template: 'template' in args,
496 link: 'link-to' in args
497 };
498
499 cfg.lib = cfg.lib || {};
500 cfg.lib.www = wwwCfg;
501 }
502 return cordova.create(dir, id, name, cfg, events || undefined);
503}