UNPKG

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