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