UNPKG

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