UNPKG

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