Cordova: support for local plugins

This commit is contained in:
Bartosz Wojtkowiak
2015-04-17 07:41:03 -07:00
committed by Slava Kim
parent 3521fabb60
commit cd28950846
8 changed files with 338 additions and 37 deletions

View File

@@ -396,12 +396,10 @@ var ensureCordovaProject = function (projectContext, appName) {
// --- Cordova platforms ---
// Ensures that the Cordova platforms are synchronized with the app-level
// platforms.
var ensureCordovaPlatforms = function (projectContext) {
verboseLog('Ensuring that platforms in cordova build project are in sync');
// Get the currently installed platforms from cordova build
var getCordovaInstalledPlatforms = function(projectContext) {
var cordovaPath = projectContext.getProjectLocalDirectory('cordova-build');
var platforms = projectContext.platformList.getCordovaPlatforms();
var platformsList = execFileSyncOrThrow(
localCordova, ['platform', 'list'], { cwd: cordovaPath, env: buildCordovaEnv() });
@@ -415,9 +413,18 @@ var ensureCordovaPlatforms = function (projectContext) {
throw new Error('Failed to parse the output of `cordova platform list`: ' +
platformsList.stdout);
var installedPlatforms = _.map(platformsStrings.split(', '), function (s) {
return installedPlatforms = _.map(platformsStrings.split(', '), function (s) {
return s.split(' ')[0];
});
}
// Ensures that the Cordova platforms are synchronized with the app-level
// platforms.
var ensureCordovaPlatforms = function (projectContext) {
verboseLog('Ensuring that platforms in cordova build project are in sync');
var cordovaPath = projectContext.getProjectLocalDirectory('cordova-build');
var platforms = projectContext.platformList.getCordovaPlatforms();
var installedPlatforms = getCordovaInstalledPlatforms(projectContext);
_.each(platforms, function (platform) {
if (_.contains(installedPlatforms, platform))
@@ -464,8 +471,16 @@ var targetsToPlatforms = cordova.targetsToPlatforms = function (targets) {
var installPlugin = function (cordovaPath, name, version, conf) {
verboseLog('Installing a plugin', name, version);
var pluginInstallCommand;
if (utils.isUrlWithFileUri(version)) {
// Strip file://
pluginInstallCommand = version.substr(7);
} else {
pluginInstallCommand = version ? name + '@' + version : name;
}
// XXX do something different for plugins fetched from a url.
var pluginInstallCommand = version ? name + '@' + version : name;
var localPluginsPath = localPluginsPathFromCordovaPath(cordovaPath);
if (version && utils.isUrlWithSha(version)) {
@@ -559,7 +574,7 @@ var writeTarballPluginsLock = function (cordovaPath, tarballPluginsLock) {
// Returns the list of installed plugins as a hash from plugin name to version.
var getInstalledPlugins = function (cordovaPath) {
verboseLog('Getting installed plugins for project');
verboseLog('Getting installed plugins for project in ' + cordovaPath);
var installedPlugins = {};
var pluginsOutput = execFileSyncOrThrow(localCordova, ['plugin', 'list'],
@@ -614,17 +629,30 @@ var ensureCordovaPlugins = function (projectContext, options) {
// 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.
// we just reinstall all of the plugins. Additionally if there are any plugins
// added from the local path, we will reinstall them just to be sure they
// are up to date since we do not track changes in their sources.
var shouldReinstallPlugins = false;
var shouldReinstallPlugins = false,
shouldReinstallPluginsFromLocalPaths = false,
pluginsFromLocalPath = {};
// Iterate through all of the plugin and find if any of them have a new
// version.
// 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 pluginFromLocalPath;
_.each(plugins, function (version, name) {
// Check if plugin is installed from local path
pluginFromLocalPath = utils.isUrlWithFileUri(version);
if (pluginFromLocalPath) {
shouldReinstallPluginsFromLocalPaths = true;
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) {
if (! _.has(installedPlugins, name) ||
(installedPlugins[name] !== version && !pluginFromLocalPath)) {
// The version of the plugin has changed, or we do not contain a plugin.
shouldReinstallPlugins = true;
}
@@ -638,37 +666,63 @@ var ensureCordovaPlugins = function (projectContext, options) {
}
});
if (shouldReinstallPlugins) {
if (shouldReinstallPluginsFromLocalPaths)
verboseLog('Reinstalling cordova plugins added from the local path');
if (shouldReinstallPlugins || shouldReinstallPluginsFromLocalPaths) {
// Loop through all of the current plugins and remove them one by one until
// we have no plugins. It's necessary to loop because we might have
// dependencies between plugins.
var uninstallAllPlugins = function () {
// we have deleted proper amount of plugins. It's necessary to loop because
// we might have dependencies between plugins.
var uninstallPlugins = function (pluginsToUninstall, clearPluginsDirectory) {
installedPlugins = getInstalledPlugins(cordovaPath);
while (_.size(installedPlugins)) {
_.each(installedPlugins, function (version, name) {
uninstallPlugin(cordovaPath, name, utils.isUrlWithSha(version));
var uninstalled = 1;
// Detect if we couldn't delete any more plugins just to avoid
// hanging forever
while (uninstalled.length !== 0 && _.size(pluginsToUninstall)) {
_.each(pluginsToUninstall, function (version, name) {
uninstallPlugin(cordovaPath, name, utils.isUrlWithSha(version));
});
installedPlugins = getInstalledPlugins(cordovaPath);
uninstalled = _.difference(
Object.keys(pluginsToUninstall), Object.keys(installedPlugins)
);
pluginsToUninstall = _.omit(pluginsToUninstall, uninstalled);
}
// XXX HACK, because Cordova doesn't properly clear its plugins on `rm`.
// This will completely destroy the project state. We should work with
// Cordova to fix the bug in their system, because it doesn't seem
// like there's a way around this.
files.rm_recursive(files.pathJoin(cordovaPath, 'platforms'));
if (clearPluginsDirectory)
files.rm_recursive(files.pathJoin(cordovaPath, 'platforms'));
ensureCordovaPlatforms(projectContext);
};
buildmessage.enterJob({ title: "installing Cordova plugins"}, function () {
uninstallAllPlugins();
installedPlugins = getInstalledPlugins(cordovaPath);
if (shouldReinstallPlugins)
uninstallPlugins(installedPlugins, true);
else
uninstallPlugins(pluginsFromLocalPath, false);
// Now install all of the plugins.
// Now install necessary plugins.
try {
// XXX: forkJoin with parallel false?
var pluginsInstalled = 0;
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(plugins, function (version, name) {
_.each(pluginsToInstall, function (version, name) {
installPlugin(cordovaPath, name, version, pluginsConfiguration[name]);
buildmessage.reportProgress({
@@ -680,7 +734,7 @@ var ensureCordovaPlugins = function (projectContext, options) {
// If a plugin fails to install, then remove all plugins and throw the
// error. Cordova doesn't remove the plugin by default for some reason.
// XXX don't throw and improve this error message.
uninstallAllPlugins();
uninstallPlugins(getInstalledPlugins(cordovaPath), true);
throw err;
}
});

View File

@@ -0,0 +1,10 @@
Package.describe({
version: "1.0.0",
summary: "contains a empty cordova plugin"
});
Package.onUse(function(api) {
Cordova.depends({
'com.cordova.empty': 'file://../../../../cordova-local-plugin'
});
});

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
id="com.cordova.empty" version="0.2.3">
<name>Empty</name>
<description>Cordova Empty Plugin</description>
<keywords>cordova,empty</keywords>
<repo>https://github.com</repo>
<js-module src="www/Empty.js" name="Empty">
<clobbers target="cordova.plugins.Empty" />
</js-module>
<platform name="android">
<config-file target="res/xml/config.xml" parent="/*">
<feature name="Empty" >
<param name="android-package" value="com.cordova.empty.Empty"/>
</feature>
</config-file>
<source-file src="src/android/Empty.java" target-dir="src/com/cordova/empty" />
</platform>
</plugin>

View File

@@ -0,0 +1,17 @@
package com.cordova.empty;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.json.JSONArray;
import org.json.JSONException;
public class Empty extends CordovaPlugin {
public Object onMessage(String id, Object data) {
return null;
}
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
return false;
}
}

View File

@@ -0,0 +1,18 @@
package com.cordova.empty;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.json.JSONArray;
import org.json.JSONException;
public class Empty extends CordovaPlugin {
public Object onMessage(String id, Object data) {
System.out.println("change");
return null;
}
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
return false;
}
}

View File

@@ -0,0 +1,3 @@
var Empty = {
};
module.exports = Empty;

View File

@@ -17,17 +17,15 @@ var copyFile = function(from, to, sand) {
sand.write(to, contents);
};
var localCordova = files.pathJoin(files.getCurrentToolsDir(), "tools",
"cordova-scripts", "cordova.sh");
// Given a sandbox, that has the app as its currend cwd, read the versions file
// and check that it contains the plugins that we are looking for. We don't
// check the order, we just want to make sure that the right dependencies are
// in.
// and read the plugins list.
//
// sand: a sandbox, that has the main app directory as its cwd.
// plugins: an array of plugins in order.
var checkCordovaPlugins = selftest.markStack(function(sand, plugins) {
var getCordovaPluginsList = function(sand) {
var lines = selftest.execFileSync(localCordova, ['plugins'],
{
cwd: files.pathJoin(sand.cwd, '.meteor', 'local', 'cordova-build'),
@@ -38,12 +36,24 @@ var checkCordovaPlugins = selftest.markStack(function(sand, plugins) {
if (lines[0].match(/No plugins/)) {
lines = [];
}
lines.sort();
return lines;
}
// Given a sandbox, that has the app as its currend cwd, read the versions file
// and check that it contains the plugins that we are looking for. We don't
// check the order, we just want to make sure that the right dependencies are
// in.
//
// sand: a sandbox, that has the main app directory as its cwd.
// plugins: an array of plugins in order.
var checkCordovaPlugins = selftest.markStack(function(sand, plugins) {
var cordovaPlugins = getCordovaPluginsList(sand);
plugins = _.clone(plugins).sort();
var i = 0;
_.each(lines, function(line) {
_.each(cordovaPlugins, function(line) {
if (!line || line === '') return;
// XXX should check for the version as well?
selftest.expectEqual(line.split(' ')[0], plugins[i]);
@@ -52,6 +62,16 @@ var checkCordovaPlugins = selftest.markStack(function(sand, plugins) {
selftest.expectEqual(plugins.length, i);
});
// Like the function above but only looks if a certain plugin is on the list
var checkCordovaPluginExists = selftest.markStack(function(sand, plugin) {
var cordovaPlugins = getCordovaPluginsList(sand);
var found = false;
cordovaPlugins = cordovaPlugins.map(function (line) {
if (line && line !== '') return line.split(' ')[0];
});
selftest.expectTrue(_.contains(cordovaPlugins, plugin));
});
// Given a sandbox, that has the app as its cwd, read the cordova plugins
// file and check that it contains exactly the plugins specified, in order.
//
@@ -131,7 +151,6 @@ selftest.define("change cordova plugins", ["cordova"], function () {
run.match("restarted");
});
// Add plugins through the command line, and make sure that the correct set of
// changes is reflected in .meteor/packages, .meteor/versions and list
selftest.define("add cordova plugins", ["slow", "cordova"], function () {
@@ -194,7 +213,7 @@ selftest.define("add cordova plugins", ["slow", "cordova"], function () {
run.match("android");
run = s.run("build", "../a", "--server", "localhost:3000");
run.waitSecs(30);
run.waitSecs(60);
// This fails because the FB plugin does not compile without additional
// configuration for android.
run.expectExit(1);
@@ -232,6 +251,35 @@ selftest.define("add cordova plugins", ["slow", "cordova"], function () {
run.waitSecs(60);
run.expectExit(0);
checkCordovaPlugins(s, ["org.apache.cordova.device"]);
run = s.run("remove", "cordova:org.apache.cordova.device");
run.match("removed");
run.expectExit(0);
run = s.run("add", "cordova:com.example.plugin@file://");
run.matchErr("exact version of dependency");
run.expectExit(1);
run = s.run("add", "cordova:com.example.plugin@file://../../plugin_directory");
run.waitSecs(5);
run.match("added cordova plugin com.example.plugin");
run.expectExit(0);
checkUserPlugins(s, ["com.example.plugin"]);
// This should fail beacuse the plugin does not exists at the specified path
run = s.run("build", "../a", "--server", "localhost:3000");
run.waitSecs(30);
run.expectExit(1);
checkCordovaPlugins(s, []);
// Add a package with Cordova.depends with local plugin (added from path)
run = s.run("add", "empty-cordova-plugin");
run.match("added,");
run.match("contains a empty cordova plugin");
run.expectExit(0);
});
selftest.define("remove cordova plugins", function () {
@@ -260,6 +308,19 @@ selftest.define("remove cordova plugins", function () {
run.match("removed");
run.expectExit(1);
checkUserPlugins(s, []);
run = s.run("add", "cordova:com.example.plugin@file://../../plugin_directory");
run.waitSecs(5);
run.match("added cordova plugin com.example.plugin");
run.expectExit(0);
checkUserPlugins(s, ["com.example.plugin"]);
run = s.run("remove", "cordova:com.example.plugin");
run.waitSecs(5);
run.match("removed");
run.expectExit(0);
checkUserPlugins(s, []);
});
selftest.define("meteor exits when cordova platforms change", ["slow", "cordova"], function () {
@@ -328,6 +389,115 @@ selftest.define("meteor exits when cordova platforms change", ["slow", "cordova"
run.expectExit(254);
});
selftest.define("meteor reinstalls only local cordova plugins on consecutive builds/runs", ["slow", "cordova"], function () {
var s = new Sandbox();
var run;
s.createApp("myapp", "package-tests");
s.cd("myapp");
run = s.run("add-platform", "android");
run.match("Do you agree");
run.write("Y\n");
run.waitSecs(90);
run.match("added platform");
var
pluginPath = "../cordova-local-plugin",
pluginSource = "packages/empty-cordova-plugin/plugin",
androidPluginSource = ".meteor/local/cordova-build/platforms/android/src";
// Copy fake cordova plugin to ../cordova-local-plugin
s.mkdir(pluginPath);
s.cp(pluginSource + '/plugin.xml', pluginPath + '/plugin.xml');
s.mkdir(pluginPath + '/www');
s.mkdir(pluginPath + '/src');
s.mkdir(pluginPath + '/src/android');
s.cp(pluginSource + '/www/Empty.js', pluginPath +'/www/Empty.js');
s.cp(
pluginSource + '/src/android/Empty.java',
pluginPath + '/src/android/Empty.java'
);
// Add the local cordova plugin
run = s.run("add", "cordova:com.cordova.empty@file://../../../../cordova-local-plugin");
run.waitSecs(60);
run.match("added cordova plugin com.cordova.empty");
run.expectExit(0);
checkUserPlugins(s, ["com.cordova.empty"]);
// Run meteor and check if the cordova android build have the plugin file.
// Using "android-device" because "android" would start the simulator.
run = s.run("run", "android-device");
run.waitSecs(60);
run.match("Started your app");
run.stop();
selftest.expectTrue(
s.read(
androidPluginSource + "/com/cordova/empty/Empty.java"
).indexOf('change') === -1
);
selftest.expectTrue(
s.read(
androidPluginSource + "/com/cordova/empty/Empty.java"
).indexOf('CordovaPlugin') > -1
);
// Copy changed file to the plugin
s.cp(
pluginSource + '/src/android/Empty_changed.java',
pluginPath + '/src/android/Empty.java'
);
// Check if the local plugin will be refreshed
run = s.run("run", "android-device");
run.waitSecs(60);
run.match("Started your app");
run.stop();
selftest.expectTrue(
s.read(
androidPluginSource + "/com/cordova/empty/Empty.java"
).indexOf('change') > -1
);
// Now test the same scenario but with builds
s.cp(
pluginSource + '/src/android/Empty.java',
pluginPath + '/src/android/Empty.java'
);
run = s.run("build", "../a", "--server", "localhost:3000");
run.waitSecs(60);
run.expectExit(0);
selftest.expectTrue(
s.read(
"../a/android/project/src/com/cordova/empty/Empty.java"
).indexOf('change') === -1
);
checkCordovaPluginExists(s, "com.cordova.empty");
s.cp(
pluginSource + '/src/android/Empty_changed.java',
pluginPath + '/src/android/Empty.java'
);
run = s.run("build", "../a", "--server", "localhost:3000");
run.waitSecs(60);
run.expectExit(0);
selftest.expectTrue(
s.read(
"../a/android/project/src/com/cordova/empty/Empty.java"
).indexOf('change') > -1
);
});
selftest.define("meteor exits when cordova plugins change", ["slow", "cordova"], function () {
var s = new Sandbox();
var run;

View File

@@ -466,6 +466,10 @@ exports.generateSubsetsOfIncreasingSize = function (total, cb) {
}
};
exports.isUrlWithFileUri = function (x) {
return /^file:\/\/.+/.test(x);
};
exports.isUrlWithSha = function (x) {
// For now, just support http/https, which is at least less restrictive than
// the old "github only" rule.
@@ -491,7 +495,8 @@ exports.ensureOnlyExactVersions = function (dependencies) {
});
};
exports.isExactVersion = function (version) {
return semver.valid(version) || exports.isUrlWithSha(version);
return semver.valid(version) || exports.isUrlWithSha(version)
|| exports.isUrlWithFileUri(version);
};