blob: 9858366d28f0d35bbb19268f2c0afeee5aa9e78e [file] [log] [blame]
/*
this file is found by cordova-lib when you attempt to
'cordova platform add PATH' where path is this repo.
*/
/*jslint node: true */
var shell = require('shelljs');
var path = require('path');
var fs = require('fs');
var cdvcmn = require('cordova-common');
var CordovaLogger = cdvcmn.CordovaLogger;
var ConfigParser = cdvcmn.ConfigParser;
var ActionStack = cdvcmn.ActionStack;
var PluginInfo = cdvcmn.PluginInfo;
var selfEvents = cdvcmn.events;
var xmlHelpers = cdvcmn.xmlHelpers;
var PlatformJson = cdvcmn.PlatformJson;
var PlatformMunger = cdvcmn.ConfigChanges.PlatformMunger;
var PluginInfoProvider = cdvcmn.PluginInfoProvider;
var BrowserParser = require('./browser_parser');
var PLATFORM_NAME = 'browser';
function setupEvents(externalEventEmitter) {
if (externalEventEmitter) {
// This will make the platform internal events visible outside
selfEvents.forwardEventsTo(externalEventEmitter);
return externalEventEmitter;
}
// There is no logger if external emitter is not present,
// so attach a console logger
CordovaLogger.get().subscribe(selfEvents);
return selfEvents;
}
function Api(platform, platformRootDir, events) {
this.platform = platform || PLATFORM_NAME;
// MyApp/platforms/browser
this.root = path.resolve(__dirname, '..');
this.events = setupEvents(events);
this.parser = new BrowserParser(this.root);
this._handler = require('./browser_handler');
this.locations = {
platformRootDir: platformRootDir,
root: this.root,
www: path.join(this.root, 'www'),
res: path.join(this.root, 'res'),
platformWww: path.join(this.root, 'platform_www'),
configXml: path.join(this.root, 'config.xml'),
defaultConfigXml: path.join(this.root, 'cordova/defaults.xml'),
build: path.join(this.root, 'build'),
// NOTE: Due to platformApi spec we need to return relative paths here
cordovaJs: 'bin/templates/project/assets/www/cordova.js',
cordovaJsSrc: 'cordova-js-src'
};
this._platformJson = PlatformJson.load(this.root, platform);
this._pluginInfoProvider = new PluginInfoProvider();
this._munger = new PlatformMunger(platform, this.root, this._platformJson, this._pluginInfoProvider);
}
Api.createPlatform = function (dest, config, options, events) {
var creator = require('../../lib/create');
events = setupEvents(events);
var name = "HelloCordova";
var id = "io.cordova.hellocordova";
if(config) {
name = config.name();
id = config.packageName();
}
var result;
try {
// we create the project using our scripts in this platform
result = creator.createProject(dest, id, name, options)
.then(function () {
// after platform is created we return Api instance based on new Api.js location
// Api.js has been copied to the new project
// This is required to correctly resolve paths in the future api calls
var PlatformApi = require(path.resolve(dest, 'cordova/Api'));
return new PlatformApi('browser', dest, events);
});
}
catch(e) {
events.emit('error','createPlatform is not callable from the browser project API.');
throw(e);
}
return result;
};
Api.updatePlatform = function (dest, options, events) {
// console.log("test-platform:Api:updatePlatform");
// todo?: create projectInstance and fulfill promise with it.
return Promise.resolve();
};
Api.prototype.getPlatformInfo = function () {
// console.log("browser-platform:Api:getPlatformInfo");
// return PlatformInfo object
return {
"locations":this.locations,
"root": this.root,
"name": this.platform,
"version": { "version" : "1.0.0" }, // um, todo!
"projectConfig": this.config
};
};
Api.prototype.prepare = function (cordovaProject,options) {
// First cleanup current config and merge project's one into own
var defaultConfigPath = path.join(this.locations.platformRootDir,'cordova',
'defaults.xml');
var ownConfigPath = this.locations.configXml;
var sourceCfg = cordovaProject.projectConfig;
// If defaults.xml is present, overwrite platform config.xml with it.
// Otherwise save whatever is there as defaults so it can be
// restored or copy project config into platform if none exists.
if (fs.existsSync(defaultConfigPath)) {
this.events.emit('verbose', 'Generating config.xml from defaults for platform "' + this.platform + '"');
shell.cp('-f', defaultConfigPath, ownConfigPath);
}
else if (fs.existsSync(ownConfigPath)) {
this.events.emit('verbose', 'Generating defaults.xml from own config.xml for platform "' + this.platform + '"');
shell.cp('-f', ownConfigPath, defaultConfigPath);
}
else {
this.events.emit('verbose', 'case 3"' + this.platform + '"');
shell.cp('-f', sourceCfg.path, ownConfigPath);
}
// merge our configs
this.config = new ConfigParser(ownConfigPath);
xmlHelpers.mergeXml(cordovaProject.projectConfig.doc.getroot(),
this.config.doc.getroot(),
this.platform, true);
this.config.write();
// Update own www dir with project's www assets and plugins' assets and js-files
this.parser.update_www(cordovaProject.locations.www);
// Copy or Create manifest.json
// todo: move this to a manifest helper module
// output path
var manifestPath = path.join(this.locations.www,'manifest.json');
var srcManifestPath =path.join(cordovaProject.locations.www,'manifest.json');
if(fs.existsSync(srcManifestPath)) {
// just blindly copy it to our output/www
// todo: validate it? ensure all properties we expect exist?
this.events.emit('verbose','copying ' + srcManifestPath + ' => ' + manifestPath);
shell.cp('-f',srcManifestPath,manifestPath);
}
else {
var manifestJson = {
"background_color": "#FFF",
"display": "standalone"
};
if(this.config){
if(this.config.name()) {
manifestJson.name = this.config.name();
}
if(this.config.shortName()) {
manifestJson.short_name = this.config.shortName();
}
if(this.config.packageName()) {
manifestJson.version = this.config.packageName();
}
if(this.config.description()) {
manifestJson.description = this.config.description();
}
if(this.config.author()) {
manifestJson.author = this.config.author();
}
// icons
var icons = this.config.getStaticResources('browser','icon');
var manifestIcons = icons.map(function(icon) {
// given a tag like this :
// <icon src="res/ios/icon.png" width="57" height="57" density="mdpi" />
/* configParser returns icons that look like this :
{ src: 'res/ios/icon.png',
target: undefined,
density: 'mdpi',
platform: null,
width: 57,
height: 57
} ******/
/* manifest expects them to be like this :
{ "src": "images/touch/icon-128x128.png",
"type": "image/png",
"sizes": "128x128"
} ******/
// ?Is it worth looking at file extentions?
return {"src":icon.src, "type":"image/png",
"sizes":(icon.width + "x" + icon.height)};
});
manifestJson.icons = manifestIcons;
// orientation
// <preference name="Orientation" value="landscape" />
var oriPref = this.config.getGlobalPreference('Orientation');
if(oriPref) {
// if it's a supported value, use it
if(["landscape","portrait"].indexOf(oriPref) > -1) {
manifestJson.orientation = oriPref;
}
else { // anything else maps to 'any'
manifestJson.orientation = 'any';
}
}
// get start_url
var contentNode = this.config.doc.find('content') || {'attrib':{'src':'index.html'}}; // sensible default
manifestJson.start_url = contentNode.attrib.src;
// now we get some values from start_url page ...
var startUrlPath = path.join(cordovaProject.locations.www,manifestJson.start_url);
if(fs.existsSync(startUrlPath)) {
var contents = fs.readFileSync(startUrlPath, 'utf-8');
// matches <meta name="theme-color" content="#FF0044">
var themeColorRegex = /<meta(?=[^>]*name="theme-color")\s[^>]*content="([^>]*)"/i;
var result = themeColorRegex.exec(contents);
var themeColor;
if(result && result.length>=2) {
themeColor = result[1];
}
else { // see if there is a preference in config.xml
// <preference name="StatusBarBackgroundColor" value="#000000" />
themeColor = this.config.getGlobalPreference('StatusBarBackgroundColor');
}
if(themeColor) {
manifestJson.theme_color = themeColor;
}
}
}
fs.writeFileSync(manifestPath, JSON.stringify(manifestJson, null, 2), 'utf8');
}
// update project according to config.xml changes.
return this.parser.update_project(this.config, options);
};
Api.prototype.addPlugin = function (pluginInfo, installOptions) {
// console.log(new Error().stack);
if (!pluginInfo) {
return Promise.reject('The parameter is incorrect. The first parameter ' +
'should be valid PluginInfo instance');
}
installOptions = installOptions || {};
installOptions.variables = installOptions.variables || {};
// CB-10108 platformVersion option is required for proper plugin installation
installOptions.platformVersion = installOptions.platformVersion ||
this.getPlatformInfo().version;
var self = this;
var actions = new ActionStack();
var projectFile = this._handler.parseProjectFile && this._handler.parseProjectFile(this.root);
// gather all files needs to be handled during install
pluginInfo.getFilesAndFrameworks(this.platform)
.concat(pluginInfo.getAssets(this.platform))
.concat(pluginInfo.getJsModules(this.platform))
.forEach(function(item) {
actions.push(actions.createAction(
self._getInstaller(item.itemType),
[item, pluginInfo.dir, pluginInfo.id, installOptions, projectFile],
self._getUninstaller(item.itemType),
[item, pluginInfo.dir, pluginInfo.id, installOptions, projectFile]));
});
// run through the action stack
return actions.process(this.platform, this.root)
.then(function () {
if (projectFile) {
projectFile.write();
}
// Add PACKAGE_NAME variable into vars
if (!installOptions.variables.PACKAGE_NAME) {
installOptions.variables.PACKAGE_NAME = self._handler.package_name(self.root);
}
self._munger
// Ignore passed `is_top_level` option since platform itself doesn't know
// anything about managing dependencies - it's responsibility of caller.
.add_plugin_changes(pluginInfo, installOptions.variables, /*is_top_level=*/true, /*should_increment=*/true)
.save_all();
var targetDir = installOptions.usePlatformWww ?
self.getPlatformInfo().locations.platformWww :
self.getPlatformInfo().locations.www;
self._addModulesInfo(pluginInfo, targetDir);
});
};
Api.prototype.removePlugin = function (plugin, uninstallOptions) {
//console.log("NotImplemented :: browser-platform:Api:removePlugin ",plugin, uninstallOptions);
uninstallOptions = uninstallOptions || {};
// CB-10108 platformVersion option is required for proper plugin installation
uninstallOptions.platformVersion = uninstallOptions.platformVersion ||
this.getPlatformInfo().version;
var self = this;
var actions = new ActionStack();
var projectFile = this._handler.parseProjectFile && this._handler.parseProjectFile(this.root);
// queue up plugin files
plugin.getFilesAndFrameworks(this.platform)
.concat(plugin.getAssets(this.platform))
.concat(plugin.getJsModules(this.platform))
.forEach(function(item) {
actions.push(actions.createAction(
self._getUninstaller(item.itemType), [item, plugin.dir, plugin.id, uninstallOptions, projectFile],
self._getInstaller(item.itemType), [item, plugin.dir, plugin.id, uninstallOptions, projectFile]));
});
// run through the action stack
return actions.process(this.platform, this.root)
.then(function() {
if (projectFile) {
projectFile.write();
}
self._munger
// Ignore passed `is_top_level` option since platform itself doesn't know
// anything about managing dependencies - it's responsibility of caller.
.remove_plugin_changes(plugin, /*is_top_level=*/true)
.save_all();
var targetDir = uninstallOptions.usePlatformWww ?
self.getPlatformInfo().locations.platformWww :
self.getPlatformInfo().locations.www;
self._removeModulesInfo(plugin, targetDir);
// Remove stale plugin directory
// TODO: this should be done by plugin files uninstaller
shell.rm('-rf', path.resolve(self.root, 'Plugins', plugin.id));
});
};
Api.prototype._getInstaller = function(type) {
var self = this;
return function (item, plugin_dir, plugin_id, options, project) {
var installer = self._handler[type];
if(!installer) {
console.log("unrecognized type " + type);
return;
}
else {
var wwwDest = options.usePlatformWww ?
self.getPlatformInfo().locations.platformWww :
self._handler.www_dir(self.root);
if(type === 'asset') {
installer.install(item, plugin_dir, wwwDest);
}
else if(type === 'js-module') {
installer.install(item, plugin_dir, plugin_id, wwwDest);
}
else {
installer.install(item, plugin_dir, self.root, plugin_id, options, project);
}
}
};
};
Api.prototype._getUninstaller = function(type) {
var self = this;
return function (item, plugin_dir, plugin_id, options, project) {
var installer = self._handler[type];
if(!installer) {
console.log("browser plugin uninstall: unrecognized type, skipping : " + type);
return;
}
else {
var wwwDest = options.usePlatformWww ?
self.getPlatformInfo().locations.platformWww :
self._handler.www_dir(self.root);
if(['asset','js-module'].indexOf(type) > -1) {
return installer.uninstall(item, wwwDest, plugin_id);
}
else {
return installer.uninstall(item, self.root, plugin_id, options, project);
}
}
};
};
/**
* Removes the specified modules from list of installed modules and updates
* platform_json and cordova_plugins.js on disk.
*
* @param {PluginInfo} plugin PluginInfo instance for plugin, which modules
* needs to be added.
* @param {String} targetDir The directory, where updated cordova_plugins.js
* should be written to.
*/
Api.prototype._addModulesInfo = function(plugin, targetDir) {
var installedModules = this._platformJson.root.modules || [];
var installedPaths = installedModules.map(function (installedModule) {
return installedModule.file;
});
var modulesToInstall = plugin.getJsModules(this.platform)
.filter(function (moduleToInstall) {
return installedPaths.indexOf(moduleToInstall.file) === -1;
}).map(function (moduleToInstall) {
var moduleName = plugin.id + '.' + ( moduleToInstall.name || moduleToInstall.src.match(/([^\/]+)\.js/)[1] );
var obj = {
file: ['plugins', plugin.id, moduleToInstall.src].join('/'),
id: moduleName,
pluginId: plugin.id
};
if (moduleToInstall.clobbers.length > 0) {
obj.clobbers = moduleToInstall.clobbers.map(function(o) { return o.target; });
}
if (moduleToInstall.merges.length > 0) {
obj.merges = moduleToInstall.merges.map(function(o) { return o.target; });
}
if (moduleToInstall.runs) {
obj.runs = true;
}
return obj;
});
this._platformJson.root.modules = installedModules.concat(modulesToInstall);
if (!this._platformJson.root.plugin_metadata) {
this._platformJson.root.plugin_metadata = {};
}
this._platformJson.root.plugin_metadata[plugin.id] = plugin.version;
this._writePluginModules(targetDir);
this._platformJson.save();
};
/**
* Fetches all installed modules, generates cordova_plugins contents and writes
* it to file.
*
* @param {String} targetDir Directory, where write cordova_plugins.js to.
* Ususally it is either <platform>/www or <platform>/platform_www
* directories.
*/
Api.prototype._writePluginModules = function (targetDir) {
// Write out moduleObjects as JSON wrapped in a cordova module to cordova_plugins.js
var final_contents = 'cordova.define(\'cordova/plugin_list\', function(require, exports, module) {\n';
final_contents += 'module.exports = ' + JSON.stringify(this._platformJson.root.modules, null, ' ') + ';\n';
final_contents += 'module.exports.metadata = \n';
final_contents += '// TOP OF METADATA\n';
final_contents += JSON.stringify(this._platformJson.root.plugin_metadata || {}, null, ' ') + '\n';
final_contents += '// BOTTOM OF METADATA\n';
final_contents += '});'; // Close cordova.define.
shell.mkdir('-p', targetDir);
fs.writeFileSync(path.join(targetDir, 'cordova_plugins.js'), final_contents, 'utf-8');
};
/**
* Removes the specified modules from list of installed modules and updates
* platform_json and cordova_plugins.js on disk.
*
* @param {PluginInfo} plugin PluginInfo instance for plugin, which modules
* needs to be removed.
* @param {String} targetDir The directory, where updated cordova_plugins.js
* should be written to.
*/
Api.prototype._removeModulesInfo = function(plugin, targetDir) {
var installedModules = this._platformJson.root.modules || [];
var modulesToRemove = plugin.getJsModules(this.platform)
.map(function (jsModule) {
return ['plugins', plugin.id, jsModule.src].join('/');
});
var updatedModules = installedModules
.filter(function (installedModule) {
return (modulesToRemove.indexOf(installedModule.file) === -1);
});
this._platformJson.root.modules = updatedModules;
if (this._platformJson.root.plugin_metadata) {
delete this._platformJson.root.plugin_metadata[plugin.id];
}
this._writePluginModules(targetDir);
this._platformJson.save();
};
Api.prototype.build = function (buildOptions) {
var self = this;
return require('./lib/check_reqs').run()
.then(function () {
return require('./lib/build').run.call(self, buildOptions);
});
};
Api.prototype.run = function(runOptions) {
return require('./lib/run').run(runOptions);
};
Api.prototype.clean = function(cleanOptions) {
return require('./lib/clean').run(cleanOptions);
};
Api.prototype.requirements = function() {
return require('./lib/check_reqs').run();
};
module.exports = Api;