diff --git a/tools/cli/commands-cordova.js b/tools/cli/commands-cordova.js
index bd1aa5e578e..5ca9a0c68e4 100644
--- a/tools/cli/commands-cordova.js
+++ b/tools/cli/commands-cordova.js
@@ -6,8 +6,8 @@ import { ProjectContext, PlatformList } from '../project-context.js';
import buildmessage from '../utils/buildmessage.js';
import files from '../fs/files.js';
-import { AVAILABLE_PLATFORMS, ensureCordovaPlatformsAreSynchronized, checkPlatformRequirements } from '../cordova/platforms.js';
-import { createCordovaProjectIfNecessary } from '../cordova/project.js';
+import * as cordova from '../cordova';
+import { CordovaProject } from '../cordova/project.js';
function createProjectContext(appDir) {
const projectContext = new ProjectContext({
@@ -42,17 +42,18 @@ main.registerCommand({
for (platform of platformsToAdd) {
if (_.contains(installedPlatforms, platform)) {
buildmessage.error(`${platform}: platform is already added`);
- } else if (!_.contains(AVAILABLE_PLATFORMS, platform)) {
+ } else if (!_.contains(cordova.AVAILABLE_PLATFORMS, platform)) {
buildmessage.error(`${platform}: no such platform`);
}
}
if (buildmessage.jobHasMessages()) return;
- const cordovaProject = createCordovaProjectIfNecessary(projectContext);
+ const cordovaProject = new CordovaProject(projectContext);
installedPlatforms = installedPlatforms.concat(platformsToAdd)
- ensureCordovaPlatformsAreSynchronized(cordovaProject, installedPlatforms);
+ const cordovaPlatforms = cordova.filterPlatforms(installedPlatforms);
+ cordovaProject.ensurePlatformsAreSynchronized(cordovaPlatforms);
if (buildmessage.jobHasMessages()) return;
@@ -60,7 +61,7 @@ main.registerCommand({
for (platform of platformsToAdd) {
Console.info(`${platform}: added platform`);
- checkPlatformRequirements(cordovaProject, platform);
+ cordovaProject.checkPlatformRequirements(platform);
}
});
@@ -96,10 +97,11 @@ main.registerCommand({
if (buildmessage.jobHasMessages()) return;
- installedPlatforms = _.without(installedPlatforms, ...platformsToRemove);
+ const cordovaProject = new CordovaProject(projectContext);
- const cordovaProject = createCordovaProjectIfNecessary(projectContext);
- ensureCordovaPlatformsAreSynchronized(cordovaProject, installedPlatforms);
+ installedPlatforms = _.without(installedPlatforms, ...platformsToRemove);
+ const cordovaPlatforms = cordova.filterPlatforms(installedPlatforms);
+ cordovaProject.ensurePlatformsAreSynchronized(cordovaPlatforms);
if (buildmessage.jobHasMessages()) return;
diff --git a/tools/cli/commands-packages.js b/tools/cli/commands-packages.js
index 52d769db0d4..f7713f1d377 100644
--- a/tools/cli/commands-packages.js
+++ b/tools/cli/commands-packages.js
@@ -11,7 +11,6 @@ var catalog = require('../packaging/catalog/catalog.js');
var catalogRemote = require('../packaging/catalog/catalog-remote.js');
var isopack = require('../isobuild/isopack.js');
var updater = require('../packaging/updater.js');
-import { filterCordovaPackages } from '../cordova/plugins.js';
var Console = require('../console/console.js').Console;
var projectContextModule = require('../project-context.js');
var colonConverter = require('../utils/colon-converter.js');
@@ -24,6 +23,8 @@ var packageMapModule = require('../packaging/package-map.js');
var packageClient = require('../packaging/package-client.js');
var tropohouse = require('../packaging/tropohouse.js');
+import * as cordova from '../cordova';
+
// For each release (or package), we store a meta-record with its name,
// maintainers, etc. This function takes in a name, figures out if
// it is a release or a package, and fetches the correct record.
@@ -1810,14 +1811,14 @@ main.registerCommand({
var exitCode = 0;
- var filteredPackages = filterCordovaPackages(options.args);
- var pluginsToAdd = filteredPackages.plugins;
+ const { plugins: pluginsToAdd, packages: packagesToAdd } =
+ cordova.splitPluginsAndPackages(options.args);
if (pluginsToAdd.length) {
- var plugins = projectContext.cordovaPluginsFile.getPluginVersions();
- var changed = false;
- _.each(pluginsToAdd, function (pluginSpec) {
- var parts = pluginSpec.split('@');
+ let plugins = projectContext.cordovaPluginsFile.getPluginVersions();
+ let changed = false;
+ for (pluginSpec of pluginsToAdd) {
+ let parts = pluginSpec.split('@');
if (parts.length !== 2) {
Console.error(
pluginSpec + ': exact version or tarball url is required');
@@ -1831,13 +1832,11 @@ main.registerCommand({
changed = true;
Console.info("added cordova plugin " + parts[0]);
}
- });
+ }
changed && projectContext.cordovaPluginsFile.write(plugins);
}
- var args = filteredPackages.rest;
-
- if (_.isEmpty(args))
+ if (_.isEmpty(packagesToAdd))
return exitCode;
// Messages that we should print if we make any changes, but that don't count
@@ -1849,7 +1848,7 @@ main.registerCommand({
// them -- add should be an atomic operation regardless of the package
// order.
var messages = buildmessage.capture(function () {
- _.each(args, function (packageReq) {
+ _.each(packagesToAdd, function (packageReq) {
buildmessage.enterJob("adding package " + packageReq, function () {
var constraint = utils.parsePackageConstraint(packageReq, {
useBuildmessage: true
@@ -1993,16 +1992,16 @@ main.registerCommand({
});
// Special case on reserved package namespaces, such as 'cordova'
- var filteredPackages = filterCordovaPackages(options.args);
- var pluginsToRemove = filteredPackages.plugins;
+ const { plugins: pluginsToRemove, packages } =
+ cordova.splitPluginsAndPackages(options.args);
- var exitCode = 0;
+ let exitCode = 0;
// Update the plugins list
if (pluginsToRemove.length) {
- var plugins = projectContext.cordovaPluginsFile.getPluginVersions();
- var changed = false;
- _.each(pluginsToRemove, function (pluginName) {
+ let plugins = projectContext.cordovaPluginsFile.getPluginVersions();
+ let changed = false;
+ for (pluginName of pluginsToRemove) {
if (/@/.test(pluginName)) {
Console.error(pluginName + ": do not specify version constraints.");
exitCode = 1;
@@ -2015,21 +2014,19 @@ main.registerCommand({
" is not in this project.");
exitCode = 1;
}
- });
+ }
changed && projectContext.cordovaPluginsFile.write(plugins);
}
- var args = filteredPackages.rest;
-
- if (_.isEmpty(args))
+ if (_.isEmpty(packages))
return exitCode;
// For each package name specified, check if we already have it and warn the
// user. Because removing each package is a completely atomic operation that
// has no chance of failure, this is just a warning message, it doesn't cause
// us to stop.
- var packagesToRemove = [];
- _.each(args, function (packageName) {
+ let packagesToRemove = [];
+ _.each(packages, function (packageName) {
if (/@/.test(packageName)) {
Console.error(packageName + ": do not specify version constraints.");
exitCode = 1;
diff --git a/tools/cli/commands.js b/tools/cli/commands.js
index 6f2fecfb681..ff0165505b6 100644
--- a/tools/cli/commands.js
+++ b/tools/cli/commands.js
@@ -13,14 +13,15 @@ var httpHelpers = require('../utils/http-helpers.js');
var archinfo = require('../utils/archinfo.js');
var catalog = require('../packaging/catalog/catalog.js');
var stats = require('../meteor-services/stats.js');
-import { platformsForTargets } from '../cordova/platforms.js';
-import { buildCordovaProject } from '../cordova/build.js';
-import { buildCordovaRunners } from '../cordova/run.js';
var Console = require('../console/console.js').Console;
var projectContextModule = require('../project-context.js');
-
var release = require('../packaging/release.js');
+import * as cordova from '../cordova';
+import { CordovaProject } from '../cordova/project.js';
+import { CordovaRunner } from '../cordova/runner.js';
+import { iOSRunTarget, AndroidRunTarget } from '../cordova/run-targets.js';
+
// The architecture used by MDG's hosted servers; it's the architecture used by
// 'meteor deploy'.
var DEPLOY_ARCH = 'os.linux.x86_64';
@@ -72,78 +73,77 @@ var showInvalidArchMsg = function (arch) {
// Utility functions to parse options in run/build/test-packages commands
export function parseServerOptionsForRunCommand(options) {
- const serverUrl = parsePortOption(options.port);
+ const parsedServerUrl = parsePortOption(options.port);
// XXX COMPAT WITH 0.9.2.2 -- the 'mobile-port' option is deprecated
const mobileServerOption = options['mobile-server'] || options['mobile-port'];
- let mobileServerUrl;
+ let parsedMobileServerUrl;
if (mobileServerOption) {
- mobileServerUrl = parseMobileServerOption(mobileServerOption);
+ parsedMobileServerUrl = parseMobileServerOption(mobileServerOption);
} else {
- mobileServerUrl = mobileServerUrlForServerUrl(serverUrl,
+ parsedMobileServerUrl = detectMobileServerUrl(parsedServerUrl,
isRunOnDeviceRequested(options));
}
- return { serverUrl, mobileServerUrl };
+ return { parsedServerUrl, parsedMobileServerUrl };
}
function parsePortOption(portOption) {
- let serverUrl;
+ let parsedServerUrl;
try {
- serverUrl = utils.parseUrl(portOption);
- } catch (err) {
+ parsedServerUrl = utils.parseUrl(portOption);
+ } catch (error) {
if (options.verbose) {
Console.rawError(
- `Error while parsing --port option: ${err.stack} \n`);
+ `Error while parsing --port option: ${error.stack} \n`);
} else {
- Console.error(err.message);
+ Console.error(error.message);
}
throw new main.ExitWithCode(1);
}
- if (!serverUrl.port) {
+ if (!parsedServerUrl.port) {
Console.error("--port must include a port.");
throw new main.ExitWithCode(1);
}
- return serverUrl;
+ return parsedServerUrl;
}
function parseMobileServerOption(mobileServerOption,
optionName = 'mobile-server') {
- let mobileServerUrl;
+ let parsedMobileServerUrl;
try {
- mobileServerUrl = utils.parseUrl(mobileServerOption, {
- protocol: 'http://'
- });
- } catch (err) {
+ parsedMobileServerUrl = utils.parseUrl(mobileServerOption, {
+ protocol: 'http://'});
+ } catch (error) {
if (options.verbose) {
Console.rawError(
- `Error while parsing --${optionName} option: ${err.stack} \n`);
+ `Error while parsing --${optionName} option: ${error.stack} \n`);
} else {
- Console.error(err.message);
+ Console.error(error.message);
}
throw new main.ExitWithCode(1);
}
- if (!mobileServerUrl.host) {
- Console.error(`--${optionName} must specify a hostname.`);
+ if (!parsedMobileServerUrl.host) {
+ Console.error(`--${optionName} must include a hostname.`);
throw new main.ExitWithCode(1);
}
- return mobileServerUrl;
+ return parsedMobileServerUrl;
}
-function mobileServerUrlForServerUrl(serverUrl, isRunOnDeviceRequested) {
+function detectMobileServerUrl(parsedServerUrl, isRunOnDeviceRequested) {
// If we are running on a device, use the auto-detected IP
if (isRunOnDeviceRequested) {
let myIp;
try {
myIp = utils.ipAddress();
- } catch (err) {
+ } catch (error) {
Console.error(
`Error detecting IP address for mobile app to connect to:
-${err.message}
+${error.message}
Please specify the address that the mobile app should connect
to with --mobile-server.`);
throw new main.ExitWithCode(1);
@@ -151,14 +151,14 @@ to with --mobile-server.`);
return {
protocol: 'http://',
host: myIp,
- port: serverUrl.port
+ port: parsedServerUrl.port
};
} else {
// We are running a simulator, use localhost
return {
protocol: 'http://',
host: 'localhost',
- port: serverUrl.port
+ port: parsedServerUrl.port
};
}
}
@@ -166,10 +166,27 @@ to with --mobile-server.`);
// Is a run on a device requested?
// XXX This shouldn't be hard-coded
function isRunOnDeviceRequested(options) {
- return !!_.intersection(options.args,
- ['ios-device', 'android-device']).length;
+ return !_.isEmpty(_.intersection(options.args,
+ ['ios-device', 'android-device']));
}
+function parseRunTargets(targets) {
+ return targets.map((target) => {
+ const targetParts = target.split('-');
+ const platform = targetParts[0];
+ const isDevice = targetParts[1] === 'device';
+
+ if (platform == 'ios') {
+ return new iOSRunTarget(isDevice);
+ } else if (platform == 'android') {
+ return new AndroidRunTarget(isDevice);
+ } else {
+ Console.error(`Unknown run target: ${target}`);
+ throw new main.ExitWithCode(1);
+ }
+ });
+};
+
///////////////////////////////////////////////////////////////////////////////
// options that act like commands
///////////////////////////////////////////////////////////////////////////////
@@ -289,7 +306,7 @@ main.registerCommand(_.extend(
function doRunCommand(options) {
Console.setVerbose(!!options.verbose);
- const { serverUrl, mobileServerUrl } =
+ const { parsedServerUrl, parsedMobileServerUrl } =
parseServerOptionsForRunCommand(options);
var projectContext = new projectContextModule.ProjectContext({
@@ -313,47 +330,6 @@ function doRunCommand(options) {
}
}
- var runners = [];
- // If additional args were specified, then also start a mobile build.
- // XXX We should defer this work until after the proxy is listening!
- // eg, move it into a CordovaBuildRunner or something.
-
- if (options.args.length) {
- let cordovaProject;
- // will asynchronously start mobile emulators/devices
- try {
- Console.debug('Will compile mobile builds');
- // Run the constraint solver and build local packages.
- // XXX This code should be part of the main runner loop so that we can
- // wait on a fix, just like in the non-Cordova case! (That would also
- // move the build after the proxy listen.)
- main.captureAndExit("=> Errors while initializing project:", function () {
- projectContext.prepareProjectForBuild();
- });
- projectContext.packageMapDelta.displayOnConsole();
-
- let targets = options.args;
- var platforms = platformsForTargets(targets);
- cordovaProject = buildCordovaProject(projectContext, platforms, _.extend({
- debug: !options.production
- }, options, {
- protocol: mobileServerUrl.protocol,
- host: mobileServerUrl.host,
- port: mobileServerUrl.port
- }));
-
- runners = runners.concat(
- buildCordovaRunners(projectContext, cordovaProject, targets, options));
- } catch (err) {
- if (err instanceof main.ExitWithCode) {
- throw err;
- } else {
- Console.printError(err, 'Error while running for mobile platforms');
- return 1;
- }
- }
- }
-
let appHost, appPort;
if (options['app-port']) {
var appPortMatch = options['app-port'].match(/^(?:(.+):)?([0-9]+)?$/);
@@ -379,22 +355,30 @@ function doRunCommand(options) {
// NOTE: this calls process.exit() when testing is done.
if (options['test']){
options.once = true;
- const serverUrlString = "http://" + (serverUrl.host || "localhost") +
- ":" + serverUrl.port;
+ const serverUrlForVelocity =
+ `http://${(parsedServerUrl.host || "localhost")}:${parsedServerUrl.port}`;
const velocity = require('../runners/run-velocity.js');
- velocity.runVelocity(serverUrlString);
+ velocity.runVelocity(serverUrlForVelocity);
}
- let mobileServerUrlString = mobileServerUrl.protocol + mobileServerUrl.host;
- if (mobileServerUrl.port) {
- mobileServerUrlString += `:${mobileServerUrl.port}`;
+ // Additional args are interpreted as run targets
+ const runTargets = parseRunTargets(options.args);
+
+ let cordovaRunner;
+
+ if (!_.isEmpty(runTargets)) {
+ main.captureAndExit('', 'initializing Cordova project', () => {
+ const cordovaProject = new CordovaProject(projectContext);
+ cordovaRunner = new CordovaRunner(cordovaProject, runTargets);
+ cordovaRunner.checkPlatformsForRunTargets();
+ });
}
var runAll = require('../runners/run-all.js');
return runAll.run({
projectContext: projectContext,
- proxyPort: serverUrl.port,
- proxyHost: serverUrl.host,
+ proxyPort: parsedServerUrl.port,
+ proxyHost: parsedServerUrl.host,
appPort: appPort,
appHost: appHost,
debugPort: options['debug-port'],
@@ -406,9 +390,9 @@ function doRunCommand(options) {
rootUrl: process.env.ROOT_URL,
mongoUrl: process.env.MONGO_URL,
oplogUrl: process.env.MONGO_OPLOG_URL,
- mobileServerUrl: mobileServerUrlString,
+ mobileServerUrl: utils.formatUrl(parsedMobileServerUrl),
once: options.once,
- extraRunners: runners
+ cordovaRunner: cordovaRunner
});
}
@@ -802,41 +786,34 @@ var buildCommand = function (options) {
options.settings = options['mobile-settings'];
}
- var mobilePlatforms = [];
- if (! options._serverOnly) {
- mobilePlatforms = projectContext.platformList.getCordovaPlatforms();
- }
+ const appName = files.pathBasename(options.appDir);
- if (!_.isEmpty(mobilePlatforms) && !options._serverOnly) {
- // XXX COMPAT WITH 0.9.2.2 -- the --mobile-port option is deprecated
- const mobileServerOption = options.server || options["mobile-port"];
- if (!mobileServerOption) {
- // For Cordova builds, require '--server'.
- // XXX better error message?
- Console.error(
- "Supply the server hostname and port in the --server option " +
- "for mobile app builds.");
- return 1;
- }
- const mobileServerUrl = parseMobileServerOption(mobileServerOption,
- 'server');
+ let cordovaPlatforms;
+ let parsedMobileServerUrl;
+ if (!options._serverOnly) {
+ cordovaPlatforms = projectContext.platformList.getCordovaPlatforms();
- var cordovaSettings = {};
+ if (process.platform !== 'darwin' && _.contains(cordovaPlatforms, 'ios')) {
+ cordovaPlatforms = _.without(cordovaPlatforms, 'ios');
+ Console.warn("Currently, it is only possible to build iOS apps on an OS X system.");
+ }
- try {
- cordovaProject =
- buildCordovaProject(projectContext, mobilePlatforms, _.extend({},
- options, {
- protocol: mobileServerUrl.protocol,
- host: mobileServerUrl.host,
- port: mobileServerUrl.port
- }));
- } catch (err) {
- if (err instanceof main.ExitWithCode)
- throw err;
- Console.printError(err, "Error while building for mobile platforms");
- return 1;
+ if (!_.isEmpty(cordovaPlatforms)) {
+ // XXX COMPAT WITH 0.9.2.2 -- the --mobile-port option is deprecated
+ const mobileServerOption = options.server || options["mobile-port"];
+ if (!mobileServerOption) {
+ // For Cordova builds, require '--server'.
+ // XXX better error message?
+ Console.error(
+ "Supply the server hostname and port in the --server option " +
+ "for mobile app builds.");
+ return 1;
+ }
+ parsedMobileServerUrl = parseMobileServerOption(mobileServerOption,
+ 'server');
}
+ } else {
+ cordovaPlatforms = [];
}
var buildDir = projectContext.getProjectLocalDirectory('build_tar');
@@ -844,7 +821,7 @@ var buildCommand = function (options) {
// Unless we're just making a tarball, warn if people try to build inside the
// app directory.
- if (options.directory || ! _.isEmpty(mobilePlatforms)) {
+ if (options.directory || ! _.isEmpty(cordovaPlatforms)) {
var relative = files.pathRelative(options.appDir, outputPath);
// We would like the output path to be outside the app directory, which
// means the first step to getting there is going up a level.
@@ -882,7 +859,7 @@ var buildCommand = function (options) {
// is then 'meteor bundle' with no args fails if you have any local
// packages with binary npm dependencies
serverArch: bundleArch,
- buildMode: options.debug ? 'development' : 'production'
+ buildMode: options.debug ? 'development' : 'production',
}
});
if (bundleResult.errors) {
@@ -895,54 +872,72 @@ var buildCommand = function (options) {
files.mkdir_p(outputPath);
if (! options.directory) {
- try {
- var outputTar = options._serverOnly ? outputPath :
- files.pathJoin(outputPath, cordovaProject.appName + '.tar.gz');
-
- files.createTarball(files.pathJoin(buildDir, 'bundle'), outputTar);
- } catch (err) {
- Console.error("Errors during tarball creation:");
- Console.error(err.message);
- files.rm_recursive(buildDir);
- return 1;
- }
+ main.captureAndExit('', 'creating server tarball', () => {
+ try {
+ var outputTar = options._serverOnly ? outputPath :
+ files.pathJoin(outputPath, appName + '.tar.gz');
+
+ files.createTarball(files.pathJoin(buildDir, 'bundle'), outputTar);
+ } catch (err) {
+ buildmessage.exception(err);
+ files.rm_recursive(buildDir);
+ }
+ });
}
- // Copy over the Cordova builds AFTER we bundle so that they are not included
- // in the main bundle.
- !options._serverOnly && _.each(mobilePlatforms, function (platformName) {
- var buildPath = files.pathJoin(
- projectContext.getProjectLocalDirectory('cordova-build'),
- 'platforms', platformName);
- var platformPath = files.pathJoin(outputPath, platformName);
-
- if (platformName === 'ios') {
- if (process.platform !== 'darwin') return;
- files.cp_r(buildPath, files.pathJoin(platformPath, 'project'));
- files.writeFile(
- files.pathJoin(platformPath, 'README'),
- "This is an auto-generated XCode project for your iOS application.\n\n" +
- "Instructions for publishing your iOS app to App Store can be found at:\n" +
- "https://github.com/meteor/meteor/wiki/How-to-submit-your-iOS-app-to-App-Store\n",
- "utf8");
- } else if (platformName === 'android') {
- files.cp_r(buildPath, files.pathJoin(platformPath, 'project'));
- var apkPath = findApkPath(files.pathJoin(buildPath, 'build'), options.debug);
- files.copyFile(apkPath, files.pathJoin(platformPath, options.debug ? 'debug.apk' : 'release-unsigned.apk'));
- files.writeFile(
- files.pathJoin(platformPath, 'README'),
- "This is an auto-generated Gradle project for your Android application.\n\n" +
- "Instructions for publishing your Android app to Play Store can be found at:\n" +
- "https://github.com/meteor/meteor/wiki/How-to-submit-your-Android-app-to-Play-Store\n",
- "utf8");
- }
- });
+ if (!_.isEmpty(cordovaPlatforms)) {
+ let cordovaProject;
- files.rm_recursive(buildDir);
-};
+ main.captureAndExit('', () => {
+ buildmessage.enterJob({ title: "preparing Cordova project"}, () => {
+ cordovaProject = new CordovaProject(projectContext, appName);
-var findApkPath = function (dirPath, debug) {
- return files.pathJoin(dirPath, 'outputs', 'apk', debug ? 'android-debug.apk' : 'android-release-unsigned.apk');
+ const plugins = cordova.pluginsFromStarManifest(
+ bundleResult.starManifest);
+
+ cordovaProject.prepare(bundlePath, plugins,
+ { settingsFile: options.settings,
+ mobileServerUrl: utils.formatUrl(parsedMobileServerUrl) });
+ });
+
+ for (platform of cordovaPlatforms) {
+ buildmessage.enterJob({ title: `building Cordova project for \
+${cordova.displayNameForPlatform(platform)}`}, () => {
+ let buildOptions = [];
+ if (!options.debug) buildOptions.push('--release');
+ cordovaProject.build([platform], buildOptions);
+
+ const buildPath = files.pathJoin(
+ projectContext.getProjectLocalDirectory('cordova-build'),
+ 'platforms', platform);
+ const platformOutputPath = files.pathJoin(outputPath, platform);
+
+ if (platform === 'ios') {
+ files.cp_r(buildPath, files.pathJoin(platformOutputPath, 'project'));
+ files.writeFile(
+ files.pathJoin(platformOutputPath, 'README'),
+ "This is an auto-generated XCode project for your iOS application.\n\n" +
+ "Instructions for publishing your iOS app to App Store can be found at:\n" +
+ "https://github.com/meteor/meteor/wiki/How-to-submit-your-iOS-app-to-App-Store\n",
+ "utf8");
+ } else if (platform === 'android') {
+ files.cp_r(buildPath, files.pathJoin(platformOutputPath, 'project'));
+ const apkPath = files.pathJoin(buildPath, 'build', 'outputs', 'apk',
+ options.debug ? 'android-debug.apk' : 'android-release-unsigned.apk')
+ files.copyFile(apkPath, files.pathJoin(platformOutputPath, options.debug ? 'debug.apk' : 'release-unsigned.apk'));
+ files.writeFile(
+ files.pathJoin(platformOutputPath, 'README'),
+ "This is an auto-generated Gradle project for your Android application.\n\n" +
+ "Instructions for publishing your Android app to Play Store can be found at:\n" +
+ "https://github.com/meteor/meteor/wiki/How-to-submit-your-Android-app-to-Play-Store\n",
+ "utf8");
+ }
+ });
+ }
+ });
+ }
+
+ files.rm_recursive(buildDir);
};
///////////////////////////////////////////////////////////////////////////////
@@ -1403,7 +1398,7 @@ main.registerCommand({
}, function (options) {
Console.setVerbose(!!options.verbose);
- const { serverUrl, mobileServerUrl } =
+ const { parsedServerUrl, parsedMobileServerUrl } =
parseServerOptionsForRunCommand(options);
// Find any packages mentioned by a path instead of a package name. We will
@@ -1474,59 +1469,32 @@ main.registerCommand({
// runner, once the proxy is listening. The changes we made were persisted to
// disk, so projectContext.reset won't make us forget anything.
- var mobileOptions = ['ios', 'ios-device', 'android', 'android-device'];
- var mobileTargets = [];
- _.each(mobileOptions, function (option) {
- if (options[option])
- mobileTargets.push(option);
- });
-
- if (! _.isEmpty(mobileTargets)) {
- var runners = [];
+ const runTargets = parseRunTargets(_.intersection(
+ Object.keys(options), ['ios', 'ios-device', 'android', 'android-device']));
- var platforms = platformsForTargets(mobileTargets);
- projectContext.platformList.write(platforms);
+ let cordovaRunner;
- // Run the constraint solver and build local packages.
- // XXX This code should be part of the main runner loop so that we can
- // wait on a fix, just like in the non-Cordova case! (That would also
- // move the build after the proxy listen.)
- main.captureAndExit("=> Errors while initializing project:", function () {
- projectContext.prepareProjectForBuild();
+ if (!_.isEmpty(runTargets)) {
+ main.captureAndExit('', 'initializing Cordova project', () => {
+ const cordovaProject = new CordovaProject(projectContext);
+ cordovaRunner = new CordovaRunner(cordovaProject, runTargets);
+ projectContext.platformList.write(cordovaRunner.platformsForRunTargets);
+ cordovaRunner.checkPlatformsForRunTargets();
});
- // No need to display the PackageMapDelta here, since it would include all
- // of the packages!
-
- try {
- const cordovaProject = buildCordovaProject(projectContext, platforms,
- _.extend({}, options, {
- debug: ! options.production
- }, {
- protocol: mobileServerUrl.protocol,
- host: mobileServerUrl.host,
- port: mobileServerUrl.port
- }));
- runners = runners.concat(buildCordovaRunners(projectContext,
- cordovaProject, mobileTargets, options));
- } catch (err) {
- if (err instanceof main.ExitWithCode) {
- throw err;
- } else {
- Console.printError(err, 'Error while testing for mobile platforms');
- return 1;
- }
- }
- options.extraRunners = runners;
}
+ options.cordovaRunner = cordovaRunner;
+
if (options.velocity) {
- const serverUrlString = "http://" + (parsedUrl.host || "localhost") +
- ":" + parsedUrl.port;
+ const serverUrlForVelocity =
+ `http://${(parsedServerUrl.host || "localhost")}:${parsedServerUrl.port}`;
const velocity = require('../runners/run-velocity.js');
- velocity.runVelocity(serverUrlString);
+ velocity.runVelocity(serverUrlForVelocity);
}
- return runTestAppForPackages(projectContext, options);
+ return runTestAppForPackages(projectContext, _.extend(
+ options,
+ { mobileServerUrl: utils.formatUrl(parsedMobileServerUrl) }));
});
// Returns the "local-test:*" package names for the given package names (or for
@@ -1619,11 +1587,12 @@ var runTestAppForPackages = function (projectContext, options) {
rootUrl: process.env.ROOT_URL,
mongoUrl: process.env.MONGO_URL,
oplogUrl: process.env.MONGO_OPLOG_URL,
+ mobileServerUrl: options.mobileServerUrl,
once: options.once,
recordPackageUsage: false,
selenium: options.selenium,
seleniumBrowser: options['selenium-browser'],
- extraRunners: options.extraRunners,
+ cordovaRunner: options.cordovaRunner,
// On the first run, we shouldn't display the delta between "no packages
// in the temp app" and "all the packages we're testing". If we make
// changes and reload, though, it's fine to display them.
diff --git a/tools/cordova/android-runner.js b/tools/cordova/android-runner.js
deleted file mode 100644
index 963ce7608d6..00000000000
--- a/tools/cordova/android-runner.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import isopackets from '../tool-env/isopackets.js'
-import files from '../fs/files.js';
-import { Console } from '../console.js';
-
-import CordovaRunner from './cordova-runner.js'
-import { execFileSyncOrThrow, execFileAsyncOrThrow } from './utils.js'
-
-export default class AndroidRunner extends CordovaRunner {
- constructor(projectContext, cordovaProject, isDevice, options) {
- super(projectContext, cordovaProject, options);
- this.isDevice = isDevice;
- }
-
- get platform() {
- return 'android';
- }
-
- get displayName() {
- return this.isDevice ? 'Android Device' : 'Android Emulator';
- }
-
- checkRequirementsAndSetEnvIfNeeded() {
- const platformsDir = files.pathJoin(this.cordovaProject.projectRoot, 'platforms');
- const modulePath = files.pathJoin(platformsDir, 'android', 'cordova', 'lib', 'check_reqs');
- Promise.await(require(files.convertToOSPath(modulePath)).run());
- }
-
- async run(options) {
- return this.cordovaProject.run(this.platform, this.isDevice, options)
- }
-
- async tailLogs(options) {
- // Make cordova-android handle requirements and set env if needed
- this.checkRequirementsAndSetEnvIfNeeded();
-
- // Clear logs
- execFileSyncOrThrow('adb', ['logcat', '-c']);
-
- execFileAsyncOrThrow('adb', ['logcat'], {
- verbose: true,
- lineMapper: null
- });
- }
-}
diff --git a/tools/cordova/build.js b/tools/cordova/build.js
deleted file mode 100644
index ef90f6a0804..00000000000
--- a/tools/cordova/build.js
+++ /dev/null
@@ -1,193 +0,0 @@
-import _ from 'underscore';
-import util from 'util';
-import { Console } from '../console.js';
-import buildmessage from '../buildmessage.js';
-import files from '../fs/files.js';
-import bundler from '../isobuild/bundler.js';
-import archinfo from '../archinfo.js';
-import release from '../packaging/release.js';
-import isopackets from '../tool-env/isopackets.js'
-
-import { createCordovaProjectIfNecessary } from './project.js';
-import { AVAILABLE_PLATFORMS, ensureCordovaPlatformsAreSynchronized,
- checkCordovaPlatforms } from './platforms.js';
-import { ensureCordovaPluginsAreSynchronized } from './plugins.js';
-import { processMobileControlFile } from './mobile-control-file.js';
-
-const WEB_ARCH_NAME = "web.cordova";
-
-// Returns the cordovaDependencies of the Cordova arch from a star json.
-export function getCordovaDependenciesFromStar(star) {
- var cordovaProgram = _.findWhere(star.programs, { arch: WEB_ARCH_NAME });
- if (cordovaProgram) {
- return cordovaProgram.cordovaDependencies;
- } else {
- return {};
- }
-}
-
-// Build a Cordova project, creating it if necessary.
-export function buildCordovaProject(projectContext, platforms, options) {
- if (_.isEmpty(platforms)) return;
-
- Console.debug('Building the Cordova project');
-
- platforms = checkCordovaPlatforms(projectContext, platforms);
-
- // Make sure there is a project, as all other operations depend on that
- const cordovaProject = createCordovaProjectIfNecessary(projectContext);
-
- buildmessage.enterJob({ title: 'building for mobile devices' }, function () {
- const bundlePath =
- projectContext.getProjectLocalDirectory('build-cordova-temp');
-
- Console.debug('Bundling the web.cordova program of the app');
- const bundle = getBundle(projectContext, bundlePath, options);
-
- // Check and consume the control file
- const controlFilePath =
- files.pathJoin(projectContext.projectDir, 'mobile-config.js');
-
- processMobileControlFile(
- controlFilePath,
- projectContext,
- cordovaProject,
- options.host);
-
- ensureCordovaPlatformsAreSynchronized(cordovaProject,
- projectContext.platformList.getPlatforms());
-
- ensureCordovaPluginsAreSynchronized(cordovaProject, getCordovaDependenciesFromStar(
- bundle.starManifest));
-
- const wwwPath = files.pathJoin(cordovaProject.projectRoot, 'www');
-
- Console.debug('Removing the www folder');
- files.rm_recursive(wwwPath);
-
- const applicationPath = files.pathJoin(wwwPath, 'application');
- const programPath = files.pathJoin(bundlePath, 'programs', WEB_ARCH_NAME);
-
- Console.debug('Writing www/application folder');
- files.mkdir_p(applicationPath);
- files.cp_r(programPath, applicationPath);
-
- // Clean up the temporary bundle directory
- files.rm_recursive(bundlePath);
-
- Console.debug('Writing index.html');
-
- // Generate index.html
- var indexHtml = generateCordovaBoilerplate(
- projectContext, applicationPath, options);
- files.writeFile(files.pathJoin(applicationPath, 'index.html'), indexHtml, 'utf8');
-
- // Write the cordova loader
- Console.debug('Writing meteor_cordova_loader');
- var loaderPath = files.pathJoin(__dirname, 'client', 'meteor_cordova_loader.js');
- var loaderCode = files.readFile(loaderPath);
- files.writeFile(files.pathJoin(wwwPath, 'meteor_cordova_loader.js'), loaderCode);
-
- Console.debug('Writing a default index.html for cordova app');
- var indexPath = files.pathJoin(__dirname, 'client', 'cordova_index.html');
- var indexContent = files.readFile(indexPath);
- files.writeFile(files.pathJoin(wwwPath, 'index.html'), indexContent);
-
- // Cordova Build Override feature (c)
- var buildOverridePath =
- files.pathJoin(projectContext.projectDir, 'cordova-build-override');
-
- if (files.exists(buildOverridePath) &&
- files.stat(buildOverridePath).isDirectory()) {
- Console.debug('Copying over the cordova-build-override');
- files.cp_r(buildOverridePath, cordovaProject.projectRoot);
- }
-
- // Run the actual build
- Console.debug('Running the build command');
-
- buildmessage.enterJob({ title: 'building mobile project' }, () => {
- const buildOptions = options.debug ? [] : ['release'];
- Promise.await(cordovaProject.build({ platforms: platforms, options: buildOptions }));
- });
- });
-
- Console.debug('Done building the cordova build project');
-
- return cordovaProject;
-};
-
-// options
-// - debug
-function getBundle(projectContext, bundlePath, options) {
- var bundleResult = bundler.bundle({
- projectContext: projectContext,
- outputPath: bundlePath,
- buildOptions: {
- minifyMode: options.debug ? 'development' : 'production',
- // XXX can we ask it not to create the server arch?
- serverArch: archinfo.host(),
- webArchs: [WEB_ARCH_NAME],
- includeDebug: !!options.debug
- }
- });
-
- if (bundleResult.errors) {
- // XXX better error handling?
- throw new Error("Errors prevented bundling:\n" +
- bundleResult.errors.formatMessages());
- }
-
- return bundleResult;
-};
-
-function generateCordovaBoilerplate(projectContext, clientDir, options) {
- var clientJsonPath = files.convertToOSPath(files.pathJoin(clientDir, 'program.json'));
- var clientJson = JSON.parse(files.readFile(clientJsonPath, 'utf8'));
- var manifest = clientJson.manifest;
- var settings = options.settings ?
- JSON.parse(files.readFile(options.settings, 'utf8')) : {};
- var publicSettings = settings['public'];
-
- var meteorRelease =
- release.current.isCheckout() ? "none" : release.current.name;
-
- var configDummy = {};
- if (publicSettings) configDummy.PUBLIC_SETTINGS = publicSettings;
-
- const { WebAppHashing } = isopackets.load('cordova-support')['webapp-hashing'];
- var calculatedHash =
- WebAppHashing.calculateClientHash(manifest, null, configDummy);
-
- // XXX partially copied from autoupdate package
- var version = process.env.AUTOUPDATE_VERSION || calculatedHash;
-
- var mobileServer = options.protocol + options.host;
- if (options.port) {
- mobileServer = mobileServer + ":" + options.port;
- }
-
- var runtimeConfig = {
- meteorRelease: meteorRelease,
- ROOT_URL: mobileServer + "/",
- // XXX propagate it from options?
- ROOT_URL_PATH_PREFIX: '',
- DDP_DEFAULT_CONNECTION_URL: mobileServer,
- autoupdateVersionCordova: version,
- appId: projectContext.appIdentifier
- };
-
- if (publicSettings)
- runtimeConfig.PUBLIC_SETTINGS = publicSettings;
-
- const { Boilerplate } = isopackets.load('cordova-support')['boilerplate-generator'];
- var boilerplate = new Boilerplate(WEB_ARCH_NAME, manifest, {
- urlMapper: _.identity,
- pathMapper: (path) => files.convertToOSPath(files.pathJoin(clientDir, path)),
- baseDataExtension: {
- meteorRuntimeConfig: JSON.stringify(
- encodeURIComponent(JSON.stringify(runtimeConfig)))
- }
- });
- return boilerplate.toHTML();
-};
diff --git a/tools/cordova/builder.js b/tools/cordova/builder.js
new file mode 100644
index 00000000000..9958c926774
--- /dev/null
+++ b/tools/cordova/builder.js
@@ -0,0 +1,589 @@
+import _ from 'underscore';
+import util from 'util';
+import { Console } from '../console/console.js';
+import buildmessage from '../utils/buildmessage.js';
+import files from '../fs/files.js';
+import bundler from '../isobuild/bundler.js';
+import archinfo from '../utils/archinfo.js';
+import release from '../packaging/release.js';
+import isopackets from '../tool-env/isopackets.js';
+import utils from '../utils/utils.js';
+
+import { CORDOVA_ARCH } from './index.js';
+
+// Hard-coded size constants
+
+const iconsIosSizes = {
+ 'iphone': '60x60',
+ 'iphone_2x': '120x120',
+ 'iphone_3x': '180x180',
+ 'ipad': '76x76',
+ 'ipad_2x': '152x152'
+};
+
+const iconsAndroidSizes = {
+ 'android_ldpi': '36x36',
+ 'android_mdpi': '42x42',
+ 'android_hdpi': '72x72',
+ 'android_xhdpi': '96x96'
+};
+
+const launchIosSizes = {
+ 'iphone': '320x480',
+ 'iphone_2x': '640x960',
+ 'iphone5': '640x1136',
+ 'iphone6': '750x1334',
+ 'iphone6p_portrait': '1242x2208',
+ 'iphone6p_landscape': '2208x1242',
+ 'ipad_portrait': '768x1004',
+ 'ipad_portrait_2x': '1536x2008',
+ 'ipad_landscape': '1024x748',
+ 'ipad_landscape_2x': '2048x1496'
+};
+
+const launchAndroidSizes = {
+ 'android_ldpi_portrait': '320x426',
+ 'android_ldpi_landscape': '426x320',
+ 'android_mdpi_portrait': '320x470',
+ 'android_mdpi_landscape': '470x320',
+ 'android_hdpi_portrait': '480x640',
+ 'android_hdpi_landscape': '640x480',
+ 'android_xhdpi_portrait': '720x960',
+ 'android_xhdpi_landscape': '960x720'
+};
+
+export class CordovaBuilder {
+ constructor(cordovaProject, bundlePath, plugins, options) {
+ this.cordovaProject = cordovaProject;
+
+ this.bundlePath = bundlePath;
+ this.plugins = plugins;
+ this.options = options;
+
+ this.resourcesPath = files.pathJoin(
+ this.cordovaProject.projectRoot,
+ 'resources');
+ }
+
+ get projectContext() {
+ return this.cordovaProject.projectContext;
+ }
+
+ start() {
+ buildmessage.assertInCapture();
+
+ buildmessage.enterJob({ title: `preparing Cordova project` }, () => {
+ this.initalizeDefaults();
+
+ this.processControlFile();
+
+ this.writeConfigXmlAndCopyResources();
+ this.copyWWW();
+ this.copyBuildOverride();
+
+ this.cordovaProject.ensurePlatformsAreSynchronized();
+ this.cordovaProject.ensurePluginsAreSynchronized(this.plugins,
+ this.pluginsConfiguration);
+ });
+ }
+
+ initalizeDefaults() {
+ const defaultBuildNumber = (Date.now() % 1000000).toString();
+ this.metadata = {
+ id: 'com.id' + this.projectContext.appIdentifier,
+ version: '0.0.1',
+ buildNumber: defaultBuildNumber,
+ name: this.cordovaProject.appName,
+ description: 'New Meteor Mobile App',
+ author: 'A Meteor Developer',
+ email: 'n/a',
+ website: 'n/a'
+ };
+
+ // set some defaults different from the Phonegap/Cordova defaults
+ this.additionalConfiguration = {
+ 'webviewbounce': false,
+ 'DisallowOverscroll': true,
+ 'deployment-target': '7.0'
+ };
+
+ if (this.projectContext.packageMap.getInfo('launch-screen')) {
+ this.additionalConfiguration.AutoHideSplashScreen = false;
+ this.additionalConfiguration.SplashScreen = 'screen';
+ this.additionalConfiguration.SplashScreenDelay = 10000;
+ }
+
+ // Default access rules for plain Meteor-Cordova apps.
+ // Rules can be extended with mobile-config API.
+ // The value is `true` if the protocol or domain should be allowed,
+ // 'external' if should handled externally.
+ this.accessRules = {
+ // Allow external calls to things like email client or maps app or a
+ // phonebook app.
+ 'tel:*': 'external',
+ 'geo:*': 'external',
+ 'mailto:*': 'external',
+ 'sms:*': 'external',
+ 'market:*': 'external',
+
+ // phonegap/cordova related protocols
+ // "file:" protocol is used to access first files from disk
+ 'file:*': true,
+ 'cdv:*': true,
+ 'gap:*': true,
+
+ // allow Meteor's local emulated server url - this is the url from which the
+ // application loads its assets
+ 'http://meteor.local/*': true
+ };
+
+ const mobileServerUrl = this.options.mobileServerUrl;
+ const serverDomain = mobileServerUrl ?
+ utils.parseUrl(mobileServerUrl).host : null;
+
+ // If the remote server domain is known, allow access to it for xhr and DDP
+ // connections.
+ if (serverDomain) {
+ this.accessRules['*://' + serverDomain + '/*'] = true;
+ // Android talks to localhost over 10.0.2.2. This config file is used for
+ // multiple platforms, so any time that we say the server is on localhost we
+ // should also say it is on 10.0.2.2.
+ if (serverDomain === 'localhost') {
+ this.accessRules['*://10.0.2.2/*'] = true;
+ }
+ }
+
+ this.imagePaths = {
+ icon: {},
+ splash: {}
+ };
+
+ // Defaults are Meteor meatball images located in tools/cordova/assets directory
+ const assetsPath = files.pathJoin(__dirname, 'assets');
+ const iconsPath = files.pathJoin(assetsPath, 'icons');
+ const launchScreensPath = files.pathJoin(assetsPath, 'launchscreens');
+
+ const setIcon = (size, name) => {
+ this.imagePaths.icon[name] = files.pathJoin(iconsPath, size + '.png');
+ };
+
+ const setLaunchscreen = (size, name) => {
+ this.imagePaths.splash[name] = files.pathJoin(launchScreensPath, size + '.png');
+ };
+
+ _.each(iconsIosSizes, setIcon);
+ _.each(iconsAndroidSizes, setIcon);
+ _.each(launchIosSizes, setLaunchscreen);
+ _.each(launchAndroidSizes, setLaunchscreen);
+
+ this.pluginsConfiguration = {};
+ }
+
+ processControlFile() {
+ const controlFilePath =
+ files.pathJoin(this.projectContext.projectDir, 'mobile-config.js');
+
+
+ if (files.exists(controlFilePath)) {
+ const code = files.readFile(controlFilePath, 'utf8');
+
+ try {
+ Console.debug('Running the mobile control file');
+ files.runJavaScript(code, {
+ filename: 'mobile-config.js',
+ symbols: { App: App(this) }
+ });
+ } catch (error) {
+ throw new Error('Error reading mobile-config.js:' + error.stack);
+ }
+ }
+ }
+
+ writeConfigXmlAndCopyResources() {
+ const { XmlBuilder } = isopackets.load('cordova-support')['xmlbuilder'];
+
+ let config = XmlBuilder.create('widget');
+
+ // set the root attributes
+ _.each({
+ id: this.metadata.id,
+ version: this.metadata.version,
+ 'android-versionCode': this.metadata.buildNumber,
+ 'ios-CFBundleVersion': this.metadata.buildNumber,
+ xmlns: 'http://www.w3.org/ns/widgets',
+ 'xmlns:cdv': 'http://cordova.apache.org/ns/1.0'
+ }, (value, key) => {
+ if (value) {
+ config.att(key, value);
+ }
+ });
+
+ // set the metadata
+ config.element('name').txt(this.metadata.name);
+ config.element('description').txt(this.metadata.description);
+ config.element('author', {
+ href: this.metadata.website,
+ email: this.metadata.email
+ }).txt(this.metadata.author);
+
+ // set the additional configuration preferences
+ _.each(this.additionalConfiguration, (value, key) => {
+ config.element('preference', {
+ name: key,
+ value: value.toString()
+ });
+ });
+
+ // load from index.html by default
+ config.element('content', { src: 'index.html' });
+
+ // Copy all the access rules
+ _.each(this.accessRules, (rule, pattern) => {
+ var opts = { origin: pattern };
+ if (rule === 'external')
+ opts['launch-external'] = true;
+
+ config.element('access', opts);
+ });
+
+ const iosPlatformElement = config.element('platform', { name: 'ios' });
+ const androidPlatformElement = config.element('platform', { name: 'android' });
+
+ // Prepare the resources folder
+ files.rm_recursive(this.resourcesPath);
+ files.mkdir_p(this.resourcesPath);
+
+ Console.debug('Copying resources for mobile apps');
+
+ // add icons and launch screens to config and copy the files on fs
+ this.configureAndCopyImages(iconsIosSizes, iosPlatformElement, 'icon');
+ this.configureAndCopyImages(iconsAndroidSizes, androidPlatformElement, 'icon');
+ this.configureAndCopyImages(launchIosSizes, iosPlatformElement, 'splash');
+ this.configureAndCopyImages(launchAndroidSizes, androidPlatformElement, 'splash');
+
+ Console.debug('Writing new config.xml');
+ const configXmlPath = files.pathJoin(this.cordovaProject.projectRoot, 'config.xml');
+ const formattedXmlConfig = config.end({ pretty: true });
+ files.writeFile(configXmlPath, formattedXmlConfig, 'utf8');
+ }
+
+ configureAndCopyImages(sizes, xmlElement, tag) {
+ const imageAttributes = (name, width, height, src) => {
+ const androidMatch = /android_(.?.dpi)_(landscape|portrait)/g.exec(name);
+
+ let attributes = {
+ src: src,
+ width: width,
+ height: height
+ };
+
+ // XXX special case for Android
+ if (androidMatch) {
+ attributes.density = androidMatch[2].substr(0, 4) + '-' + androidMatch[1];
+ }
+
+ return attributes;
+ };
+
+ _.each(sizes, (size, name) => {
+ const [width, height] = size.split('x');
+
+ const suppliedPath = this.imagePaths[tag][name];
+ if (!suppliedPath)
+ return;
+
+ const suppliedFilename = _.last(suppliedPath.split(files.pathSep));
+ let extension = _.last(suppliedFilename.split('.'));
+
+ // XXX special case for 9-patch png's
+ if (suppliedFilename.match(/\.9\.png$/)) {
+ extension = '9.png';
+ }
+
+ const filename = name + '.' + tag + '.' + extension;
+ const src = files.pathJoin('resources', filename);
+
+ // copy the file to the build folder with a standardized name
+ files.copyFile(
+ files.pathResolve(this.projectContext.projectDir, suppliedPath),
+ files.pathJoin(this.resourcesPath, filename));
+
+ // set it to the xml tree
+ xmlElement.element(tag, imageAttributes(name, width, height, src));
+
+ // XXX reuse one size for other dimensions
+ const dups = {
+ '60x60': ['29x29', '40x40', '50x50', '57x57', '58x58'],
+ '76x76': ['72x72'],
+ '152x152': ['144x144'],
+ '120x120': ['80x80', '100x100', '114x114'],
+ '768x1004': ['768x1024'],
+ '1536x2008': ['1536x2048'],
+ '1024x748': ['1024x768'],
+ '2048x1496': ['2048x1536']
+ }[size];
+
+ // just use the same image
+ _.each(dups, (size) => {
+ const [width, height] = size.split('x');
+ // XXX this is fine to not supply a name since it is always iOS, but
+ // this is a hack right now.
+ xmlElement.element(tag, imageAttributes('n/a', width, height, src));
+ });
+ });
+ }
+
+ copyWWW() {
+ const wwwPath = files.pathJoin(this.cordovaProject.projectRoot, 'www');
+
+ // Remove existing www
+ files.rm_recursive(wwwPath);
+
+ // Create www and www/application directories
+ const applicationPath = files.pathJoin(wwwPath, 'application');
+ files.mkdir_p(applicationPath);
+
+ // Copy Cordova arch program from bundle to www/application
+ const programPath = files.pathJoin(this.bundlePath, 'programs', CORDOVA_ARCH);
+ files.cp_r(programPath, applicationPath);
+
+ const bootstrapPage = this.generateBootstrapPage(applicationPath);
+ files.writeFile(files.pathJoin(applicationPath, 'index.html'),
+ bootstrapPage, 'utf8');
+
+ files.copyFile(
+ files.pathJoin(__dirname, 'client', 'meteor_cordova_loader.js'),
+ files.pathJoin(wwwPath, 'meteor_cordova_loader.js'));
+ files.copyFile(
+ files.pathJoin(__dirname, 'client', 'cordova_index.html'),
+ files.pathJoin(wwwPath, 'index.html'));
+ }
+
+ generateBootstrapPage(applicationPath) {
+ const programJsonPath = files.convertToOSPath(
+ files.pathJoin(applicationPath, 'program.json'));
+ const programJson = JSON.parse(files.readFile(programJsonPath, 'utf8'));
+ const manifest = programJson.manifest;
+
+ const settingsFile = this.options.settingsFile;
+ const settings = settingsFile ?
+ JSON.parse(files.readFile(settingsFile, 'utf8')) : {};
+ const publicSettings = settings['public'];
+
+ const meteorRelease =
+ release.current.isCheckout() ? "none" : release.current.name;
+
+ let configDummy = {};
+ if (publicSettings) {
+ configDummy.PUBLIC_SETTINGS = publicSettings;
+ }
+
+ const { WebAppHashing } = isopackets.load('cordova-support')['webapp-hashing'];
+ const calculatedHash =
+ WebAppHashing.calculateClientHash(manifest, null, configDummy);
+
+ // XXX partially copied from autoupdate package
+ const version = process.env.AUTOUPDATE_VERSION || calculatedHash;
+
+ const mobileServerUrl = this.options.mobileServerUrl;
+
+ const runtimeConfig = {
+ meteorRelease: meteorRelease,
+ ROOT_URL: mobileServerUrl + "/",
+ // XXX propagate it from this.options?
+ ROOT_URL_PATH_PREFIX: '',
+ DDP_DEFAULT_CONNECTION_URL: mobileServerUrl,
+ autoupdateVersionCordova: version,
+ appId: this.projectContext.appIdentifier
+ };
+
+ if (publicSettings)
+ runtimeConfig.PUBLIC_SETTINGS = publicSettings;
+
+ const { Boilerplate } = isopackets.load('cordova-support')['boilerplate-generator'];
+ const boilerplate = new Boilerplate(CORDOVA_ARCH, manifest, {
+ urlMapper: _.identity,
+ pathMapper: (path) => files.convertToOSPath(
+ files.pathJoin(applicationPath, path)),
+ baseDataExtension: {
+ meteorRuntimeConfig: JSON.stringify(
+ encodeURIComponent(JSON.stringify(runtimeConfig)))
+ }
+ });
+
+ return boilerplate.toHTML();
+ }
+
+ copyBuildOverride() {
+ const buildOverridePath =
+ files.pathJoin(this.projectContext.projectDir, 'cordova-build-override');
+
+ if (files.exists(buildOverridePath) &&
+ files.stat(buildOverridePath).isDirectory()) {
+ Console.debug('Copying over the cordova-build-override directory');
+ files.cp_r(buildOverridePath, this.cordovaProject.projectRoot);
+ }
+ }
+}
+
+function App(builder) {
+ /**
+ * @namespace App
+ * @global
+ * @summary The App configuration object in mobile-config.js
+ */
+ return {
+ /**
+ * @summary Set your mobile app's core configuration information.
+ * @param {Object} options
+ * @param {String} [options.id,version,name,description,author,email,website]
+ * Each of the options correspond to a key in the app's core configuration
+ * as described in the [PhoneGap documentation](http://docs.phonegap.com/en/3.5.0/config_ref_index.md.html#The%20config.xml%20File_core_configuration_elements).
+ * @memberOf App
+ */
+ info: function (options) {
+ // check that every key is meaningful
+ _.each(options, function (value, key) {
+ if (!_.has(builder.metadata, key))
+ throw new Error("Unknown key in App.info configuration: " + key);
+ });
+
+ _.extend(builder.metadata, options);
+ },
+ /**
+ * @summary Add a preference for your build as described in the
+ * [PhoneGap documentation](http://docs.phonegap.com/en/3.5.0/config_ref_index.md.html#The%20config.xml%20File_global_preferences).
+ * @param {String} name A preference name supported by Phonegap's
+ * `config.xml`.
+ * @param {String} value The value for that preference.
+ * @memberOf App
+ */
+ setPreference: function (key, value) {
+ builder.additionalConfiguration[key] = value;
+ },
+
+ /**
+ * @summary Set the build-time configuration for a Phonegap plugin.
+ * @param {String} pluginName The identifier of the plugin you want to
+ * configure.
+ * @param {Object} config A set of key-value pairs which will be passed
+ * at build-time to configure the specified plugin.
+ * @memberOf App
+ */
+ configurePlugin: function (pluginName, config) {
+ builder.pluginsConfiguration[pluginName] = config;
+ },
+
+ /**
+ * @summary Set the icons for your mobile app.
+ * @param {Object} icons An Object where the keys are different
+ * devices and screen sizes, and values are image paths
+ * relative to the project root directory.
+ *
+ * Valid key values:
+ * - `iphone`
+ * - `iphone_2x`
+ * - `iphone_3x`
+ * - `ipad`
+ * - `ipad_2x`
+ * - `android_ldpi`
+ * - `android_mdpi`
+ * - `android_hdpi`
+ * - `android_xhdpi`
+ * @memberOf App
+ */
+ icons: function (icons) {
+ var validDevices =
+ _.keys(iconsIosSizes).concat(_.keys(iconsAndroidSizes));
+ _.each(icons, function (value, key) {
+ if (!_.include(validDevices, key))
+ throw new Error(key + ": unknown key in App.icons configuration.");
+ });
+ _.extend(builder.imagePaths.icon, icons);
+ },
+
+ /**
+ * @summary Set the launch screen images for your mobile app.
+ * @param {Object} launchScreens A dictionary where keys are different
+ * devices, screen sizes, and orientations, and the values are image paths
+ * relative to the project root directory.
+ *
+ * For Android, launch screen images should
+ * be special "Nine-patch" image files that specify how they should be
+ * stretched. See the [Android docs](https://developer.android.com/guide/topics/graphics/2d-graphics.html#nine-patch).
+ *
+ * Valid key values:
+ * - `iphone`
+ * - `iphone_2x`
+ * - `iphone5`
+ * - `iphone6`
+ * - `iphone6p_portrait`
+ * - `iphone6p_landscape`
+ * - `ipad_portrait`
+ * - `ipad_portrait_2x`
+ * - `ipad_landscape`
+ * - `ipad_landscape_2x`
+ * - `android_ldpi_portrait`
+ * - `android_ldpi_landscape`
+ * - `android_mdpi_portrait`
+ * - `android_mdpi_landscape`
+ * - `android_hdpi_portrait`
+ * - `android_hdpi_landscape`
+ * - `android_xhdpi_portrait`
+ * - `android_xhdpi_landscape`
+ *
+ * @memberOf App
+ */
+ launchScreens: function (launchScreens) {
+ var validDevices =
+ _.keys(launchIosSizes).concat(_.keys(launchAndroidSizes));
+
+ _.each(launchScreens, function (value, key) {
+ if (!_.include(validDevices, key))
+ throw new Error(key + ": unknown key in App.launchScreens configuration.");
+ });
+ _.extend(builder.imagePaths.splash, launchScreens);
+ },
+
+ /**
+ * @summary Set a new access rule based on origin domain for your app.
+ * By default your application has a limited list of servers it can contact.
+ * Use this method to extend this list.
+ *
+ * Default access rules:
+ *
+ * - `tel:*`, `geo:*`, `mailto:*`, `sms:*`, `market:*` are allowed and
+ * launch externally (phone app, or an email client on Android)
+ * - `gap:*`, `cdv:*`, `file:` are allowed (protocols required to access
+ * local file-system)
+ * - `http://meteor.local/*` is allowed (a domain Meteor uses to access
+ * app's assets)
+ * - The domain of the server passed to the build process (or local ip
+ * address in the development mode) is used to be able to contact the
+ * Meteor app server.
+ *
+ * Read more about domain patterns in [Cordova
+ * docs](http://cordova.apache.org/docs/en/4.0.0/guide_appdev_whitelist_index.md.html).
+ *
+ * Starting with Meteor 1.0.4 access rule for all domains and protocols
+ * (``) is no longer set by default due to
+ * [certain kind of possible
+ * attacks](http://cordova.apache.org/announcements/2014/08/04/android-351.html).
+ *
+ * @param {String} domainRule The pattern defining affected domains or URLs.
+ * @param {Object} [options]
+ * @param {Boolean} options.launchExternal Set to true if the matching URL
+ * should be handled externally (e.g. phone app or email client on Android).
+ * @memberOf App
+ */
+ accessRule: function (domainRule, options) {
+ options = options || {};
+ options.launchExternal = !!options.launchExternal;
+ if (options.launchExternal) {
+ builder.accessRules[domainRule] = 'external';
+ } else {
+ builder.accessRules[domainRule] = true;
+ }
+ }
+ };
+}
diff --git a/tools/cordova/cordova-runner.js b/tools/cordova/cordova-runner.js
deleted file mode 100644
index e6e30bee666..00000000000
--- a/tools/cordova/cordova-runner.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import _ from 'underscore';
-import { Console } from '../console.js';
-
-// This is a runner, that we pass to Runner (run-all.js)
-export default class CordovaRunner {
- constructor(projectContext, cordovaProject, options) {
- this.projectContext = projectContext;
- this.cordovaProject = cordovaProject;
- this.options = options;
- }
-
- get title() {
- return `app on ${this.displayName}`;
- }
-
- prestart() {
- // OAuth2 packages don't work so well with any mobile platform except the iOS
- // simulator. Print a warning and direct users to the wiki page for help. (We
- // do this now instead of in start() so we don't have to worry about
- // projectContext being asynchronously reset.)
- if (!(this.platform === "ios" && this.isDevice) &&
- this.projectContext.packageMap.getInfo('oauth2')) {
- Console.warn();
- Console.labelWarn(
- "It looks like you are using OAuth2 login in your app. " +
- "Meteor's OAuth2 implementation does not currently work with " +
- "mobile apps in local development mode, except in the iOS " +
- "simulator. You can run the iOS simulator with 'meteor run ios'. " +
- "For additional workarounds, see " +
- Console.url(
- "https://github.com/meteor/meteor/wiki/" +
- "OAuth-for-mobile-Meteor-clients."));
- }
-
- // If we are targeting the remote devices, warn about ports and same network
- if (this.isDevice) {
- Console.warn();
- Console.labelWarn(
- "You are testing your app on a remote device. " +
- "For the mobile app to be able to connect to the local server, make " +
- "sure your device is on the same network, and that the network " +
- "configuration allows clients to talk to each other " +
- "(no client isolation).");
- }
- }
-
- start() {
- Console.debug('Running Cordova for target', this.displayName);
-
- try {
- Promise.await(this.run(this.options));
- } catch (err) {
- Console.error(`${this.displayName}: failed to start the app.`,
- err.message);
- }
-
- try {
- Promise.await(this.tailLogs(this.options));
- } catch (err) {
- Console.error(`${this.displayName}: failed to tail logs.`,
- err.message);
- }
- }
-}
diff --git a/tools/cordova/index.js b/tools/cordova/index.js
new file mode 100644
index 00000000000..ebda15a3e89
--- /dev/null
+++ b/tools/cordova/index.js
@@ -0,0 +1,43 @@
+import _ from 'underscore';
+
+export const CORDOVA_ARCH = "web.cordova";
+
+export const AVAILABLE_PLATFORMS = ['ios', 'android'];
+
+const PLATFORM_TO_DISPLAY_NAME_MAP = {
+ 'ios': 'iOS',
+ 'android': 'Android'
+};
+
+export function displayNameForPlatform(platform) {
+ return PLATFORM_TO_DISPLAY_NAME_MAP[platform] || platform;
+};
+
+export function filterPlatforms(platforms) {
+ return _.intersection(platforms, AVAILABLE_PLATFORMS);
+}
+
+export function splitPluginsAndPackages(packages) {
+ let result = {
+ plugins: [],
+ packages: []
+ };
+
+ for (package of packages) {
+ const [namespace, ...rest] = package.split(':');
+ if (namespace === 'cordova') {
+ const name = rest.join(':');
+ result.plugins.push(name);
+ } else {
+ result.packages.push(package);
+ }
+ }
+
+ return result;
+}
+
+// Returns the cordovaDependencies of the Cordova arch from a star manifest.
+export function pluginsFromStarManifest(star) {
+ var cordovaProgram = _.findWhere(star.programs, { arch: CORDOVA_ARCH });
+ return cordovaProgram ? cordovaProgram.cordovaDependencies : {};
+}
diff --git a/tools/cordova/ios-runner.js b/tools/cordova/ios-runner.js
deleted file mode 100644
index 3ee319d1b17..00000000000
--- a/tools/cordova/ios-runner.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import _ from 'underscore';
-import chalk from 'chalk';
-import { Console } from '../console.js';
-import files from '../fs/files.js';
-import isopackets from '../tool-env/isopackets.js'
-
-import CordovaRunner from './cordova-runner.js'
-import { execFileSyncOrThrow, execFileAsyncOrThrow } from './utils.js'
-
-export default class iOSRunner extends CordovaRunner {
- constructor(projectContext, cordovaProject, isDevice, options) {
- super(projectContext, cordovaProject, options);
- this.isDevice = isDevice;
- }
-
- get platform() {
- return 'ios';
- }
-
- get displayName() {
- return this.isDevice ? 'iOS Device' : 'iOS Simulator';
- }
-
- async run(options = {}) {
- // ios-deploy is super buggy, so we just open xcode and let the user
- // start the app themselves.
- if (this.isDevice) {
- openInXcode(files.pathJoin(this.cordovaProject.projectRoot, 'platforms', 'ios'));
- } else {
- const cordovaBinPath = files.convertToOSPath(
- files.pathJoin(files.getCurrentToolsDir(),
- 'packages/cordova/.npm/package/node_modules/.bin'));
- return this.cordovaProject.run(this.platform, this.isDevice,
- _.extend(options, { extraPaths: [cordovaBinPath] }));
- }
- }
-
- async tailLogs(options) {
- var logFilePath =
- files.pathJoin(this.cordovaProject.projectRoot, 'platforms', 'ios', 'cordova', 'console.log');
- Console.debug('Printing logs for ios emulator, tailing file', logFilePath);
-
- // overwrite the file so we don't have to print the old logs
- files.writeFile(logFilePath, '');
- // print the log file
- execFileAsyncOrThrow('tail', ['-f', logFilePath], {
- verbose: true,
- lineMapper: null
- });
- }
-}
-
-function openInXcode(projectDir) {
- // XXX this is buggy if your app directory is under something with a space,
- // because the this.projectRoot part is not quoted for sh!
- args = ['-c', 'open ' +
- '"' + projectDir.replace(/"/g, "\\\"") + '"/*.xcodeproj'];
-
- try {
- execFileSyncOrThrow('sh', args);
- } catch (err) {
- Console.error();
- Console.error(chalk.green("Could not open your project in Xcode."));
- Console.error(chalk.green("Try running again with the --verbose option."));
- Console.error(
- chalk.green("Instructions for running your app on an iOS device: ") +
- Console.url(
- "https://github.com/meteor/meteor/wiki/" +
- "How-to-run-your-app-on-an-iOS-device")
- );
- Console.error();
- process.exit(2);
- }
-
- Console.info();
- Console.info(
- chalk.green(
- "Your project has been opened in Xcode so that you can run your " +
- "app on an iOS device. For further instructions, visit this " +
- "wiki page: ") +
- Console.url(
- "https://github.com/meteor/meteor/wiki/" +
- "How-to-run-your-app-on-an-iOS-device"
- ));
- Console.info();
-}
diff --git a/tools/cordova/mobile-control-file.js b/tools/cordova/mobile-control-file.js
deleted file mode 100644
index 52898225a6e..00000000000
--- a/tools/cordova/mobile-control-file.js
+++ /dev/null
@@ -1,441 +0,0 @@
-import _ from 'underscore';
-import { Console } from '../console.js';
-import files from '../fs/files.js';
-import isopackets from '../tool-env/isopackets.js'
-
-// Hard-coded constants
-var iconIosSizes = {
- 'iphone': '60x60',
- 'iphone_2x': '120x120',
- 'iphone_3x': '180x180',
- 'ipad': '76x76',
- 'ipad_2x': '152x152'
-};
-
-var iconAndroidSizes = {
- 'android_ldpi': '36x36',
- 'android_mdpi': '42x42',
- 'android_hdpi': '72x72',
- 'android_xhdpi': '96x96'
-};
-
-var launchIosSizes = {
- 'iphone': '320x480',
- 'iphone_2x': '640x960',
- 'iphone5': '640x1136',
- 'iphone6': '750x1334',
- 'iphone6p_portrait': '1242x2208',
- 'iphone6p_landscape': '2208x1242',
- 'ipad_portrait': '768x1004',
- 'ipad_portrait_2x': '1536x2008',
- 'ipad_landscape': '1024x748',
- 'ipad_landscape_2x': '2048x1496'
-};
-
-var launchAndroidSizes = {
- 'android_ldpi_portrait': '320x426',
- 'android_ldpi_landscape': '426x320',
- 'android_mdpi_portrait': '320x470',
- 'android_mdpi_landscape': '470x320',
- 'android_hdpi_portrait': '480x640',
- 'android_hdpi_landscape': '640x480',
- 'android_xhdpi_portrait': '720x960',
- 'android_xhdpi_landscape': '960x720'
-};
-
-// Given the mobile control file converts it to the Phongep/Cordova project's
-// config.xml file and copies the necessary files (icons and launch screens) to
-// the correct build location. Replaces all the old resources.
-export function processMobileControlFile(controlFilePath, projectContext, cordovaProject, serverDomain) {
- Console.debug('Processing the mobile control file');
-
- // clean up the previous settings and resources
- files.rm_recursive(files.pathJoin(cordovaProject.projectRoot, 'resources'));
-
- var code = '';
-
- if (files.exists(controlFilePath)) {
- // read the file if it exists
- code = files.readFile(controlFilePath, 'utf8');
- }
-
- var defaultBuildNumber = (Date.now() % 1000000).toString();
- var metadata = {
- id: 'com.id' + projectContext.appIdentifier,
- version: '0.0.1',
- buildNumber: defaultBuildNumber,
- name: cordovaProject.appName,
- description: 'New Meteor Mobile App',
- author: 'A Meteor Developer',
- email: 'n/a',
- website: 'n/a'
- };
-
- // set some defaults different from the Phonegap/Cordova defaults
- var additionalConfiguration = {
- 'webviewbounce': false,
- 'DisallowOverscroll': true,
- 'deployment-target': '7.0'
- };
-
- if (projectContext.packageMap.getInfo('launch-screen')) {
- additionalConfiguration.AutoHideSplashScreen = false;
- additionalConfiguration.SplashScreen = 'screen';
- additionalConfiguration.SplashScreenDelay = 10000;
- }
-
- // Defaults are Meteor meatball images located in tools/cordova/assets directory
- var assetsPath = files.pathJoin(__dirname, 'assets');
- var iconsPath = files.pathJoin(assetsPath, 'icons');
- var launchscreensPath = files.pathJoin(assetsPath, 'launchscreens');
- var imagePaths = {
- icon: {},
- splash: {}
- };
-
- // Default access rules for plain Meteor-Cordova apps.
- // Rules can be extended with mobile-config API described below.
- // The value is `true` if the protocol or domain should be allowed,
- // 'external' if should handled externally.
- var accessRules = {
- // Allow external calls to things like email client or maps app or a
- // phonebook app.
- 'tel:*': 'external',
- 'geo:*': 'external',
- 'mailto:*': 'external',
- 'sms:*': 'external',
- 'market:*': 'external',
-
- // phonegap/cordova related protocols
- // "file:" protocol is used to access first files from disk
- 'file:*': true,
- 'cdv:*': true,
- 'gap:*': true,
-
- // allow Meteor's local emulated server url - this is the url from which the
- // application loads its assets
- 'http://meteor.local/*': true
- };
-
- // If the remote server domain is known, allow access to it for xhr and DDP
- // connections.
- if (serverDomain) {
- accessRules['*://' + serverDomain + '/*'] = true;
- // Android talks to localhost over 10.0.2.2. This config file is used for
- // multiple platforms, so any time that we say the server is on localhost we
- // should also say it is on 10.0.2.2.
- if (serverDomain === 'localhost') {
- accessRules['*://10.0.2.2/*'] = true;
- }
- }
-
- var setIcon = function (size, name) {
- imagePaths.icon[name] = files.pathJoin(iconsPath, size + '.png');
- };
- var setLaunch = function (size, name) {
- imagePaths.splash[name] = files.pathJoin(launchscreensPath, size + '.png');
- };
-
- _.each(iconIosSizes, setIcon);
- _.each(iconAndroidSizes, setIcon);
- _.each(launchIosSizes, setLaunch);
- _.each(launchAndroidSizes, setLaunch);
-
- /**
- * @namespace App
- * @global
- * @summary The App configuration object in mobile-config.js
- */
- var App = {
- /**
- * @summary Set your mobile app's core configuration information.
- * @param {Object} options
- * @param {String} [options.id,version,name,description,author,email,website]
- * Each of the options correspond to a key in the app's core configuration
- * as described in the [PhoneGap documentation](http://docs.phonegap.com/en/3.5.0/config_ref_index.md.html#The%20config.xml%20File_core_configuration_elements).
- * @memberOf App
- */
- info: function (options) {
- // check that every key is meaningful
- _.each(options, function (value, key) {
- if (!_.has(metadata, key))
- throw new Error("Unknown key in App.info configuration: " + key);
- });
-
- _.extend(metadata, options);
- },
- /**
- * @summary Add a preference for your build as described in the
- * [PhoneGap documentation](http://docs.phonegap.com/en/3.5.0/config_ref_index.md.html#The%20config.xml%20File_global_preferences).
- * @param {String} name A preference name supported by Phonegap's
- * `config.xml`.
- * @param {String} value The value for that preference.
- * @memberOf App
- */
- setPreference: function (key, value) {
- additionalConfiguration[key] = value;
- },
-
- /**
- * @summary Set the build-time configuration for a Phonegap plugin.
- * @param {String} pluginName The identifier of the plugin you want to
- * configure.
- * @param {Object} config A set of key-value pairs which will be passed
- * at build-time to configure the specified plugin.
- * @memberOf App
- */
- configurePlugin: function (pluginName, config) {
- pluginsConfiguration[pluginName] = config;
- },
-
- /**
- * @summary Set the icons for your mobile app.
- * @param {Object} icons An Object where the keys are different
- * devices and screen sizes, and values are image paths
- * relative to the project root directory.
- *
- * Valid key values:
- * - `iphone`
- * - `iphone_2x`
- * - `iphone_3x`
- * - `ipad`
- * - `ipad_2x`
- * - `android_ldpi`
- * - `android_mdpi`
- * - `android_hdpi`
- * - `android_xhdpi`
- * @memberOf App
- */
- icons: function (icons) {
- var validDevices =
- _.keys(iconIosSizes).concat(_.keys(iconAndroidSizes));
- _.each(icons, function (value, key) {
- if (!_.include(validDevices, key))
- throw new Error(key + ": unknown key in App.icons configuration.");
- });
- _.extend(imagePaths.icon, icons);
- },
-
- /**
- * @summary Set the launch screen images for your mobile app.
- * @param {Object} launchScreens A dictionary where keys are different
- * devices, screen sizes, and orientations, and the values are image paths
- * relative to the project root directory.
- *
- * For Android, launch screen images should
- * be special "Nine-patch" image files that specify how they should be
- * stretched. See the [Android docs](https://developer.android.com/guide/topics/graphics/2d-graphics.html#nine-patch).
- *
- * Valid key values:
- * - `iphone`
- * - `iphone_2x`
- * - `iphone5`
- * - `iphone6`
- * - `iphone6p_portrait`
- * - `iphone6p_landscape`
- * - `ipad_portrait`
- * - `ipad_portrait_2x`
- * - `ipad_landscape`
- * - `ipad_landscape_2x`
- * - `android_ldpi_portrait`
- * - `android_ldpi_landscape`
- * - `android_mdpi_portrait`
- * - `android_mdpi_landscape`
- * - `android_hdpi_portrait`
- * - `android_hdpi_landscape`
- * - `android_xhdpi_portrait`
- * - `android_xhdpi_landscape`
- *
- * @memberOf App
- */
- launchScreens: function (launchScreens) {
- var validDevices =
- _.keys(launchIosSizes).concat(_.keys(launchAndroidSizes));
-
- _.each(launchScreens, function (value, key) {
- if (!_.include(validDevices, key))
- throw new Error(key + ": unknown key in App.launchScreens configuration.");
- });
- _.extend(imagePaths.splash, launchScreens);
- },
-
- /**
- * @summary Set a new access rule based on origin domain for your app.
- * By default your application has a limited list of servers it can contact.
- * Use this method to extend this list.
- *
- * Default access rules:
- *
- * - `tel:*`, `geo:*`, `mailto:*`, `sms:*`, `market:*` are allowed and
- * launch externally (phone app, or an email client on Android)
- * - `gap:*`, `cdv:*`, `file:` are allowed (protocols required to access
- * local file-system)
- * - `http://meteor.local/*` is allowed (a domain Meteor uses to access
- * app's assets)
- * - The domain of the server passed to the build process (or local ip
- * address in the development mode) is used to be able to contact the
- * Meteor app server.
- *
- * Read more about domain patterns in [Cordova
- * docs](http://cordova.apache.org/docs/en/4.0.0/guide_appdev_whitelist_index.md.html).
- *
- * Starting with Meteor 1.0.4 access rule for all domains and protocols
- * (``) is no longer set by default due to
- * [certain kind of possible
- * attacks](http://cordova.apache.org/announcements/2014/08/04/android-351.html).
- *
- * @param {String} domainRule The pattern defining affected domains or URLs.
- * @param {Object} [options]
- * @param {Boolean} options.launchExternal Set to true if the matching URL
- * should be handled externally (e.g. phone app or email client on Android).
- * @memberOf App
- */
- accessRule: function (domainRule, options) {
- options = options || {};
- options.launchExternal = !!options.launchExternal;
- if (options.launchExternal) {
- accessRules[domainRule] = 'external';
- } else {
- accessRules[domainRule] = true;
- }
- }
- };
-
- try {
- Console.debug('Running the mobile control file');
- files.runJavaScript(code, {
- filename: 'mobile-config.js',
- symbols: { App: App }
- });
- } catch (err) {
- throw new Error('Error reading mobile-config.js:' + err.stack);
- }
-
- const { XmlBuilder } = isopackets.load('cordova-support')['xmlbuilder'];
- var config = XmlBuilder.create('widget');
-
- _.each({
- id: metadata.id,
- version: metadata.version,
- 'android-versionCode': metadata.buildNumber,
- 'ios-CFBundleVersion': metadata.buildNumber,
- xmlns: 'http://www.w3.org/ns/widgets',
- 'xmlns:cdv': 'http://cordova.apache.org/ns/1.0'
- }, function (val, key) {
- config.att(key, val);
- });
-
- // set all the metadata
- config.ele('name').txt(metadata.name);
- config.ele('description').txt(metadata.description);
- config.ele('author', {
- href: metadata.website,
- email: metadata.email
- }).txt(metadata.author);
-
- // set the additional configuration preferences
- _.each(additionalConfiguration, function (value, key) {
- config.ele('preference', {
- name: key,
- value: value.toString()
- });
- });
-
- // load from index.html by default
- config.ele('content', { src: 'index.html' });
-
- // Copy all the access rules
- _.each(accessRules, function (rule, pattern) {
- var opts = { origin: pattern };
- if (rule === 'external')
- opts['launch-external'] = true;
-
- config.ele('access', opts);
- });
-
- var iosPlatform = config.ele('platform', { name: 'ios' });
- var androidPlatform = config.ele('platform', { name: 'android' });
-
- // Prepare the resources folder
- var resourcesPath = files.pathJoin(cordovaProject.projectRoot, 'resources');
- files.rm_recursive(resourcesPath);
- files.mkdir_p(resourcesPath);
-
- Console.debug('Copying resources for mobile apps');
-
- var imageXmlRec = function (name, width, height, src) {
- var androidMatch = /android_(.?.dpi)_(landscape|portrait)/g.exec(name);
- var xmlRec = {
- src: src,
- width: width,
- height: height
- };
-
- // XXX special case for Android
- if (androidMatch)
- xmlRec.density = androidMatch[2].substr(0, 4) + '-' + androidMatch[1];
-
- return xmlRec;
- };
- var setImages = function (sizes, xmlEle, tag) {
- _.each(sizes, function (size, name) {
- var width = size.split('x')[0];
- var height = size.split('x')[1];
-
- var suppliedPath = imagePaths[tag][name];
- if (!suppliedPath)
- return;
-
- var suppliedFilename = _.last(suppliedPath.split(files.pathSep));
- var extension = _.last(suppliedFilename.split('.'));
-
- // XXX special case for 9-patch png's
- if (suppliedFilename.match(/\.9\.png$/)) {
- extension = '9.png';
- }
-
- var fileName = name + '.' + tag + '.' + extension;
- var src = files.pathJoin('resources', fileName);
-
- // copy the file to the build folder with a standardized name
- files.copyFile(files.pathResolve(projectContext.projectDir, suppliedPath),
- files.pathJoin(resourcesPath, fileName));
-
- // set it to the xml tree
- xmlEle.ele(tag, imageXmlRec(name, width, height, src));
-
- // XXX reuse one size for other dimensions
- var dups = {
- '60x60': ['29x29', '40x40', '50x50', '57x57', '58x58'],
- '76x76': ['72x72'],
- '152x152': ['144x144'],
- '120x120': ['80x80', '100x100', '114x114'],
- '768x1004': ['768x1024'],
- '1536x2008': ['1536x2048'],
- '1024x748': ['1024x768'],
- '2048x1496': ['2048x1536']
- }[size];
-
- // just use the same image
- _.each(dups, function (size) {
- width = size.split('x')[0];
- height = size.split('x')[1];
- // XXX this is fine to not supply a name since it is always iOS, but
- // this is a hack right now.
- xmlEle.ele(tag, imageXmlRec('n/a', width, height, src));
- });
- });
- };
-
- // add icons and launch screens to config and copy the files on fs
- setImages(iconIosSizes, iosPlatform, 'icon');
- setImages(iconAndroidSizes, androidPlatform, 'icon');
- setImages(launchIosSizes, iosPlatform, 'splash');
- setImages(launchAndroidSizes, androidPlatform, 'splash');
-
- var formattedXmlConfig = config.end({ pretty: true });
- var configPath = files.pathJoin(cordovaProject.projectRoot, 'config.xml');
-
- Console.debug('Writing new config.xml');
- files.writeFile(configPath, formattedXmlConfig, 'utf8');
-};
diff --git a/tools/cordova/platforms.js b/tools/cordova/platforms.js
deleted file mode 100644
index 1a931bdef53..00000000000
--- a/tools/cordova/platforms.js
+++ /dev/null
@@ -1,117 +0,0 @@
-import _ from 'underscore';
-import chalk from 'chalk';
-import main from '../cli/main.js';
-import { Console } from '../console.js';
-import { ProjectContext, PlatformList } from '../project-context.js';
-import buildmessage from '../buildmessage.js';
-
-export const AVAILABLE_PLATFORMS = PlatformList.DEFAULT_PLATFORMS.concat(
- ['android', 'ios']);
-
-const PLATFORM_TO_DISPLAY_NAME_MAP = {
- 'ios': 'iOS',
- 'android': 'Android'
-};
-
-export function displayNameForPlatform(platform) {
- return PLATFORM_TO_DISPLAY_NAME_MAP[platform] || platform;
-};
-
-export function platformsForTargets(targets) {
- targets = _.uniq(targets);
-
- var platforms = [];
- // Find the platforms that correspond to the targets
- // ie. ["ios", "android", "ios-device"] will produce ["ios", "android"]
- _.each(targets, function (targetName) {
- var platform = targetName.split('-')[0];
- if (!_.contains(platforms, platform)) {
- platforms.push(platform);
- }
- });
-
- return platforms;
-};
-
-// Ensures that the Cordova platforms are synchronized with the app-level
-// platforms.
-export function ensureCordovaPlatformsAreSynchronized(cordovaProject, platforms) {
- // Filter out the default platforms, leaving the Cordova platforms
- platforms = _.difference(platforms, PlatformList.DEFAULT_PLATFORMS);
- const installedPlatforms = cordovaProject.getInstalledPlatforms();
-
- for (platform of platforms) {
- if (_.contains(installedPlatforms, platform)) continue;
-
- buildmessage.enterJob(`Adding platform: ${platform}`, () => {
- Promise.await(cordovaProject.addPlatform(platform));
- });
- }
-
- for (platform of installedPlatforms) {
- if (!_.contains(platforms, platform) &&
- _.contains(AVAILABLE_PLATFORMS, platform)) {
- buildmessage.enterJob(`Removing platform: ${platform}`, () => {
- Promise.await(cordovaProject.removePlatform(platform));
- });
- }
- }
-};
-
-export function checkPlatformRequirements(cordovaProject, platform) {
- const requirements = Promise.await(cordovaProject.checkRequirements([platform]));
- let platformRequirements = requirements[platform];
-
- if (!platformRequirements) {
- Console.warn("Could not check platform requirements");
- return;
- }
-
- // We don't use ios-deploy, but open Xcode to run on a device instead
- platformRequirements = _.reject(platformRequirements, requirement => requirement.id === 'ios-deploy');
-
- const satisifed = _.every(platformRequirements, requirement => requirement.installed);
-
- if (!satisifed) {
- Console.info(`Make sure all installation requirements are satisfied
- before running or building for ${displayNameForPlatform(platform)}:`);
- for (requirement of platformRequirements) {
- const name = requirement.name;
- if (requirement.installed) {
- Console.success(name);
- } else {
- const reason = requirement.metadata && requirement.metadata.reason;
- if (reason) {
- Console.failInfo(`${name}: ${reason}`);
- } else {
- Console.failInfo(name);
- }
- }
- }
- }
-
- return satisifed;
-}
-
-// Filter out unsupported Cordova platforms, and exit if platform hasn't been
-// added to the project yet
-export function checkCordovaPlatforms(projectContext, platforms) {
- var cordovaPlatformsInProject = projectContext.platformList.getCordovaPlatforms();
- return _.filter(platforms, function (platform) {
- var inProject = _.contains(cordovaPlatformsInProject, platform);
-
- if (platform === 'ios' && process.platform !== 'darwin') {
- Console.warn("Currently, it is only possible to build iOS apps on an OS X system.");
- return false;
- }
-
- if (!inProject) {
- Console.warn("Please add the " + displayNameForPlatform(platform) +
- " platform to your project first.");
- Console.info("Run: " + Console.command("meteor add-platform " + platform));
- throw new main.ExitWithCode(2);
- }
-
- return true;
- });
-}
diff --git a/tools/cordova/plugins.js b/tools/cordova/plugins.js
deleted file mode 100644
index b41e43f19cc..00000000000
--- a/tools/cordova/plugins.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import _ from 'underscore';
-import { Console } from '../console.js';
-import buildmessage from '../buildmessage.js';
-import files from '../fs/files.js';
-import utils from '../utils/utils.js';
-
-// packages - list of strings
-export function filterCordovaPackages(packages) {
-// We hard-code the 'cordova' namespace
- var ret = {
- rest: [],
- plugins: []
- };
-
- _.each(packages, function (p) {
- var namespace = p.split(':')[0];
- var name = p.split(':').slice(1).join(':');
- if (namespace === 'cordova') {
- ret.plugins.push(name);
- } else {
- ret.rest.push(p); // leave it the same
- }
- });
- return ret;
-}
-
-// Ensures that the Cordova plugins are synchronized with the app-level
-// plugins.
-export function ensureCordovaPluginsAreSynchronized(cordovaProject, plugins,
- pluginsConfiguration = {}) {
- Console.debug('Ensuring that the Cordova plugins are synchronized with the app-level plugins', plugins);
-
- var installedPlugins = Promise.await(cordovaProject.getInstalledPlugins());
-
- // Due to the dependency structure of Cordova plugins, it is impossible to
- // upgrade the version on an individual Cordova plugin. Instead, whenever a
- // new Cordova plugin is added or removed, or its version is changed,
- // we just reinstall all of the plugins.
- var shouldReinstallPlugins = false;
-
- // Iterate through all of the plugins and find if any of them have a new
- // version. Additionally check if we have plugins installed from local path.
- var pluginsFromLocalPath = {};
- _.each(plugins, function (version, name) {
- // Check if plugin is installed from local path
- let pluginFromLocalPath = utils.isUrlWithFileScheme(version);
- if (pluginFromLocalPath) {
- pluginsFromLocalPath[name] = version;
- }
-
- // XXX there is a hack here that never updates a package if you are
- // trying to install it from a URL, because we can't determine if
- // it's the right version or not
- if (!_.has(installedPlugins, name) ||
- (installedPlugins[name] !== version && !pluginFromLocalPath)) {
- // The version of the plugin has changed, or we do not contain a plugin.
- shouldReinstallPlugins = true;
- }
- });
-
- if (!_.isEmpty(pluginsFromLocalPath)) {
- Console.debug('Reinstalling Cordova plugins added from the local path');
- }
-
- // Check to see if we have any installed plugins that are not in the current
- // set of plugins.
- _.each(installedPlugins, function (version, name) {
- if (!_.has(plugins, name)) {
- shouldReinstallPlugins = true;
- }
- });
-
- if (shouldReinstallPlugins || !_.isEmpty(pluginsFromLocalPath)) {
- buildmessage.enterJob({ title: "installing Cordova plugins"}, function () {
- installedPlugins = Promise.await(cordovaProject.getInstalledPlugins());
-
- if (shouldReinstallPlugins) {
- cordovaProject.removePlugins(installedPlugins);
- } else {
- cordovaProject.removePlugins(pluginsFromLocalPath);
- }
-
- // Now install necessary plugins.
- var pluginsInstalled, pluginsToInstall;
-
- if (shouldReinstallPlugins) {
- pluginsInstalled = 0;
- pluginsToInstall = plugins;
- } else {
- pluginsInstalled = _.size(installedPlugins);
- pluginsToInstall = pluginsFromLocalPath;
- }
-
- var pluginsCount = _.size(plugins);
-
- buildmessage.reportProgress({ current: 0, end: pluginsCount });
- _.each(pluginsToInstall, function (version, name) {
- Promise.await(cordovaProject.addPlugin(name, version, pluginsConfiguration[name]));
-
- buildmessage.reportProgress({
- current: ++pluginsInstalled,
- end: pluginsCount
- });
- });
- });
- }
-};
diff --git a/tools/cordova/project.js b/tools/cordova/project.js
index 2d0425de3eb..157a54e147c 100644
--- a/tools/cordova/project.js
+++ b/tools/cordova/project.js
@@ -1,12 +1,20 @@
import _ from 'underscore';
+import util from 'util';
+import path from 'path';
+import assert from 'assert';
import chalk from 'chalk';
+
import isopackets from '../tool-env/isopackets.js'
import files from '../fs/files.js';
import utils from '../utils/utils.js';
import { Console } from '../console/console.js';
import buildmessage from '../utils/buildmessage.js';
+import main from '../cli/main.js';
import httpHelpers from '../utils/http-helpers.js';
+import { AVAILABLE_PLATFORMS, displayNameForPlatform } from './index.js';
+import { CordovaBuilder } from './builder.js';
+
function loadDependenciesFromCordovaPackageIfNeeded() {
if (typeof Cordova !== 'undefined') return;
@@ -15,104 +23,214 @@ function loadDependenciesFromCordovaPackageIfNeeded() {
events.on('results', logIfVerbose);
events.on('log', logIfVerbose);
- events.on('warn', console.warn);
+ events.on('warn', log);
events.on('verbose', logIfVerbose);
}
-const logIfVerbose = (...args) => {
+function logIfVerbose(...args) {
if (Console.verbose) {
- console.log(args);
+ log(...args);
}
};
-// Creates a Cordova project if necessary.
-export function createCordovaProjectIfNecessary(projectContext) {
- const cordovaPath = projectContext.getProjectLocalDirectory('cordova-build');
- const appName = files.pathBasename(projectContext.projectDir);
- const cordovaProject = new CordovaProject(cordovaPath, appName);
-
- if (!files.exists(cordovaPath)) {
- Console.debug('Cordova project doesn\'t exist, creating one');
- files.mkdir_p(files.pathDirname(cordovaPath));
- Promise.await(cordovaProject.create());
- }
-
- return cordovaProject;
-};
+function log(...args) {
+ Console.rawInfo(`%% ${util.format.apply(null, args)}\n`);
+}
-export default class CordovaProject {
- constructor(projectRoot, appName) {
+export class CordovaProject {
+ constructor(projectContext, appName = files.pathBasename(projectContext.projectDir)) {
loadDependenciesFromCordovaPackageIfNeeded();
- this.projectRoot = projectRoot;
+ this.projectContext = projectContext;
+
+ this.projectRoot = projectContext.getProjectLocalDirectory('cordova-build');
this.appName = appName;
this.pluginsDir = files.pathJoin(this.projectRoot, 'plugins');
this.localPluginsDir = files.pathJoin(this.projectRoot, 'local-plugins');
this.tarballPluginsLockPath = files.pathJoin(this.projectRoot, 'cordova-tarball-plugins.json');
- }
- async create() {
- // Cordova app identifiers have to look like Java namespaces.
- // Change weird characters (especially hyphens) into underscores.
- const appId = 'com.meteor.userapps.' + this.appName.replace(/[^a-zA-Z\d_$.]/g, '_');
- return await cordova.raw.create(files.convertToOSPath(this.projectRoot), appId, this.appName);
+ this.createIfNeeded();
}
- chdirToProjectRoot() {
- process.chdir(files.convertToOSPath(this.projectRoot));
+ // Creating
+
+ createIfNeeded() {
+ buildmessage.assertInCapture();
+
+ if (!files.exists(this.projectRoot)) {
+ buildmessage.enterJob({ title: "creating Cordova project" }, () => {
+ files.mkdir_p(files.pathDirname(this.projectRoot));
+ // Cordova app identifiers have to look like Java namespaces.
+ // Change weird characters (especially hyphens) into underscores.
+ const appId = 'com.meteor.userapps.' + this.appName.replace(/[^a-zA-Z\d_$.]/g, '_');
+
+ // Don't set cwd to project root in runCommands because it doesn't exist yet
+ this.runCommands(async () => {
+ await cordova.raw.create(files.convertToOSPath(this.projectRoot), appId, this.appName);
+ }, this.defaultEnvWithPathsAdded(), null);
+ });
+ }
}
- get defaultOptions() {
- return { silent: !Console.verbose, verbose: Console.verbose };
+ // Preparing
+
+ prepare(bundlePath, plugins, options = {}) {
+ assert(bundlePath);
+ assert(plugins);
+
+ Console.debug('Preparing Cordova project');
+
+ const builder = new CordovaBuilder(this, bundlePath, plugins, options);
+ builder.start();
}
- env(...extraPaths) {
- let paths = (this.defaultPaths || []);
- paths.unshift(...extraPaths);
- const env = files.currentEnvWithPathsAdded(...paths);
- return env;
+ // Building
+
+ build(platforms = this.installedPlatforms, options = [], extraPaths) {
+ const env = this.defaultEnvWithPathsAdded(...extraPaths);
+ const commandOptions = _.extend(this.defaultOptions,
+ { platforms: platforms, options: options });
+
+ Console.debug('Building Cordova project', commandOptions);
+
+ this.runCommands(async () => {
+ await cordova.raw.build(commandOptions);
+ });
}
- get defaultPaths() {
- const nodeBinDir = files.getCurrentNodeBinDir();
- return [nodeBinDir];
+ // Running
+
+ async run(platform, isDevice, options = [], extraPaths) {
+ const env = this.defaultEnvWithPathsAdded(...extraPaths);
+ const commandOptions = _.extend(this.defaultOptions,
+ { platforms: [platform], options: options });
+
+ Console.debug('Running Cordova project', commandOptions);
+
+
+ this.runCommands(async () => {
+ if (isDevice) {
+ await cordova.raw.run(commandOptions);
+ } else {
+ await cordova.raw.emulate(commandOptions);
+ }
+ }, env);
}
// Platforms
- async checkRequirements(platforms = null) {
- this.chdirToProjectRoot();
- superspawn.setEnv(this.env());
- return await cordova.raw.requirements(platforms, this.defaultOptions);
+ checkPlatformRequirements(platform) {
+ if (platform === 'ios' && process.platform !== 'darwin') {
+ Console.warn("Currently, it is only possible to build iOS apps on an OS X system.");
+ return false;
+ }
+
+ const installedPlatforms = this.installedPlatforms;
+ const inProject = _.contains(installedPlatforms, platform);
+ if (!inProject) {
+ Console.warn(`Please add the ${displayNameForPlatform(platform)} \
+platform to your project first.`);
+ Console.info(`Run: ${Console.command(`meteor add-platform ${platform}`)}`);
+ return false;
+ }
+
+ const allRequirements = this.runCommands(
+ async () => {
+ return await cordova.raw.requirements([platform], this.defaultOptions);
+ });
+ let requirements = allRequirements && allRequirements[platform];
+ if (!requirements) {
+ Console.error(`Failed to check requirements for platform \
+${displayNameForPlatform(platform)}`);
+ return false;
+ } else if (requirements instanceof CordovaError) {
+ Console.error(`cordova: ${requirements.message}`);
+ return false;
+ }
+
+ // We don't use ios-deploy, but open Xcode to run on a device instead
+ requirements = _.reject(requirements, requirement => requirement.id === 'ios-deploy');
+
+ const satisfied = _.every(requirements, requirement => requirement.installed);
+ if (!satisfied) {
+ Console.info();
+ Console.info(`Make sure all installation requirements are satisfied \
+before running or building for ${displayNameForPlatform(platform)}:`);
+ for (requirement of requirements) {
+ const name = requirement.name;
+ if (requirement.installed) {
+ Console.success(name);
+ } else {
+ const reason = requirement.metadata && requirement.metadata.reason;
+ if (reason) {
+ Console.failInfo(`${name}: ${reason}`);
+ } else {
+ Console.failInfo(name);
+ }
+ }
+ }
+ }
+ return satisfied;
}
- getInstalledPlatforms() {
+ get installedPlatforms() {
return cordova_util.listPlatforms(files.convertToOSPath(this.projectRoot));
}
- async addPlatform(platform) {
- this.chdirToProjectRoot();
- superspawn.setEnv(this.env());
- return await cordova.raw.platform('add', platform, this.defaultOptions);
+ updatePlatforms(platforms = this.installedPlatforms) {
+ this.runCommands(async () => {
+ await cordova.raw.platform('update', platforms, this.defaultOptions);
+ });
+ }
+
+ addPlatform(platform) {
+ this.runCommands(async () => {
+ await cordova.raw.platform('add', platform, this.defaultOptions);
+ });
+ }
+
+ removePlatform(platform) {
+ this.runCommands(async () => {
+ await cordova.raw.platform('rm', platform, this.defaultOptions);
+ });
}
- async removePlatform(platform) {
- this.chdirToProjectRoot();
- superspawn.setEnv(this.env());
- return await cordova.raw.platform('rm', platform, this.defaultOptions);
+ get cordovaPlatformsInApp() {
+ return this.projectContext.platformList.getCordovaPlatforms();
+ }
+
+ // Ensures that the Cordova platforms are synchronized with the app-level
+ // platforms.
+ ensurePlatformsAreSynchronized(platforms = this.cordovaPlatformsInApp) {
+ const installedPlatforms = this.installedPlatforms;
+
+ for (platform of platforms) {
+ if (_.contains(installedPlatforms, platform)) continue;
+
+ this.addPlatform(platform);
+ }
+
+ for (platform of installedPlatforms) {
+ if (!_.contains(platforms, platform) &&
+ _.contains(AVAILABLE_PLATFORMS, platform)) {
+ this.removePlatform(platform);
+ }
+ }
}
// Plugins
- getInstalledPlugins() {
- let pluginInfoProvider = new PluginInfoProvider();
- return _.object(_.map(pluginInfoProvider.getAllWithinSearchPath(files.convertToOSPath(this.pluginsDir)), plugin => {
+ get installedPlugins() {
+ const pluginInfoProvider = new PluginInfoProvider();
+ const plugins = pluginInfoProvider.getAllWithinSearchPath(
+ files.convertToOSPath(this.pluginsDir));
+ return _.object(plugins.map(plugin => {
return [ plugin.id, plugin.version ];
}));
}
- async addPlugin(name, version, config) {
+ addPlugin(name, version, config) {
let pluginTarget;
if (version && utils.isUrlWithSha(version)) {
pluginTarget = files.convertToOSPath(this.fetchCordovaPluginFromShaUrl(version, name));
@@ -123,6 +241,8 @@ export default class CordovaProject {
pluginTarget = version ? `${name}@${version}` : name;
}
+ Console.debug('Adding a Cordova plugin', pluginTarget);
+
let additionalArgs = [];
_.each(config || {}, (value, variable) => {
additionalArgs.push('--variable');
@@ -130,44 +250,42 @@ export default class CordovaProject {
});
pluginTarget.concat(additionalArgs)
- this.chdirToProjectRoot();
- superspawn.setEnv(this.env());
- return await cordova.raw.plugin('add', pluginTarget, this.defaultOptions);
+ this.runCommands(async () => {
+ await cordova.raw.plugin('add', pluginTarget, this.defaultOptions);
+ });
+
+ if (utils.isUrlWithSha(version)) {
+ Console.debug('Adding plugin to the tarball plugins lock', name);
+ let lock = this.getTarballPluginsLock(this.projectRoot);
+ lock[name] = version;
+ this.writeTarballPluginsLock(this.projectRoot, lock);
+ }
}
- async removePlugin(plugin, isFromTarballUrl = false) {
- verboseLog('Removing a plugin', name);
+ removePlugin(plugin, isFromTarballUrl = false) {
+ Console.debug('Removing a Cordova plugin', plugin);
- this.chdirToProjectRoot();
- superspawn.setEnv(this.env());
- await cordova.raw.plugin('rm', plugin, this.defaultOptions);
+ this.runCommands(async () => {
+ await cordova.raw.plugin('rm', plugin, this.defaultOptions);
+ });
if (isFromTarballUrl) {
- Console.debug('Removing plugin from the tarball plugins lock', name);
+ Console.debug('Removing plugin from the tarball plugins lock', plugin);
// also remove from tarball-url-based plugins lock
- var lock = getTarballPluginsLock(this.projectRoot);
+ let lock = getTarballPluginsLock(this.projectRoot);
delete lock[name];
writeTarballPluginsLock(this.projectRoot, lock);
}
}
- async removePlugins(pluginsToRemove) {
- Console.debug('Removing plugins', pluginsToRemove);
-
- // Loop through all of the plugins to remove and remove them one by one until
- // we have deleted proper amount of plugins. It's necessary to loop because
- // we might have dependencies between plugins.
- while (pluginsToRemove.length > 0) {
- await Promise.all(_.map(pluginsToRemove, (version, name) => {
- removePlugin(name, utils.isUrlWithSha(version));
- }));
- let installedPlugins = await this.installedPlugins();
-
- uninstalledPlugins = _.difference(
- Object.keys(pluginsToRemove), Object.keys(installedPlugins)
- );
- plugins = _.omit(pluginsToRemove, uninstalledPlugins);
- };
+ removePlugins(pluginsToRemove) {
+ Console.debug('Removing Cordova plugins', pluginsToRemove);
+
+ if (_.isEmpty(pluginsToRemove)) return;
+
+ this.runCommands(async () => {
+ await cordova.raw.plugin('rm', Object.keys(pluginsToRemove), this.defaultOptions);
+ });
}
getTarballPluginsLock() {
@@ -205,7 +323,7 @@ export default class CordovaProject {
}
fetchCordovaPluginFromShaUrl(urlWithSha, pluginName) {
- Console.debug('Fetching a tarball from url:', urlWithSha);
+ Console.debug('Fetching a Cordova plugin tarball from url:', urlWithSha);
var pluginPath = files.pathJoin(this.localPluginsDir, pluginName);
var pluginTarball = buildmessage.enterJob("downloading Cordova plugin", () => {
@@ -246,34 +364,140 @@ export default class CordovaProject {
getCordovaLocalPluginPath(pluginPath) {
pluginPath = pluginPath.substr("file://".length);
if (utils.isPathRelative(pluginPath)) {
- return path.relative(this.projectRoot, path.resolve(projectDir, pluginPath));
+ return path.relative(
+ this.projectRoot,
+ path.resolve(this.projectContext.projectDir, pluginPath));
} else {
return pluginPath;
}
}
- // Build the project
- async build(options) {
- this.chdirToProjectRoot();
+ // Ensures that the Cordova plugins are synchronized with the app-level
+ // plugins.
+ ensurePluginsAreSynchronized(plugins, pluginsConfiguration = {}) {
+ buildmessage.assertInCapture();
+
+ Console.debug('Ensuring Cordova plugins are synchronized', plugins,
+ pluginsConfiguration);
+
+ var installedPlugins = this.installedPlugins;
+
+ // Due to the dependency structure of Cordova plugins, it is impossible to
+ // upgrade the version on an individual Cordova plugin. Instead, whenever a
+ // new Cordova plugin is added or removed, or its version is changed,
+ // we just reinstall all of the plugins.
+ var shouldReinstallPlugins = false;
+
+ // Iterate through all of the plugins and find if any of them have a new
+ // version. Additionally check if we have plugins installed from local path.
+ var pluginsFromLocalPath = {};
+ _.each(plugins, (version, name) => {
+ // Check if plugin is installed from local path
+ let pluginFromLocalPath = utils.isUrlWithFileScheme(version);
+ if (pluginFromLocalPath) {
+ pluginsFromLocalPath[name] = version;
+ }
+
+ // XXX there is a hack here that never updates a package if you are
+ // trying to install it from a URL, because we can't determine if
+ // it's the right version or not
+ if (!_.has(installedPlugins, name) ||
+ (installedPlugins[name] !== version && !pluginFromLocalPath)) {
+ // The version of the plugin has changed, or we do not contain a plugin.
+ shouldReinstallPlugins = true;
+ }
+ });
+
+ if (!_.isEmpty(pluginsFromLocalPath)) {
+ Console.debug('Reinstalling Cordova plugins added from the local path');
+ }
+
+ // Check to see if we have any installed plugins that are not in the current
+ // set of plugins.
+ _.each(installedPlugins, (version, name) => {
+ if (!_.has(plugins, name)) {
+ shouldReinstallPlugins = true;
+ }
+ });
+
+ if (shouldReinstallPlugins || !_.isEmpty(pluginsFromLocalPath)) {
+ buildmessage.enterJob({ title: "installing Cordova plugins"}, () => {
+ installedPlugins = this.installedPlugins;
+
+ if (shouldReinstallPlugins) {
+ this.removePlugins(installedPlugins);
+ } else {
+ this.removePlugins(pluginsFromLocalPath);
+ }
+
+ // Now install necessary plugins.
+ var pluginsInstalled, pluginsToInstall;
+
+ if (shouldReinstallPlugins) {
+ pluginsInstalled = 0;
+ pluginsToInstall = plugins;
+ } else {
+ pluginsInstalled = _.size(installedPlugins);
+ pluginsToInstall = pluginsFromLocalPath;
+ }
+
+ var pluginsCount = _.size(plugins);
+
+ buildmessage.reportProgress({ current: 0, end: pluginsCount });
+ _.each(pluginsToInstall, (version, name) => {
+ this.addPlugin(name, version, pluginsConfiguration[name]);
+
+ buildmessage.reportProgress({
+ current: ++pluginsInstalled,
+ end: pluginsCount
+ });
+ });
+ });
+ }
+ };
+
+ // Cordova commands support
+
+ get defaultOptions() {
+ return { silent: !Console.verbose, verbose: Console.verbose };
+ }
- superspawn.setEnv(this.env(...options.extraPaths));
- options = _.extend(this.defaultOptions, options);
+ defaultEnvWithPathsAdded(...extraPaths) {
+ let paths = (this.defaultPaths || []);
+ paths.unshift(...extraPaths);
+ const env = files.currentEnvWithPathsAdded(...paths);
+ return env;
+ }
- return await cordova.raw.build(options);
+ get defaultPaths() {
+ const nodeBinDir = files.getCurrentNodeBinDir();
+ return [nodeBinDir];
}
- // Run the project
- async run(platform, isDevice, options) {
- this.chdirToProjectRoot();
+ runCommands(asyncFunc, env = this.defaultEnvWithPathsAdded(),
+ cwd = this.projectRoot) {
+ const oldCwd = process.cwd();
+ if (cwd) {
+ process.chdir(files.convertToOSPath(cwd));
+ }
- superspawn.setEnv(this.env(...options.extraPaths));
- options = _.extend(this.defaultOptions, options,
- { platforms: [platform] });
+ superspawn.setEnv(env);
- if (isDevice) {
- return await cordova.raw.run(options);
- } else {
- return await cordova.raw.emulate(options);
+ try {
+ return Promise.await(asyncFunc());
+ } catch (error) {
+ if (error instanceof CordovaError) {
+ Console.error(`cordova: ${error.message}`);
+ Console.error(chalk.green("Try running again with the --verbose option \
+to help diagnose the issue."));
+ throw new main.ExitWithCode(1);
+ } else {
+ throw error;
+ }
+ } finally {
+ if (oldCwd) {
+ process.chdir(oldCwd);
+ }
}
}
}
diff --git a/tools/cordova/run-targets.js b/tools/cordova/run-targets.js
new file mode 100644
index 00000000000..20577f2cc66
--- /dev/null
+++ b/tools/cordova/run-targets.js
@@ -0,0 +1,92 @@
+import _ from 'underscore';
+import chalk from 'chalk';
+import child_process from 'child_process';
+
+import runLog from '../runners/run-log.js';
+import { Console } from '../console/console.js';
+import files from '../fs/files.js';
+
+export class CordovaRunTarget {
+ get title() {
+ return `app on ${this.displayName}`;
+ }
+}
+
+export class iOSRunTarget extends CordovaRunTarget {
+ constructor(isDevice) {
+ super();
+ this.platform = 'ios';
+ this.isDevice = isDevice;
+ }
+
+ get displayName() {
+ return this.isDevice ? "iOS Device" : "iOS Simulator";
+ }
+
+ async start(cordovaProject) {
+ // ios-deploy is super buggy, so we just open Xcode and let the user
+ // start the app themselves.
+ if (this.isDevice) {
+ openXcodeProject(files.pathJoin(cordovaProject.projectRoot,
+ 'platforms', 'ios', `${cordovaProject.appName}.xcodeproj`));
+ } else {
+ // Add the cordova package npm bin path so Cordova can find ios-sim
+ const cordovaBinPath = files.convertToOSPath(
+ files.pathJoin(files.getCurrentToolsDir(),
+ 'packages/cordova/.npm/package/node_modules/.bin'));
+
+ await cordovaProject.run(this.platform, this.isDevice, undefined,
+ [cordovaBinPath]);
+
+ // Bring iOS Simulator to front
+ child_process.spawn('osascript', ['-e',
+ 'tell application "System Events" \
+ to set frontmost of process "iOS Simulator" to true']);
+ }
+ }
+}
+
+function openXcodeProject(projectPath) {
+ child_process.execFile('open', [projectPath], undefined,
+ (error, stdout, stderr) => {
+ if (error) {
+ Console.error();
+ Console.error(chalk.green(`Failed to open your project in Xcode:
+${error.message}`));
+ Console.error(
+ chalk.green("Instructions for running your app on an iOS device: ") +
+ Console.url("https://github.com/meteor/meteor/wiki/" +
+ "How-to-run-your-app-on-an-iOS-device")
+ );
+ Console.error();
+ } else {
+ Console.info();
+ Console.info(
+ chalk.green(
+ "Your project has been opened in Xcode so that you can run your " +
+ "app on an iOS device. For further instructions, visit this " +
+ "wiki page: ") +
+ Console.url(
+ "https://github.com/meteor/meteor/wiki/" +
+ "How-to-run-your-app-on-an-iOS-device"
+ ));
+ Console.info();
+ }
+ });
+}
+
+export class AndroidRunTarget extends CordovaRunTarget {
+ constructor(isDevice) {
+ super();
+ this.platform = 'android';
+ this.isDevice = isDevice;
+ }
+
+ get displayName() {
+ return this.isDevice ? "Android Device" : "Android Emulator";
+ }
+
+ async start(cordovaProject) {
+ await cordovaProject.run(this.platform, this.isDevice);
+ }
+}
diff --git a/tools/cordova/run.js b/tools/cordova/run.js
deleted file mode 100644
index 81a5d2ff01f..00000000000
--- a/tools/cordova/run.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import _ from 'underscore';
-import { Console } from '../console.js';
-import files from '../fs/files.js';
-import isopackets from '../tool-env/isopackets.js'
-
-import iOSRunner from './ios-runner.js';
-import AndroidRunner from './android-runner.js';
-
-export function buildCordovaRunners(projectContext, cordovaProject, targets, options) {
- return _.map(targets, (target) => {
- let targetParts = target.split('-');
- const platform = targetParts[0];
- const isDevice = targetParts[1] === 'device';
-
- if (platform == 'ios') {
- return new iOSRunner(projectContext, cordovaProject, isDevice, options);
- } else if (platform == 'android') {
- return new AndroidRunner(projectContext, cordovaProject, isDevice, options);
- } else {
- throw new Error(`Unknown platform: ${platform}`);
- }
- });
-};
diff --git a/tools/cordova/runner.js b/tools/cordova/runner.js
new file mode 100644
index 00000000000..92a0a8b920b
--- /dev/null
+++ b/tools/cordova/runner.js
@@ -0,0 +1,112 @@
+import _ from 'underscore';
+import buildmessage from '../utils/buildmessage.js';
+import runLog from '../runners/run-log.js';
+import { Console } from '../console/console.js';
+import main from '../cli/main.js';
+
+import { displayNameForPlatform, prepareProjectForBuild } from './index.js';
+
+export class CordovaRunner {
+ constructor(cordovaProject, runTargets) {
+ this.cordovaProject = cordovaProject;
+ this.runTargets = runTargets;
+ }
+
+ get projectContext() {
+ return this.cordovaProject.projectContext;
+ }
+
+ get platformsForRunTargets() {
+ return _.uniq(this.runTargets.map((runTarget) => runTarget.platform));
+ }
+
+ checkPlatformsForRunTargets() {
+ this.cordovaProject.ensurePlatformsAreSynchronized();
+
+ let satisfied = true;
+ const messages = buildmessage.capture(
+ { title: `checking platform requirements` }, () => {
+ for (platform of this.platformsForRunTargets) {
+ satisfied =
+ this.cordovaProject.checkPlatformRequirements(platform) &&
+ satisfied;
+ }
+ });
+
+ if (messages.hasMessages()) {
+ Console.printMessages(messages);
+ throw new main.ExitWithCode(1);
+ } else if (!satisfied) {
+ throw new main.ExitWithCode(1);
+ };
+ }
+
+ printWarningsIfNeeded() {
+ // OAuth2 packages don't work so well with any mobile platform except the iOS
+ // simulator. Print a warning and direct users to the wiki page for help.
+ if (this.projectContext.packageMap.getInfo('oauth2')) {
+ Console.warn();
+ Console.labelWarn(
+ "It looks like you are using OAuth2 login in your app. " +
+ "Meteor's OAuth2 implementation does not currently work with " +
+ "mobile apps in local development mode, except in the iOS " +
+ "simulator. You can run the iOS simulator with 'meteor run ios'. " +
+ "For additional workarounds, see " +
+ Console.url(
+ "https://github.com/meteor/meteor/wiki/" +
+ "OAuth-for-mobile-Meteor-clients."));
+ }
+
+ // If we are targeting the remote devices, warn about ports and same network
+ if (_.findWhere(this.runTargets, { isDevice: true })) {
+ Console.warn();
+ Console.labelWarn(
+ "You are testing your app on a remote device. " +
+ "For the mobile app to be able to connect to the local server, make " +
+ "sure your device is on the same network, and that the network " +
+ "configuration allows clients to talk to each other " +
+ "(no client isolation).");
+ }
+ }
+
+ prepareProject(bundlePath, plugins, options) {
+ this.cordovaProject.prepare(bundlePath, plugins, options);
+ }
+
+ build() {
+ buildmessage.assertInCapture();
+
+ buildmessage.enterJob(
+ { title: `building Cordova project for platforms: \
+${this.platformsForRunTargets}` },
+ () => {
+ this.cordovaProject.build(this.platformsForRunTargets, this.options);
+ });
+ }
+
+ startRunTargets() {
+ buildmessage.assertInCapture();
+
+ for (runTarget of this.runTargets) {
+ buildmessage.enterJob(
+ { title: `starting ${runTarget.title}` },
+ () => {
+ // Do not await the returned promise
+ runTarget.start(this.cordovaProject);
+
+ if (!buildmessage.jobHasMessages()) {
+ runLog.log(`Started ${runTarget.title}.`, { arrow: true });
+ }
+ }
+ );
+ }
+ }
+
+ havePlatformsChanged() {
+ return false;
+ }
+
+ havePluginsChanged() {
+ return false;
+ }
+}
diff --git a/tools/cordova/utils.js b/tools/cordova/utils.js
deleted file mode 100644
index b133dba1e65..00000000000
--- a/tools/cordova/utils.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import _ from 'underscore';
-import files from '../fs/files.js';
-import { Console } from '../console.js';
-import { execFileAsync, execFileSync } from '../utils/utils.js';
-
-export function execFileAsyncOrThrow(file, args, opts, cb) {
- Console.debug('Running asynchronously: ', file, args);
-
- if (_.isFunction(opts)) {
- cb = opts;
- opts = undefined;
- }
-
- var p = execFileAsync(file, args, opts);
- p.on('close', function (code) {
- var err = null;
- if (code)
- err = new Error(file + ' ' + args.join(' ') +
- ' exited with non-zero code: ' + code + '. Use -v for' +
- ' more logs.');
-
- if (cb) cb(err, code);
- else if (err) throw err;
- });
-};
-
-export function execFileSyncOrThrow(file, args, opts) {
- Console.debug('Running synchronously: ', file, args);
-
- var childProcess = execFileSync(file, args, opts);
- if (!childProcess.success) {
- // XXX Include args
- var message = 'Error running ' + file;
- if (childProcess.stderr) {
- message = message + "\n" + childProcess.stderr + "\n";
- }
- if (childProcess.stdout) {
- message = message + "\n" + childProcess.stdout + "\n";
- }
-
- throw new Error(message);
- }
-
- return childProcess;
-};
diff --git a/tools/runners/run-all.js b/tools/runners/run-all.js
index 3bca3591022..495bf77298e 100644
--- a/tools/runners/run-all.js
+++ b/tools/runners/run-all.js
@@ -21,7 +21,7 @@ class Runner {
appPort,
banner,
disableOplog,
- extraRunners,
+ cordovaRunner,
mongoUrl,
onFailure,
oplogUrl,
@@ -60,8 +60,6 @@ class Runner {
self.rootUrl = 'http://localhost:' + listenPort + '/';
}
- self.extraRunners = extraRunners ? extraRunners.slice(0) : [];
-
self.proxy = new Proxy({
listenPort,
listenHost: proxyHost,
@@ -99,6 +97,7 @@ class Runner {
rootUrl: self.rootUrl,
proxy: self.proxy,
noRestartBanner: self.quiet,
+ cordovaRunner: cordovaRunner
});
self.selenium = null;
@@ -114,11 +113,6 @@ class Runner {
start() {
const self = this;
- // XXX: Include all runners, and merge parallel-launch patch
- _.each(self.extraRunners, function (runner) {
- runner && runner.prestart && runner.prestart();
- });
-
self.proxy.start();
// print the banner only once we've successfully bound the port
@@ -133,17 +127,6 @@ class Runner {
self.updater.start();
}
- _.forEach(self.extraRunners, function (extraRunner) {
- if (! self.stopped) {
- const title = extraRunner.title;
- buildmessage.enterJob({ title: "starting " + title }, function () {
- extraRunner.start();
- });
- if (! self.quiet && ! self.stopped)
- runLog.log("Started " + title + ".", { arrow: true });
- }
- });
-
if (! self.stopped) {
buildmessage.enterJob({ title: "starting your app" }, function () {
self.appRunner.start();
@@ -204,9 +187,6 @@ class Runner {
self.proxy.stop();
self.updater.stop();
self.mongoRunner && self.mongoRunner.stop();
- _.forEach(self.extraRunners, function (extraRunner) {
- extraRunner.stop();
- });
self.appRunner.stop();
self.selenium && self.selenium.stop();
// XXX does calling this 'finish' still make sense now that runLog is a
diff --git a/tools/runners/run-app.js b/tools/runners/run-app.js
index cbb45c2a81a..3ca61b9535b 100644
--- a/tools/runners/run-app.js
+++ b/tools/runners/run-app.js
@@ -8,11 +8,12 @@ var bundler = require('../isobuild/bundler.js');
var buildmessage = require('../utils/buildmessage.js');
var runLog = require('./run-log.js');
var stats = require('../meteor-services/stats.js');
-import { getCordovaDependenciesFromStar } from '../cordova/build.js';
var Console = require('../console/console.js').Console;
var catalog = require('../packaging/catalog/catalog.js');
var Profile = require('../tool-env/profile.js').Profile;
var release = require('../packaging/release.js');
+import * as cordova from '../cordova';
+import { CordovaBuilder } from '../cordova/builder.js';
// Parse out s as if it were a bash command line.
var bashParse = function (s) {
@@ -351,6 +352,7 @@ var AppRunner = function (options) {
self.buildOptions = options.buildOptions;
self.rootUrl = options.rootUrl;
self.mobileServerUrl = options.mobileServerUrl;
+ self.cordovaRunner = options.cordovaRunner;
self.settingsFile = options.settingsFile;
self.debugPort = options.debugPort;
self.proxy = options.proxy;
@@ -363,14 +365,6 @@ var AppRunner = function (options) {
self.omitPackageMapDeltaDisplayOnFirstRun =
options.omitPackageMapDeltaDisplayOnFirstRun;
- // Keep track of the app's Cordova plugins and platforms. If the set
- // of plugins or platforms changes from one run to the next, we just
- // exit, because we don't yet have a way to, for example, get the new
- // plugins to the mobile clients or stop a running client on a
- // platform that has been removed.
- self.cordovaPlugins = null;
- self.cordovaPlatforms = null;
-
self.fiber = null;
self.startFuture = null;
self.runFuture = null;
@@ -449,7 +443,7 @@ _.extend(AppRunner.prototype, {
_runOnce: function (options) {
var self = this;
options = options || {};
- var firstRun = options.firstRun;
+ const firstRun = options.firstRun;
Console.enableProgressDisplay(true);
@@ -620,32 +614,6 @@ _.extend(AppRunner.prototype, {
};
}
- firstRun = false;
-
- var platforms = self.projectContext.platformList.getCordovaPlatforms();
- platforms.sort();
- if (self.cordovaPlatforms &&
- ! _.isEqual(self.cordovaPlatforms, platforms)) {
- return {
- outcome: 'outdated-cordova-platforms'
- };
- }
- // XXX This is racy --- we should get this from the pre-runner build, not
- // from the first runner build.
- self.cordovaPlatforms = platforms;
-
- var plugins = getCordovaDependenciesFromStar(
- bundleResult.starManifest);
-
- if (self.cordovaPlugins && ! _.isEqual(self.cordovaPlugins, plugins)) {
- return {
- outcome: 'outdated-cordova-plugins'
- };
- }
- // XXX This is racy --- we should get this from the pre-runner build, not
- // from the first runner build.
- self.cordovaPlugins = plugins;
-
var serverWatchSet = bundleResult.serverWatchSet;
serverWatchSet.merge(settingsWatchSet);
@@ -659,6 +627,41 @@ _.extend(AppRunner.prototype, {
serverWatchSet = combinedWatchSetForBundleResult(bundleResult);
}
+ const cordovaRunner = self.cordovaRunner;
+ if (cordovaRunner) {
+ if (firstRun) {
+ const plugins = cordova.pluginsFromStarManifest(bundleResult.starManifest);
+ const { settingsFile, mobileServerUrl } = self;
+ const messages = buildmessage.capture(() => {
+ cordovaRunner.prepareProject(bundlePath, plugins,
+ { settingsFile, mobileServerUrl });
+ cordovaRunner.printWarningsIfNeeded();
+ cordovaRunner.startRunTargets();
+ });
+
+ if (messages.hasMessages()) {
+ return {
+ outcome: 'bundle-fail',
+ errors: messages,
+ watchSet: combinedWatchSetForBundleResult(bundleResult)
+ };
+ }
+ } else {
+ // If the set of Cordova of platforms or plugins changes from one run
+ // to the next, we just exit, because we don't yet have a way to,
+ // for example, get the new plugins to the mobile clients or stop a
+ // running client on a platform that has been removed.
+
+ if (cordovaRunner.havePlatformsChanged()) {
+ return { outcome: 'outdated-cordova-platforms' };
+ }
+
+ if (cordovaRunner.havePluginsChanged()) {
+ return { outcome: 'outdated-cordova-plugins' };
+ }
+ }
+ }
+
// Atomically (1) see if we've been stop()'d, (2) if not, create a
// future that can be used to stop() us once we start running.
if (self.exitFuture)
@@ -700,7 +703,7 @@ _.extend(AppRunner.prototype, {
});
// Empty self._beforeStartFutures and await its elements.
- if (options.firstRun && self._beforeStartFuture) {
+ if (firstRun && self._beforeStartFuture) {
var stopped = self._beforeStartFuture.wait();
if (stopped) {
return true;
diff --git a/tools/tests/cordova-platforms.js b/tools/tests/cordova-platforms.js
index 4f570e383dc..e39a334f3d8 100644
--- a/tools/tests/cordova-platforms.js
+++ b/tools/tests/cordova-platforms.js
@@ -13,7 +13,7 @@ selftest.define("add cordova platforms", ["cordova"], function () {
run = s.run("run", "android");
run.matchErr("Please add the Android platform to your project first");
run.match("meteor add-platform android");
- run.expectExit(2);
+ run.expectExit(1);
run = s.run("install-sdk", "android");
run.waitSecs(90); // Big downloads
@@ -26,12 +26,12 @@ selftest.define("add cordova platforms", ["cordova"], function () {
run = s.run("remove-platform", "foo");
run.matchErr("foo: platform is not");
- run.expectExit(0);
+ run.expectExit(1);
run = s.run("remove-platform", "android");
run.match("removed");
run = s.run("run", "android");
run.matchErr("Please add the Android platform to your project first");
run.match("meteor add-platform android");
- run.expectExit(2);
+ run.expectExit(1);
});
diff --git a/tools/tests/cordova-run.js b/tools/tests/cordova-run.js
index 3fac6c400a2..42690b924af 100644
--- a/tools/tests/cordova-run.js
+++ b/tools/tests/cordova-run.js
@@ -1,4 +1,4 @@
-import selftest from '../selftest.js';
+import selftest from '../tool-testing/selftest.js';
import utils from '../utils/utils.js';
import { parseServerOptionsForRunCommand } from '../cli/commands-cordova.js';
@@ -34,7 +34,7 @@ selftest.define('get mobile server argument for meteor run', ['cordova'], functi
}).mobileServerUrl, { host: utils.ipAddress(), port: "3000", protocol: "http://" });
// meteor run -p example.com:3000 --mobile-server 4000 => error, mobile
- // server must specify a hostname
+ // server must include a hostname
selftest.expectThrows(() => {
parseServerOptionsForRunCommand({
port: "example.com:3000",
diff --git a/tools/utils/utils.js b/tools/utils/utils.js
index e8caa865a9e..55a2ef59135 100644
--- a/tools/utils/utils.js
+++ b/tools/utils/utils.js
@@ -20,7 +20,7 @@ var utils = exports;
// undefined} or something like that.
//
// 'defaults' is an optional object with 'host', 'port', and 'protocol' keys.
-var parseUrl = function (str, defaults) {
+exports.parseUrl = function (str, defaults) {
// XXX factor this out into a {type: host/port}?
defaults = defaults || {};
@@ -52,7 +52,16 @@ var parseUrl = function (str, defaults) {
};
};
-var ipAddress = function () {
+// 'defaults' is an optional object with 'host', 'port', and 'protocol' keys.
+exports.formatUrl = function (url, defaults) {
+ let string = url.protocol + url.host;
+ if (url.port) {
+ string += `:${url.port}`;
+ }
+ return string;
+}
+
+exports.ipAddress = function () {
let defaultRoute;
// netroute is not available on Windows
if (false) {
@@ -100,9 +109,6 @@ exports.hasScheme = function (str) {
return !! str.match(/^[A-Za-z][A-Za-z0-9+-\.]*\:\/\//);
};
-exports.parseUrl = parseUrl;
-
-exports.ipAddress = ipAddress;
exports.hasScheme = function (str) {
return !! str.match(/^[A-Za-z][A-Za-z0-9+-\.]*\:\/\//);