http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6b98390e/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/AppxManifest.js ---------------------------------------------------------------------- diff --git a/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/AppxManifest.js b/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/AppxManifest.js new file mode 100644 index 0000000..1875bf7 --- /dev/null +++ b/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/AppxManifest.js @@ -0,0 +1,733 @@ +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +var fs = require('fs'); +var util = require('util'); +var et = require('elementtree'); +var path = require('path'); +var xml= require('cordova-common').xmlHelpers; + +var UAP_RESTRICTED_CAPS = ['enterpriseAuthentication', 'sharedUserCertificates', + 'documentsLibrary', 'musicLibrary', 'picturesLibrary', + 'videosLibrary', 'removableStorage', 'internetClientClientServer', + 'privateNetworkClientServer']; + +// UAP namespace capabilities come from the XSD type ST_Capability_Uap from AppxManifestTypes.xsd +var CAPS_NEEDING_UAPNS = ['documentsLibrary', 'picturesLibrary', 'videosLibrary', + 'musicLibrary', 'enterpriseAuthentication', 'sharedUserCertificates', + 'removableStorage', 'appointments', 'contacts', 'userAccountInformation', + 'phoneCall', 'blockedChatMessages', 'objects3D']; + +var KNOWN_ORIENTATIONS = { + 'default': ['portrait', 'landscape', 'landscapeFlipped'], + 'portrait': ['portrait'], + 'landscape': ['landscape', 'landscapeFlipped'] +}; + +/** + * Store to cache appxmanifest files based on file location + * @type {Object} + */ +var manifestCache = {}; + +/** + * @constructor + * @constructs AppxManifest + * + * Wraps an AppxManifest file. Shouldn't be instantiated directly. + * AppxManifest.get should be used instead to select proper manifest type + * (AppxManifest for Win 8/8.1/Phone 8.1, Win10AppxManifest for Win 10) + * + * @param {string} path Path to appxmanifest to wrap + * @param {string} prefix A namespace prefix used to prepend some elements. + * Depends on manifest type. + */ +function AppxManifest(path, prefix) { + this.path = path; + // Append ':' to prefix if needed + prefix = prefix || ''; + this.prefix = (prefix.indexOf(':') === prefix.length - 1) ? prefix : prefix + ':'; + this.doc = xml.parseElementtreeSync(path); + if (this.doc.getroot().tag !== 'Package') { + // Some basic validation + throw new Error(path + ' has incorrect root node name (expected "Package")'); + } + + // Indicates that this manifest is for phone application (either WinPhone 8.1 or Universal Windows 10) + this.hasPhoneIdentity = this.prefix === 'uap:' || this.prefix === 'm3:'; +} + +// Static read-only property to get capabilities which need to be prefixed with uap +Object.defineProperty(AppxManifest, 'CapsNeedUapPrefix', { + writable: false, + configurable: false, + value: CAPS_NEEDING_UAPNS +}); + +/** + * @static + * @constructs AppxManifest|Win10AppxManifest + * + * Instantiates a new AppxManifest/Win10AppxManifest class. Chooses which + * constructor to use based on xmlns attributes of Package node + * + * @param {String} fileName File to create manifest for + * @param {Boolean} [ignoreCache=false] Specifies, whether manifest cache will be + * used to return resultant object + * + * @return {AppxManifest|Win10AppxManifest} Manifest instance + */ +AppxManifest.get = function (fileName, ignoreCache) { + + if (!ignoreCache && manifestCache[fileName]) { + return manifestCache[fileName]; + } + + var root = xml.parseElementtreeSync(fileName).getroot(); + var prefixes = Object.keys(root.attrib) + .reduce(function (result, attrib) { + if (attrib.indexOf('xmlns') === 0 && attrib !== 'xmlns:mp') { + result.push(attrib.replace('xmlns', '').replace(':', '')); + } + + return result; + }, []).sort(); + + var prefix = prefixes[prefixes.length - 1]; + var Manifest = prefix === 'uap' ? Win10AppxManifest : AppxManifest; + var result = new Manifest(fileName, prefix); + + if (!ignoreCache) { + manifestCache[fileName] = result; + } + + return result; +}; + +/** + * Removes manifests from cache to prevent using stale entries + * + * @param {String|String[]} [cacheKeys] The keys to delete from cache. If not + * specified, the whole cache will be purged + */ +AppxManifest.purgeCache = function (cacheKeys) { + if (!cacheKeys) { + // if no arguments passed, remove all entries + manifestCache = {}; + return; + } + + var keys = Array.isArray(cacheKeys) ? cacheKeys : [cacheKeys]; + keys.forEach(function (key) { + delete manifestCache[key]; + }); +}; + +AppxManifest.prototype.getPhoneIdentity = function () { + var phoneIdentity = this.doc.getroot().find('./mp:PhoneIdentity'); + if (!phoneIdentity) + throw new Error('Failed to find PhoneIdentity element in appxmanifest at ' + this.path); + + return { + getPhoneProductId: function () { + return phoneIdentity.attrib.PhoneProductId; + }, + setPhoneProductId: function (id) { + if (!id) throw new Error('Argument for "setPhoneProductId" must be defined in appxmanifest at ' + this.path); + phoneIdentity.attrib.PhoneProductId = id; + return this; + } + }; +}; + +AppxManifest.prototype.getIdentity = function () { + var identity = this.doc.getroot().find('./Identity'); + if (!identity) + throw new Error('Failed to find "Identity" node. The appxmanifest at ' + this.path + ' is invalid'); + + return { + getName: function () { + return identity.attrib.Name; + }, + setName: function (name) { + if (!name) throw new TypeError('Identity.Name attribute must be non-empty in appxmanifest at ' + this.path); + identity.attrib.Name = name; + return this; + }, + getPublisher: function () { + return identity.attrib.Publisher; + }, + setPublisher: function (publisherId) { + if (!publisherId) throw new TypeError('Identity.Publisher attribute must be non-empty in appxmanifest at ' + this.path); + identity.attrib.Publisher = publisherId; + return this; + }, + getVersion: function () { + return identity.attrib.Version; + }, + setVersion: function (version) { + if (!version) throw new TypeError('Identity.Version attribute must be non-empty in appxmanifest at ' + this.path ); + + // Adjust version number as per CB-5337 Windows8 build fails due to invalid app version + if(version && version.match(/\.\d/g)) { + var numVersionComponents = version.match(/\.\d/g).length + 1; + while (numVersionComponents++ < 4) { + version += '.0'; + } + } + + identity.attrib.Version = version; + return this; + } + }; +}; + +AppxManifest.prototype.getProperties = function () { + var properties = this.doc.getroot().find('./Properties'); + + if (!properties) + throw new Error('Failed to find "Properties" node. The appxmanifest at ' + this.path + ' is invalid'); + + return { + getDisplayName: function () { + var displayName = properties.find('./DisplayName'); + return displayName && displayName.text; + }, + setDisplayName: function (name) { + if (!name) throw new TypeError('Properties.DisplayName elements must be non-empty in appxmanifest at ' + this.path); + var displayName = properties.find('./DisplayName'); + + if (!displayName) { + displayName = new et.Element('DisplayName'); + properties.append(displayName); + } + + displayName.text = name; + + return this; + }, + getPublisherDisplayName: function () { + var publisher = properties.find('./PublisherDisplayName'); + return publisher && publisher.text; + }, + setPublisherDisplayName: function (name) { + if (!name) throw new TypeError('Properties.PublisherDisplayName elements must be non-empty in appxmanifest at ' + this.path); + var publisher = properties.find('./PublisherDisplayName'); + + if (!publisher) { + publisher = new et.Element('PublisherDisplayName'); + properties.append(publisher); + } + + publisher.text = name; + + return this; + }, + getDescription: function () { + var description = properties.find('./Description'); + return description && description.text; + }, + setDescription: function (text) { + + var description = properties.find('./Description'); + + if (!text || text.length === 0) { + if (description) properties.remove(description); + return this; + } + + if (!description) { + description = new et.Element('Description'); + properties.append(description); + } + + description.text = processDescription(text); + + return this; + }, + }; +}; + +AppxManifest.prototype.getApplication = function () { + var application = this.doc.getroot().find('./Applications/Application'); + if (!application) + throw new Error('Failed to find "Application" element. The appxmanifest at ' + this.path + ' is invalid'); + + var self = this; + + return { + _node: application, + getVisualElements: function () { + return self.getVisualElements(); + }, + getId: function () { + return application.attrib.Id; + }, + setId: function (id) { + if (!id) throw new TypeError('Application.Id attribute must be defined in appxmanifest at ' + this.path); + // 64 symbols restriction goes from manifest schema definition + // http://msdn.microsoft.com/en-us/library/windows/apps/br211415.aspx + var appId = id.length <= 64 ? id : id.substr(0, 64); + application.attrib.Id = appId; + return this; + }, + getStartPage: function () { + return application.attrib.StartPage; + }, + setStartPage: function (page) { + if (!page) page = 'www/index.html'; // Default valur is always index.html + application.attrib.StartPage = page; + return this; + }, + getAccessRules: function () { + return application + .findall('./ApplicationContentUriRules/Rule') + .map(function (rule) { + return rule.attrib.Match; + }); + }, + setAccessRules: function (rules) { + var appUriRules = application.find('ApplicationContentUriRules'); + if (appUriRules) { + application.remove(appUriRules); + } + + // No rules defined + if (!rules || rules.length === 0) { + return; + } + + appUriRules = new et.Element('ApplicationContentUriRules'); + application.append(appUriRules); + + rules.forEach(function(rule) { + appUriRules.append(new et.Element('Rule', {Match: rule, Type: 'include'})); + }); + + return this; + } + }; +}; + +AppxManifest.prototype.getVisualElements = function () { + var self = this; + var visualElements = this.doc.getroot().find('./Applications/Application/' + + this.prefix + 'VisualElements'); + + if (!visualElements) + throw new Error('Failed to find "VisualElements" node. The appxmanifest at ' + this.path + ' is invalid'); + + return { + _node: visualElements, + getDisplayName: function () { + return visualElements.attrib.DisplayName; + }, + setDisplayName: function (name) { + if (!name) throw new TypeError('VisualElements.DisplayName attribute must be defined in appxmanifest at ' + this.path); + visualElements.attrib.DisplayName = name; + return this; + }, + getOrientation: function () { + return visualElements.findall(self.prefix + 'Rotation') + .map(function (element) { + return element.attrib.Preference; + }); + }, + setOrientation: function (orientation) { + if (!orientation || orientation === ''){ + orientation = 'default'; + } + + var rotationPreferenceRootName = self.prefix + 'InitialRotationPreference'; + var rotationPreferenceRoot = visualElements.find('./' + rotationPreferenceRootName); + + if (!orientation && rotationPreferenceRoot) { + // Remove InitialRotationPreference root element to revert to defaults + visualElements.remove(rotationPreferenceRoot); + return this; + } + + if(!rotationPreferenceRoot) { + rotationPreferenceRoot = new et.Element(rotationPreferenceRootName); + visualElements.append(rotationPreferenceRoot); + } + + rotationPreferenceRoot.clear(); + + var orientations = KNOWN_ORIENTATIONS[orientation] || orientation.split(','); + orientations.forEach(function(orientation) { + var el = new et.Element(self.prefix + 'Rotation', {Preference: orientation} ); + rotationPreferenceRoot.append(el); + }); + + return this; + }, + getBackgroundColor: function () { + return visualElements.attrib.BackgroundColor; + }, + setBackgroundColor: function (color) { + if (!color) + throw new TypeError('VisualElements.BackgroundColor attribute must be defined in appxmanifest at ' + this.path); + + visualElements.attrib.BackgroundColor = refineColor(color); + return this; + }, + trySetBackgroundColor: function (color) { + try { + return this.setBackgroundColor(color); + } catch (e) { return this; } + }, + getForegroundText: function () { + return visualElements.attrib.ForegroundText; + }, + setForegroundText: function (color) { + // If color is not set, fall back to 'light' by default + visualElements.attrib.ForegroundText = color || 'light'; + + return this; + }, + getSplashBackgroundColor: function () { + var splashNode = visualElements.find('./' + self.prefix + 'SplashScreen'); + return splashNode && splashNode.attrib.BackgroundColor; + }, + setSplashBackgroundColor: function (color) { + var splashNode = visualElements.find('./' + self.prefix + 'SplashScreen'); + if (splashNode) { + if (color) { + splashNode.attrib.BackgroundColor = refineColor(color); + } else { + delete splashNode.attrib.BackgroundColor; + } + } + return this; + }, + getSplashScreenExtension: function (extension) { + var splashNode = visualElements.find('./' + self.prefix + 'SplashScreen'); + return splashNode && splashNode.attrib.Image && path.extname(splashNode.attrib.Image); + }, + setSplashScreenExtension: function (extension) { + var splashNode = visualElements.find('./' + self.prefix + 'SplashScreen'); + if (splashNode) { + var oldPath = splashNode.attrib.Image; + splashNode.attrib.Image = path.dirname(oldPath) + '\\' + path.basename(oldPath, path.extname(oldPath)) + extension; + } + return this; + }, + getToastCapable: function () { + return visualElements.attrib.ToastCapable; + }, + setToastCapable: function (isToastCapable) { + if (isToastCapable === true || isToastCapable.toString().toLowerCase() === 'true') { + visualElements.attrib.ToastCapable = 'true'; + } else { + delete visualElements.attrib.ToastCapable; + } + + return this; + }, + getDescription: function () { + return visualElements.attrib.Description; + }, + setDescription: function (description) { + if (!description || description.length === 0) + throw new TypeError('VisualElements.Description attribute must be defined and non-empty in appxmanifest at ' + this.path); + + visualElements.attrib.Description = processDescription(description); + return this; + }, + }; +}; + +AppxManifest.prototype.getCapabilities = function () { + var capabilities = this.doc.find('./Capabilities'); + if (!capabilities) return []; + + return capabilities.getchildren() + .map(function (element) { + return { type: element.tag, name: element.attrib.Name }; + }); +}; + +function isCSSColorName(color) { + return color.indexOf('0x') === -1 && color.indexOf('#') === -1; +} + +function refineColor(color) { + if (isCSSColorName(color)) { + return color; + } + + // return three-byte hexadecimal number preceded by "#" (required for Windows) + color = color.replace('0x', '').replace('#', ''); + if (color.length == 3) { + color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2]; + } + // alpha is not supported, so we remove it + if (color.length == 8) { // AArrggbb + color = color.slice(2); + } + return '#' + color; +} + +function processDescription(text) { + var result = text; + + // Description value limitations: https://msdn.microsoft.com/en-us/library/windows/apps/br211429.aspx + // value should be no longer than 2048 characters + if (text.length > 2048) { + result = text.substr(0, 2048); + } + + // value should not contain newlines and tabs + return result.replace(/(\n|\r)/g, ' ').replace(/\t/g, ' '); +} + +// Shortcut for getIdentity.setName +AppxManifest.prototype.setPackageName = function (name) { + this.getIdentity().setName(name); + return this; +}; + +// Shortcut for multiple inner methods calls +AppxManifest.prototype.setAppName = function (name) { + this.getProperties().setDisplayName(name); + this.getVisualElements().setDisplayName(name); + + return this; +}; + +/** + * Writes manifest to disk syncronously. If filename is specified, then manifest + * will be written to that file + * + * @param {String} [destPath] File to write manifest to. If omitted, + * manifest will be written to file it has been read from. + */ +AppxManifest.prototype.write = function(destPath) { + // sort Capability elements as per CB-5350 Windows8 build fails due to invalid 'Capabilities' definition + sortCapabilities(this.doc); + fs.writeFileSync(destPath || this.path, this.doc.write({indent: 4}), 'utf-8'); +}; + +/** + * Sorts 'capabilities' elements in manifest in ascending order + * @param {Elementtree.Document} manifest An XML document that represents + * appxmanifest + */ +function sortCapabilities(manifest) { + + // removes namespace prefix (m3:Capability -> Capability) + // this is required since elementtree returns qualified name with namespace + function extractLocalName(tag) { + return tag.split(':').pop(); // takes last part of string after ':' + } + + var capabilitiesRoot = manifest.find('.//Capabilities'), + capabilities = capabilitiesRoot.getchildren() || []; + // to sort elements we remove them and then add again in the appropriate order + capabilities.forEach(function(elem) { // no .clear() method + capabilitiesRoot.remove(elem); + // CB-7601 we need local name w/o namespace prefix to sort capabilities correctly + elem.localName = extractLocalName(elem.tag); + }); + capabilities.sort(function(a, b) { + return (a.localName > b.localName) ? 1: -1; + }); + capabilities.forEach(function(elem) { + capabilitiesRoot.append(elem); + }); +} + + +function Win10AppxManifest(path) { + AppxManifest.call(this, path, /*prefix=*/'uap'); +} + +util.inherits(Win10AppxManifest, AppxManifest); + +Win10AppxManifest.prototype.getApplication = function () { + // Call overridden method + var result = AppxManifest.prototype.getApplication.call(this); + var application = result._node; + + result.getAccessRules = function () { + return application + .findall('./uap:ApplicationContentUriRules/uap:Rule') + .map(function (rule) { + return rule.attrib.Match; + }); + }; + + result.setAccessRules = function (rules) { + var appUriRules = application.find('./uap:ApplicationContentUriRules'); + if (appUriRules) { + application.remove(appUriRules); + } + + // No rules defined + if (!rules || rules.length === 0) { + return; + } + + appUriRules = new et.Element('uap:ApplicationContentUriRules'); + application.append(appUriRules); + + rules.forEach(function(rule) { + appUriRules.append(new et.Element('uap:Rule', { Match: rule, Type: 'include', WindowsRuntimeAccess: 'all' })); + }); + + return this; + }; + + return result; +}; + +Win10AppxManifest.prototype.getVisualElements = function () { + // Call base method and extend its results + var result = AppxManifest.prototype.getVisualElements.call(this); + var defaultTitle = result._node.find('./uap:DefaultTile'); + + result.getDefaultTitle = function () { + return { + getShortName: function () { + return defaultTitle.attrib.ShortName; + }, + setShortName: function (name) { + if (!name) throw new TypeError('Argument for "setDisplayName" must be defined in appxmanifest at ' + this.path); + defaultTitle.attrib.ShortName = name; + return this; + } + }; + }; + + // ToastCapable attribute was removed in Windows 10. + // See https://msdn.microsoft.com/ru-ru/library/windows/apps/dn423310.aspx + result.getToastCapable = function () {}; + result.setToastCapable = function () { return this; }; + + // ForegroundText was removed in Windows 10 as well + result.getForegroundText = function () {}; + result.setForegroundText = function () { return this; }; + + return result; +}; + +// Shortcut for multiple inner methods calls +Win10AppxManifest.prototype.setAppName = function (name) { + // Call base method + AppxManifest.prototype.setAppName.call(this, name); + this.getVisualElements().getDefaultTitle().setShortName(name); + + return this; +}; + +/** + * Checks for capabilities which are Restricted in Windows 10 UAP. + * @return {string[]|false} An array of restricted capability names, or false. + */ +Win10AppxManifest.prototype.getRestrictedCapabilities = function () { + var restrictedCapabilities = this.getCapabilities() + .filter(function (capability) { + return UAP_RESTRICTED_CAPS.indexOf(capability.name) >= 0; + }); + + return restrictedCapabilities.length === 0 ? false : restrictedCapabilities; +}; + +/** + * Sets up a Dependencies section for appxmanifest. If no arguments provided, + * deletes Dependencies section. + * + * @param {Object[]} dependencies Array of arbitrary object, which fields + * will be used to set each dependency attributes. + * + * @returns {Win10AppxManifest} self instance + */ +Win10AppxManifest.prototype.setDependencies = function (dependencies) { + var dependenciesElement = this.doc.find('./Dependencies'); + + if ((!dependencies || dependencies.length === 0) && dependenciesElement) { + this.doc.remove(dependenciesElement); + return this; + } + + if (!dependenciesElement) { + dependenciesElement = new et.Element('Dependencies'); + this.doc.append(dependenciesElement); + } + + if (dependenciesElement.len() > 0) { + dependenciesElement.clear(); + } + + dependencies.forEach(function (uapVersionInfo) { + dependenciesElement.append(new et.Element('TargetDeviceFamily', uapVersionInfo)); + }); +}; + +/** + * Writes manifest to disk syncronously. If filename is specified, then manifest + * will be written to that file + * + * @param {String} [destPath] File to write manifest to. If omitted, + * manifest will be written to file it has been read from. + */ +Win10AppxManifest.prototype.write = function(destPath) { + fs.writeFileSync(destPath || this.path, this.writeToString(), 'utf-8'); +}; + +Win10AppxManifest.prototype.writeToString = function() { + ensureUapPrefixedCapabilities(this.doc.find('.//Capabilities')); + ensureUniqueCapabilities(this.doc.find('.//Capabilities')); + // sort Capability elements as per CB-5350 Windows8 build fails due to invalid 'Capabilities' definition + sortCapabilities(this.doc); + return this.doc.write({indent: 4}); +}; + +/** + * Checks for capabilities which require the uap: prefix in Windows 10. + * @param capabilities {ElementTree.Element} The appx manifest element for <capabilities> + */ +function ensureUapPrefixedCapabilities(capabilities) { + capabilities.getchildren() + .forEach(function(el) { + if (CAPS_NEEDING_UAPNS.indexOf(el.attrib.Name) > -1 && el.tag.indexOf('uap:') !== 0) { + el.tag = 'uap:' + el.tag; + } + }); +} + +/** + * Cleans up duplicate capability declarations that were generated during the prepare process + * @param capabilities {ElementTree.Element} The appx manifest element for <capabilities> + */ +function ensureUniqueCapabilities(capabilities) { + var uniqueCapabilities = []; + capabilities.getchildren() + .forEach(function(el) { + var name = el.attrib.Name; + if (uniqueCapabilities.indexOf(name) !== -1) { + capabilities.remove(el); + } else { + uniqueCapabilities.push(name); + } + }); +} + +module.exports = AppxManifest;
http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6b98390e/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/ConfigChanges.js ---------------------------------------------------------------------- diff --git a/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/ConfigChanges.js b/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/ConfigChanges.js new file mode 100644 index 0000000..c07a77a --- /dev/null +++ b/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/ConfigChanges.js @@ -0,0 +1,164 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +var util = require('util'); +var path = require('path'); +var CommonMunger = require('cordova-common').ConfigChanges.PlatformMunger; +var CapsNeedUapPrefix = require(path.join(__dirname, 'AppxManifest')).CapsNeedUapPrefix; + +var CAPS_SELECTOR = '/Package/Capabilities'; +var WINDOWS10_MANIFEST = 'package.windows10.appxmanifest'; + +function PlatformMunger(platform, project_dir, platformJson, pluginInfoProvider) { + CommonMunger.apply(this, arguments); +} + +util.inherits(PlatformMunger, CommonMunger); + +/** + * This is an override of apply_file_munge method from cordova-common's PlatformMunger class. + * In addition to parent's method logic also removes capabilities with 'uap:' prefix that were + * added by AppxManifest class + * + * @param {String} file A file name to apply munge to + * @param {Object} munge Serialized changes that need to be applied to the file + * @param {Boolean} [remove=false] Flag that specifies whether the changes + * need to be removed or added to the file + */ +PlatformMunger.prototype.apply_file_munge = function (file, munge, remove) { + + // Create a copy to avoid modification of original munge + var mungeCopy = cloneObject(munge); + var capabilities = mungeCopy.parents[CAPS_SELECTOR]; + + if (capabilities) { + // Add 'uap' prefixes for windows 10 manifest + if (file === WINDOWS10_MANIFEST) { + capabilities = generateUapCapabilities(capabilities); + } + + // Remove duplicates and sort capabilities when installing plugin + if (!remove) { + capabilities = getUniqueCapabilities(capabilities).sort(compareCapabilities); + } + + // Put back capabilities into munge's copy + mungeCopy.parents[CAPS_SELECTOR] = capabilities; + } + + PlatformMunger.super_.prototype.apply_file_munge.call(this, file, mungeCopy, remove); +}; + +// Recursive function to clone an object +function cloneObject(obj) { + if (obj === null || typeof obj !== 'object') { + return obj; + } + + var copy = obj.constructor(); + Object.keys(obj).forEach(function(key) { + copy[key] = cloneObject(obj[key]); + }); + + return copy; +} + +/** + * Retrieve capabality name from xml field + * @param {Object} capability with xml field like <Capability Name="CapabilityName"> + * @return {String} name of capability + */ +function getCapabilityName(capability) { + var reg = /Name\s*=\s*"(.*?)"/; + return capability.xml.match(reg)[1]; +} + +/** + * Remove capabilities with same names + * @param {Object} an array of capabilities + * @return {Object} an unique array of capabilities + */ +function getUniqueCapabilities(capabilities) { + return capabilities.reduce(function(uniqueCaps, currCap) { + + var isRepeated = uniqueCaps.some(function(cap) { + return getCapabilityName(cap) === getCapabilityName(currCap); + }); + + return isRepeated ? uniqueCaps : uniqueCaps.concat([currCap]); + }, []); +} + +/** + * Comparator function to pass to Array.sort + * @param {Object} firstCap first capability + * @param {Object} secondCap second capability + * @return {Number} either -1, 0 or 1 + */ +function compareCapabilities(firstCap, secondCap) { + var firstCapName = getCapabilityName(firstCap); + var secondCapName = getCapabilityName(secondCap); + + if (firstCapName < secondCapName) { + return -1; + } + + if (firstCapName > secondCapName) { + return 1; + } + + return 0; +} + + +/** + * Generates a new munge that contains <uap:Capability> elements created based on + * corresponding <Capability> elements from base munge. If there are no such elements + * found in base munge, the empty munge is returned (selectors might be present under + * the 'parents' key, but they will contain no changes). + * + * @param {Object} capabilities A list of capabilities + * @return {Object} A list with 'uap'-prefixed capabilities + */ +function generateUapCapabilities(capabilities) { + + function hasCapabilityChange(change) { + return /^\s*<(\w+:)?(Device)?Capability\s/.test(change.xml); + } + + function createPrefixedCapabilityChange(change) { + if (CapsNeedUapPrefix.indexOf(getCapabilityName(change)) < 0) { + return change; + } + + // If capability is already prefixed, avoid adding another prefix + var replaceXML = change.xml.indexOf('uap:') > 0 ? change.xml : change.xml.replace(/Capability/, 'uap:Capability'); + return { + xml: replaceXML, + count: change.count, + before: change.before + }; + } + + return capabilities + // For every xml change check if it adds a <Capability> element ... + .filter(hasCapabilityChange) + // ... and create a duplicate with 'uap:' prefix + .map(createPrefixedCapabilityChange); + +} + +exports.PlatformMunger = PlatformMunger; http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6b98390e/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/JsprojManager.js ---------------------------------------------------------------------- diff --git a/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/JsprojManager.js b/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/JsprojManager.js new file mode 100644 index 0000000..28b2fa3 --- /dev/null +++ b/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/JsprojManager.js @@ -0,0 +1,626 @@ +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +/* jshint quotmark:false */ + +/* + Helper for dealing with Windows Store JS app .jsproj files + */ + +var fs = require('fs'); +var et = require('elementtree'); +var path = require('path'); +var util = require('util'); +var semver = require('semver'); +var shell = require('shelljs'); +var AppxManifest = require('./AppxManifest'); +var PluginHandler = require('./PluginHandler'); +var events = require('cordova-common').events; +var CordovaError = require('cordova-common').CordovaError; +var xml_helpers = require('cordova-common').xmlHelpers; +var AppxManifest = require('./AppxManifest'); + +var WinCSharpProjectTypeGUID = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"; // .csproj +var WinCplusplusProjectTypeGUID = "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}"; // .vcxproj + +// Match a JavaScript Project +var JSPROJ_REGEXP = /(Project\("\{262852C6-CD72-467D-83FE-5EEB1973A190}"\)\s*=\s*"[^"]+",\s*"[^"]+",\s*"\{[0-9a-f\-]+}"[^\r\n]*[\r\n]*)/gi; + +// Chars in a string that need to be escaped when used in a RegExp +var ESCAPE_REGEXP = /([.?*+\^$\[\]\\(){}|\-])/g; + +function jsprojManager(location) { + this.root = path.dirname(location); + this.isUniversalWindowsApp = path.extname(location).toLowerCase() === ".projitems"; + this.projects = []; + this.master = this.isUniversalWindowsApp ? new proj(location) : new jsproj(location); + this.projectFolder = path.dirname(location); + this.www = path.join(this.root, 'www'); + this.platformWww = path.join(this.root, 'platform_www'); +} + +function getProjectName(pluginProjectXML, relative_path) { + var projNameElt = pluginProjectXML.find("PropertyGroup/ProjectName"); + // Falling back on project file name in case ProjectName is missing + return !!projNameElt ? projNameElt.text : path.basename(relative_path, path.extname(relative_path)); +} + +jsprojManager.getProject = function (directory) { + var projectFiles = shell.ls(path.join(directory, '*.projitems')); + if (projectFiles.length === 0) { + throw (new CordovaError('The directory ' + directory + + ' does not appear to be a Unified Windows Store project (no .projitems file found)')); + } + return new jsprojManager(path.normalize(projectFiles[0])); +}; + +jsprojManager.prototype = { + _projects: null, + + getPackageName: function() { + // CB-10394 Do not cache manifest file while getting package name to avoid problems + // with windows.appxmanifest cached twice (here and in ConfigFile module) + return AppxManifest.get(path.join(this.root, 'package.windows.appxmanifest'), /*ignoreCache=*/true) + .getProperties().getDisplayName(); + }, + + write: function () { + this.master.write(); + if (this._projects) { + var that = this; + this._projects.forEach(function (project) { + if (project !== that.master && project.touched) { + project.write(); + } + }); + } + }, + + addSDKRef: function (incText, targetConditions) { + events.emit('verbose', 'jsprojManager.addSDKRef(incText: ' + incText + ', targetConditions: ' + JSON.stringify(targetConditions) + ')'); + + var item = createItemGroupElement('ItemGroup/SDKReference', incText, targetConditions); + this._getMatchingProjects(targetConditions).forEach(function (project) { + project.appendToRoot(item); + }); + }, + + removeSDKRef: function (incText, targetConditions) { + events.emit('verbose', 'jsprojManager.removeSDKRef(incText: ' + incText + ', targetConditions: ' + JSON.stringify(targetConditions) + ')'); + + this._getMatchingProjects(targetConditions).forEach(function (project) { + project.removeItemGroupElement('ItemGroup/SDKReference', incText, targetConditions); + }); + }, + + addResourceFileToProject: function (sourcePath, destPath, targetConditions) { + events.emit('verbose', 'jsprojManager.addResourceFile(sourcePath: ' + sourcePath + ', destPath: ' + destPath + ', targetConditions: ' + JSON.stringify(targetConditions) + ')'); + + // add hint path with full path + var link = new et.Element('Link'); + link.text = destPath; + var children = [link]; + + var copyToOutputDirectory = new et.Element('CopyToOutputDirectory'); + copyToOutputDirectory.text = 'Always'; + children.push(copyToOutputDirectory); + + var item = createItemGroupElement('ItemGroup/Content', sourcePath, targetConditions, children); + + this._getMatchingProjects(targetConditions).forEach(function (project) { + project.appendToRoot(item); + }); + }, + + removeResourceFileFromProject: function (relPath, targetConditions) { + events.emit('verbose', 'jsprojManager.removeResourceFile(relPath: ' + relPath + ', targetConditions: ' + JSON.stringify(targetConditions) + ')'); + this._getMatchingProjects(targetConditions).forEach(function (project) { + project.removeItemGroupElement('ItemGroup/Content', relPath, targetConditions); + }); + }, + + addReference: function (relPath, targetConditions, implPath) { + events.emit('verbose', 'jsprojManager.addReference(incText: ' + relPath + ', targetConditions: ' + JSON.stringify(targetConditions) + ')'); + + // add hint path with full path + var hint_path = new et.Element('HintPath'); + hint_path.text = relPath; + var children = [hint_path]; + + var extName = path.extname(relPath); + if (extName === ".winmd") { + var mdFileTag = new et.Element("IsWinMDFile"); + mdFileTag.text = "true"; + children.push(mdFileTag); + } + + // We only need to add <Implementation> tag when dll base name differs from winmd name + if (implPath && path.basename(relPath, '.winmd') !== path.basename(implPath, '.dll')) { + var implementTag = new et.Element('Implementation'); + implementTag.text = path.basename(implPath); + children.push(implementTag); + } + + var item = createItemGroupElement('ItemGroup/Reference', path.basename(relPath, extName), targetConditions, children); + this._getMatchingProjects(targetConditions).forEach(function (project) { + project.appendToRoot(item); + }); + }, + + removeReference: function (relPath, targetConditions) { + events.emit('verbose', 'jsprojManager.removeReference(incText: ' + relPath + ', targetConditions: ' + JSON.stringify(targetConditions) + ')'); + + var extName = path.extname(relPath); + var includeText = path.basename(relPath, extName); + + this._getMatchingProjects(targetConditions).forEach(function (project) { + project.removeItemGroupElement('ItemGroup/Reference', includeText, targetConditions); + }); + }, + + addSourceFile: function (relative_path) { + events.emit('verbose', 'jsprojManager.addSourceFile(relative_path: ' + relative_path + ')'); + this.master.addSourceFile(relative_path); + }, + + removeSourceFile: function (relative_path) { + events.emit('verbose', 'jsprojManager.removeSourceFile(incText: ' + relative_path + ')'); + this.master.removeSourceFile(relative_path); + }, + + addProjectReference: function (relative_path, targetConditions) { + events.emit('verbose', 'jsprojManager.addProjectReference(incText: ' + relative_path + ', targetConditions: ' + JSON.stringify(targetConditions) + ')'); + + // relative_path is the actual path to the file in the current OS, where-as inserted_path is what we write in + // the project file, and is always in Windows format. + relative_path = path.normalize(relative_path); + var inserted_path = relative_path.split('/').join('\\'); + + var pluginProjectXML = xml_helpers.parseElementtreeSync(path.resolve(this.projectFolder, relative_path)); + + // find the guid + name of the referenced project + var projectGuid = pluginProjectXML.find("PropertyGroup/ProjectGuid").text; + var projName = getProjectName(pluginProjectXML, relative_path); + + // get the project type + var projectTypeGuid = getProjectTypeGuid(relative_path); + if (!projectTypeGuid) { + throw new CordovaError("Unrecognized project type at " + relative_path + " (not .csproj or .vcxproj)"); + } + + var preInsertText = "\tProjectSection(ProjectDependencies) = postProject\r\n" + + "\t\t" + projectGuid + "=" + projectGuid + "\r\n" + + "\tEndProjectSection\r\n"; + var postInsertText = '\r\nProject("' + projectTypeGuid + '") = "' + + projName + '", "' + inserted_path + '", ' + + '"' + projectGuid + '"\r\nEndProject'; + + var matchingProjects = this._getMatchingProjects(targetConditions); + if (matchingProjects.length === 0) { + // No projects meet the specified target and version criteria, so nothing to do. + return; + } + + // Will we be writing into the .projitems file rather than individual .jsproj files? + var useProjItems = this.isUniversalWindowsApp && matchingProjects.length === 1 && matchingProjects[0] === this.master; + + // There may be multiple solution files (for different VS versions) - process them all + getSolutionPaths(this.projectFolder).forEach(function (solutionPath) { + var solText = fs.readFileSync(solutionPath, {encoding: "utf8"}); + + if (useProjItems) { + // Insert a project dependency into every jsproj in the solution. + var jsProjectFound = false; + solText = solText.replace(JSPROJ_REGEXP, function (match) { + jsProjectFound = true; + return match + preInsertText; + }); + + if (!jsProjectFound) { + throw new CordovaError("No jsproj found in solution"); + } + } else { + // Insert a project dependency only for projects that match specified target and version + matchingProjects.forEach(function (project) { + solText = solText.replace(getJsProjRegExForProject(path.basename(project.location)), function (match) { + return match + preInsertText; + }); + }); + } + + // Add the project after existing projects. Note that this fairly simplistic check should be fine, since the last + // EndProject in the file should actually be an EndProject (and not an EndProjectSection, for example). + var pos = solText.lastIndexOf("EndProject"); + if (pos === -1) { + throw new Error("No EndProject found in solution"); + } + pos += 10; // Move pos to the end of EndProject text + solText = solText.slice(0, pos) + postInsertText + solText.slice(pos); + + fs.writeFileSync(solutionPath, solText, {encoding: "utf8"}); + }); + + // Add the ItemGroup/ProjectReference to each matching cordova project : + // <ItemGroup><ProjectReference Include="blahblah.csproj"/></ItemGroup> + var item = createItemGroupElement('ItemGroup/ProjectReference', inserted_path, targetConditions); + matchingProjects.forEach(function (project) { + project.appendToRoot(item); + }); + }, + + removeProjectReference: function (relative_path, targetConditions) { + events.emit('verbose', 'jsprojManager.removeProjectReference(incText: ' + relative_path + ', targetConditions: ' + JSON.stringify(targetConditions) + ')'); + + // relative_path is the actual path to the file in the current OS, where-as inserted_path is what we write in + // the project file, and is always in Windows format. + relative_path = path.normalize(relative_path); + var inserted_path = relative_path.split('/').join('\\'); + + // find the guid + name of the referenced project + var pluginProjectXML = xml_helpers.parseElementtreeSync(path.resolve(this.projectFolder, relative_path)); + var projectGuid = pluginProjectXML.find("PropertyGroup/ProjectGuid").text; + var projName = getProjectName(pluginProjectXML, relative_path); + + // get the project type + var projectTypeGuid = getProjectTypeGuid(relative_path); + if (!projectTypeGuid) { + throw new Error("Unrecognized project type at " + relative_path + " (not .csproj or .vcxproj)"); + } + + var preInsertTextRegExp = getProjectReferencePreInsertRegExp(projectGuid); + var postInsertTextRegExp = getProjectReferencePostInsertRegExp(projName, projectGuid, inserted_path, projectTypeGuid); + + // There may be multiple solutions (for different VS versions) - process them all + getSolutionPaths(this.projectFolder).forEach(function (solutionPath) { + var solText = fs.readFileSync(solutionPath, {encoding: "utf8"}); + + // To be safe (to handle subtle changes in formatting, for example), use a RegExp to find and remove + // preInsertText and postInsertText + + solText = solText.replace(preInsertTextRegExp, function () { + return ""; + }); + + solText = solText.replace(postInsertTextRegExp, function () { + return ""; + }); + + fs.writeFileSync(solutionPath, solText, {encoding: "utf8"}); + }); + + this._getMatchingProjects(targetConditions).forEach(function (project) { + project.removeItemGroupElement('ItemGroup/ProjectReference', inserted_path, targetConditions); + }); + }, + + _getMatchingProjects: function (targetConditions) { + // If specified, target can be 'all' (default), 'phone' or 'windows'. Ultimately should probably allow a comma + // separated list, but not needed now. + var target = getDeviceTarget(targetConditions); + var versions = getVersions(targetConditions); + + if (target || versions) { + var matchingProjects = this.projects.filter(function (project) { + return (!target || target === project.target) && + (!versions || semver.satisfies(project.getSemVersion(), versions, /* loose */ true)); + }); + + if (matchingProjects.length < this.projects.length) { + return matchingProjects; + } + } + + // All projects match. If this is a universal project, return the projitems file. Otherwise return our single + // project. + return [this.master]; + }, + + get projects() { + var projects = this._projects; + if (!projects) { + projects = []; + this._projects = projects; + + if (this.isUniversalWindowsApp) { + var projectPath = this.projectFolder; + var projectFiles = shell.ls(path.join(projectPath, '*.jsproj')); + projectFiles.forEach(function (projectFile) { + projects.push(new jsproj(projectFile)); + }); + } else { + this.projects.push(this.master); + } + } + + return projects; + } +}; + +jsprojManager.prototype.getInstaller = function (type) { + return PluginHandler.getInstaller(type); +}; + +jsprojManager.prototype.getUninstaller = function (type) { + return PluginHandler.getUninstaller(type); +}; + +function getProjectReferencePreInsertRegExp(projectGuid) { + projectGuid = escapeRegExpString(projectGuid); + return new RegExp("\\s*ProjectSection\\(ProjectDependencies\\)\\s*=\\s*postProject\\s*" + projectGuid + "\\s*=\\s*" + projectGuid + "\\s*EndProjectSection", "gi"); +} + +function getProjectReferencePostInsertRegExp(projName, projectGuid, relative_path, projectTypeGuid) { + projName = escapeRegExpString(projName); + projectGuid = escapeRegExpString(projectGuid); + relative_path = escapeRegExpString(relative_path); + projectTypeGuid = escapeRegExpString(projectTypeGuid); + return new RegExp('\\s*Project\\("' + projectTypeGuid + '"\\)\\s*=\\s*"' + projName + '"\\s*,\\s*"' + relative_path + '"\\s*,\\s*"' + projectGuid + '"\\s*EndProject', 'gi'); +} + +function getSolutionPaths(projectFolder) { + return shell.ls(path.join(projectFolder, "*.sln")); +} + +function escapeRegExpString(regExpString) { + return regExpString.replace(ESCAPE_REGEXP, "\\$1"); +} + +function getJsProjRegExForProject(projectFile) { + projectFile = escapeRegExpString(projectFile); + return new RegExp('(Project\\("\\{262852C6-CD72-467D-83FE-5EEB1973A190}"\\)\\s*=\\s*"[^"]+",\\s*"' + projectFile + '",\\s*"\\{[0-9a-f\\-]+}"[^\\r\\n]*[\\r\\n]*)', 'gi'); +} + +function getProjectTypeGuid(projectPath) { + switch (path.extname(projectPath)) { + case ".vcxproj": + return WinCplusplusProjectTypeGUID; + + case ".csproj": + return WinCSharpProjectTypeGUID; + } + return null; +} + +function createItemGroupElement(path, incText, targetConditions, children) { + path = path.split('/'); + path.reverse(); + + var lastElement = null; + path.forEach(function (elementName) { + var element = new et.Element(elementName); + if (lastElement) { + element.append(lastElement); + } else { + element.attrib.Include = incText; + + var condition = createConditionAttrib(targetConditions); + if (condition) { + element.attrib.Condition = condition; + } + + if (children) { + children.forEach(function (child) { + element.append(child); + }); + } + } + lastElement = element; + }); + + return lastElement; +} + +function getDeviceTarget(targetConditions) { + var target = targetConditions.deviceTarget; + if (target) { + target = target.toLowerCase().trim(); + if (target === "all") { + target = null; + } else if (target === "win") { + // Allow "win" as alternative to "windows" + target = "windows"; + } else if (target !== 'phone' && target !== 'windows') { + throw new Error('Invalid device-target attribute (must be "all", "phone", "windows" or "win"): ' + target); + } + } + return target; +} + +function getVersions(targetConditions) { + var versions = targetConditions.versions; + if (versions && !semver.validRange(versions, /* loose */ true)) { + throw new Error('Invalid versions attribute (must be a valid semantic version range): ' + versions); + } + return versions; +} + + +/* proj */ + +function proj(location) { + // Class to handle simple project xml operations + if (!location) { + throw new Error('Project file location can\'t be null or empty'); + } + this.location = location; + this.xml = xml_helpers.parseElementtreeSync(location); +} + +proj.prototype = { + write: function () { + fs.writeFileSync(this.location, this.xml.write({indent: 4}), 'utf-8'); + }, + + appendToRoot: function (element) { + this.touched = true; + this.xml.getroot().append(element); + }, + + removeItemGroupElement: function (path, incText, targetConditions) { + var xpath = path + '[@Include="' + incText + '"]'; + var condition = createConditionAttrib(targetConditions); + if (condition) { + xpath += '[@Condition="' + condition + '"]'; + } + xpath += '/..'; + + var itemGroup = this.xml.find(xpath); + if (itemGroup) { + this.touched = true; + this.xml.getroot().remove(itemGroup); + } + }, + + addSourceFile: function (relative_path) { + // we allow multiple paths to be passed at once as array so that + // we don't create separate ItemGroup for each source file, CB-6874 + if (!(relative_path instanceof Array)) { + relative_path = [relative_path]; + } + + // make ItemGroup to hold file. + var item = new et.Element('ItemGroup'); + + relative_path.forEach(function (filePath) { + // filePath is never used to find the actual file - it determines what we write to the project file, and so + // should always be in Windows format. + filePath = filePath.split('/').join('\\'); + + var content = new et.Element('Content'); + content.attrib.Include = filePath; + item.append(content); + }); + + this.appendToRoot(item); + }, + + removeSourceFile: function (relative_path) { + var isRegexp = relative_path instanceof RegExp; + if (!isRegexp) { + // relative_path is never used to find the actual file - it determines what we write to the project file, + // and so should always be in Windows format. + relative_path = relative_path.split('/').join('\\'); + } + + var root = this.xml.getroot(); + var that = this; + // iterate through all ItemGroup/Content elements and remove all items matched + this.xml.findall('ItemGroup').forEach(function (group) { + // matched files in current ItemGroup + var filesToRemove = group.findall('Content').filter(function (item) { + if (!item.attrib.Include) { + return false; + } + return isRegexp ? item.attrib.Include.match(relative_path) : item.attrib.Include === relative_path; + }); + + // nothing to remove, skip.. + if (filesToRemove.length < 1) { + return; + } + + filesToRemove.forEach(function (file) { + // remove file reference + group.remove(file); + }); + // remove ItemGroup if empty + if (group.findall('*').length < 1) { + that.touched = true; + root.remove(group); + } + }); + } +}; + + +/* jsproj */ + +function jsproj(location) { + function targetPlatformIdentifierToDevice(jsprojPlatform) { + var index = ["Windows", "WindowsPhoneApp", "UAP"].indexOf(jsprojPlatform); + if (index < 0) { + throw new Error("Unknown TargetPlatformIdentifier '" + jsprojPlatform + "' in project file '" + location + "'"); + } + return ["windows", "phone", "windows"][index]; + } + + function validateVersion(version) { + version = version.split('.'); + while (version.length < 3) { + version.push("0"); + } + return version.join("."); + } + + // Class to handle a jsproj file + proj.call(this, location); + + var propertyGroup = this.xml.find('PropertyGroup[TargetPlatformIdentifier]'); + if (!propertyGroup) { + throw new Error("Unable to find PropertyGroup/TargetPlatformIdentifier in project file '" + this.location + "'"); + } + + var jsprojPlatform = propertyGroup.find('TargetPlatformIdentifier').text; + this.target = targetPlatformIdentifierToDevice(jsprojPlatform); + + var version = propertyGroup.find('TargetPlatformVersion'); + if (!version) { + throw new Error("Unable to find PropertyGroup/TargetPlatformVersion in project file '" + this.location + "'"); + } + this.version = validateVersion(version.text); +} + +util.inherits(jsproj, proj); + +jsproj.prototype.target = null; +jsproj.prototype.version = null; + +// Returns valid semantic version (http://semver.org/). +jsproj.prototype.getSemVersion = function () { + // For example, for version 10.0.10240.0 we will return 10.0.10240 (first three components) + var semVersion = this.version; + var splittedVersion = semVersion.split('.'); + if (splittedVersion.length > 3) { + semVersion = splittedVersion.splice(0, 3).join('.'); + } + + return semVersion; + // Alternative approach could be replacing last dot with plus sign to + // be complaint w/ semver specification, for example + // 10.0.10240.0 -> 10.0.10240+0 +}; + +/* Common support functions */ + +function createConditionAttrib(targetConditions) { + var arch = targetConditions.arch; + if (arch) { + if (arch === "arm") { + // Specifcally allow "arm" as alternative to "ARM" + arch = "ARM"; + } else if (arch !== "x86" && arch !== "x64" && arch !== "ARM") { + throw new Error('Invalid arch attribute (must be "x86", "x64" or "ARM"): ' + arch); + } + return "'$(Platform)'=='" + arch + "'"; + } + return null; +} + + +module.exports = jsprojManager; http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6b98390e/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/PluginHandler.js ---------------------------------------------------------------------- diff --git a/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/PluginHandler.js b/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/PluginHandler.js new file mode 100644 index 0000000..c264f20 --- /dev/null +++ b/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/PluginHandler.js @@ -0,0 +1,282 @@ +/* + * + * Copyright 2013 Jesse MacFadyen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +/* jshint sub:true */ + +var fs = require('fs'); +var path = require('path'); +var shell = require('shelljs'); +var events = require('cordova-common').events; +var CordovaError = require('cordova-common').CordovaError; + +// returns relative file path for a file in the plugin's folder that can be referenced +// from a project file. +function getPluginFilePath(plugin, pluginFile, targetDir) { + var src = path.resolve(plugin.dir, pluginFile); + return '$(ProjectDir)' + path.relative(targetDir, src); +} + +var handlers = { + 'source-file': { + install:function(obj, plugin, project, options) { + var dest = path.join('plugins', plugin.id, obj.targetDir || '', path.basename(obj.src)); + if (options && options.force) { + copyFile(plugin.dir, obj.src, project.root, dest); + } else { + copyNewFile(plugin.dir, obj.src, project.root, dest); + } + // add reference to this file to jsproj. + project.addSourceFile(dest); + }, + uninstall:function(obj, plugin, project, options) { + var dest = path.join('plugins', plugin.id, obj.targetDir || '', path.basename(obj.src)); + removeFile(project.root, dest); + // remove reference to this file from csproj. + project.removeSourceFile(dest); + } + }, + 'resource-file':{ + install:function(obj, plugin, project, options) { + if (obj.reference) { + // do not copy, but reference the file in the plugin folder. This allows to + // have multiple source files map to the same target and select the appropriate + // one based on the current build settings, e.g. architecture. + // also, we don't check for existence. This allows to insert build variables + // into the source file name, e.g. + // <resource-file src="$(Platform)/My.dll" target="My.dll" /> + var relativeSrcPath = getPluginFilePath(plugin, obj.src, project.projectFolder); + project.addResourceFileToProject(relativeSrcPath, obj.target, getTargetConditions(obj)); + } else { + // if target already exists, emit warning to consider using a reference instead of copying + if (fs.existsSync(path.resolve(project.root, obj.target))) { + events.emit('warn', '<resource-file> with target ' + obj.target + ' already exists and will be overwritten ' + + 'by a <resource-file> with the same target. Consider using the attribute reference="true" in the ' + + '<resource-file> tag to avoid overwriting files with the same target. Using reference will not copy files ' + + 'to the destination, instead will create a reference to the source path.'); + } + // as per specification resource-file target is specified relative to platform root + copyFile(plugin.dir, obj.src, project.root, obj.target); + project.addResourceFileToProject(obj.target, obj.target, getTargetConditions(obj)); + } + }, + uninstall:function(obj, plugin, project, options) { + if (obj.reference) { + var relativeSrcPath = getPluginFilePath(plugin, obj.src, project.projectFolder); + project.removeResourceFileFromProject(relativeSrcPath, getTargetConditions(obj)); + } else { + removeFile(project.root, obj.target); + project.removeResourceFileFromProject(obj.target, getTargetConditions(obj)); + } + } + }, + 'lib-file': { + install:function(obj, plugin, project, options) { + var inc = obj.Include || obj.src; + project.addSDKRef(inc, getTargetConditions(obj)); + }, + uninstall:function(obj, plugin, project, options) { + events.emit('verbose', 'windows lib-file uninstall :: ' + plugin.id); + var inc = obj.Include || obj.src; + project.removeSDKRef(inc, getTargetConditions(obj)); + } + }, + 'framework': { + install:function(obj, plugin, project, options) { + events.emit('verbose', 'windows framework install :: ' + plugin.id); + + var src = obj.src; + var dest = src; + var type = obj.type; + var targetDir = obj.targetDir || ''; + var implementPath = obj.implementation; + + if(type === 'projectReference') { + dest = path.join(path.relative(project.projectFolder, plugin.dir), targetDir, src); + project.addProjectReference(dest, getTargetConditions(obj)); + } else { + // path.join ignores empty paths passed so we don't check whether targetDir is not empty + dest = path.join('plugins', plugin.id, targetDir, path.basename(src)); + copyFile(plugin.dir, src, project.root, dest); + if (implementPath) { + copyFile(plugin.dir, implementPath, project.root, path.join(path.dirname(dest), path.basename(implementPath))); + } + project.addReference(dest, getTargetConditions(obj), implementPath); + } + + }, + uninstall:function(obj, plugin, project, options) { + events.emit('verbose', 'windows framework uninstall :: ' + plugin.id ); + + var src = obj.src; + var type = obj.type; + + if(type === 'projectReference') { + project.removeProjectReference(path.join(path.relative(project.projectFolder, plugin.dir), src), getTargetConditions(obj)); + } + else { + var targetPath = path.join('plugins', plugin.id); + removeFile(project.root, targetPath); + project.removeReference(src, getTargetConditions(obj)); + } + } + }, + asset:{ + install:function(obj, plugin, project, options) { + if (!obj.src) { + throw new CordovaError(generateAttributeError('src', 'asset', plugin.id)); + } + if (!obj.target) { + throw new CordovaError(generateAttributeError('target', 'asset', plugin.id)); + } + + copyFile(plugin.dir, obj.src, project.www, obj.target); + if (options && options.usePlatformWww) copyFile(plugin.dir, obj.src, project.platformWww, obj.target); + }, + uninstall:function(obj, plugin, project, options) { + var target = obj.target || obj.src; + + if (!target) throw new CordovaError(generateAttributeError('target', 'asset', plugin.id)); + + removeFile(project.www, target); + removeFile(project.www, path.join('plugins', plugin.id)); + if (options && options.usePlatformWww) { + removeFile(project.platformWww, target); + removeFile(project.platformWww, path.join('plugins', plugin.id)); + } + } + }, + 'js-module': { + install: function (obj, plugin, project, options) { + // Copy the plugin's files into the www directory. + var moduleSource = path.resolve(plugin.dir, obj.src); + var moduleName = plugin.id + '.' + (obj.name || path.basename(obj.src, path.extname (obj.src))); + + // Read in the file, prepend the cordova.define, and write it back out. + var scriptContent = fs.readFileSync(moduleSource, 'utf-8').replace(/^\ufeff/, ''); // Window BOM + if (moduleSource.match(/.*\.json$/)) { + scriptContent = 'module.exports = ' + scriptContent; + } + scriptContent = 'cordova.define("' + moduleName + '", function(require, exports, module) {\n' + scriptContent + '\n});\n'; + + var moduleDestination = path.resolve(project.www, 'plugins', plugin.id, obj.src); + shell.mkdir('-p', path.dirname(moduleDestination)); + fs.writeFileSync(moduleDestination, scriptContent, 'utf-8'); + if (options && options.usePlatformWww) { + var platformWwwDestination = path.resolve(project.platformWww, 'plugins', plugin.id, obj.src); + shell.mkdir('-p', path.dirname(platformWwwDestination)); + fs.writeFileSync(platformWwwDestination, scriptContent, 'utf-8'); + } + }, + uninstall: function (obj, plugin, project, options) { + var pluginRelativePath = path.join('plugins', plugin.id, obj.src); + removeFileAndParents(project.www, pluginRelativePath); + if (options && options.usePlatformWww) removeFileAndParents(project.platformWww, pluginRelativePath); + } + } +}; + +// Helpers from common + +module.exports.getInstaller = function (type) { + if (handlers[type] && handlers[type].install) { + return handlers[type].install; + } + + events.emit('verbose', '<' + type + '> is not supported for Windows plugins'); +}; + +module.exports.getUninstaller = function(type) { + if (handlers[type] && handlers[type].uninstall) { + return handlers[type].uninstall; + } + + events.emit('verbose', '<' + type + '> is not supported for Windows plugins'); +}; + +function getTargetConditions(obj) { + return { versions: obj.versions, deviceTarget: obj.deviceTarget, arch: obj.arch }; +} + +function copyFile (plugin_dir, src, project_dir, dest, link) { + src = path.resolve(plugin_dir, src); + if (!fs.existsSync(src)) throw new CordovaError('"' + src + '" not found!'); + + // check that src path is inside plugin directory + var real_path = fs.realpathSync(src); + var real_plugin_path = fs.realpathSync(plugin_dir); + if (real_path.indexOf(real_plugin_path) !== 0) + throw new CordovaError('File "' + src + '" is located outside the plugin directory "' + plugin_dir + '"'); + + dest = path.resolve(project_dir, dest); + + // check that dest path is located in project directory + if (dest.indexOf(path.resolve(project_dir)) !== 0) + throw new CordovaError('Destination "' + dest + '" for source file "' + src + '" is located outside the project'); + + shell.mkdir('-p', path.dirname(dest)); + + if (link) { + fs.symlinkSync(path.relative(path.dirname(dest), src), dest); + } else if (fs.statSync(src).isDirectory()) { + // XXX shelljs decides to create a directory when -R|-r is used which sucks. http://goo.gl/nbsjq + shell.cp('-Rf', src+'/*', dest); + } else { + shell.cp('-f', src, dest); + } +} + +// Same as copy file but throws error if target exists +function copyNewFile (plugin_dir, src, project_dir, dest, link) { + var target_path = path.resolve(project_dir, dest); + if (fs.existsSync(target_path)) + throw new CordovaError('"' + target_path + '" already exists!'); + + copyFile(plugin_dir, src, project_dir, dest, !!link); +} + +// checks if file exists and then deletes. Error if doesn't exist +function removeFile (project_dir, src) { + var file = path.resolve(project_dir, src); + shell.rm('-Rf', file); +} + +function removeFileAndParents (baseDir, destFile, stopper) { + stopper = stopper || '.'; + var file = path.resolve(baseDir, destFile); + if (!fs.existsSync(file)) return; + + shell.rm('-rf', file); + + // check if directory is empty + var curDir = path.dirname(file); + + while(curDir !== path.resolve(baseDir, stopper)) { + if(fs.existsSync(curDir) && fs.readdirSync(curDir).length === 0) { + fs.rmdirSync(curDir); + curDir = path.resolve(curDir, '..'); + } else { + // directory not empty...do nothing + break; + } + } +} + +function generateAttributeError(attribute, element, id) { + return 'Required attribute "' + attribute + '" not specified in <' + element + '> element from plugin: ' + id; +} http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6b98390e/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/PluginInfo.js ---------------------------------------------------------------------- diff --git a/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/PluginInfo.js b/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/PluginInfo.js new file mode 100644 index 0000000..78c9593 --- /dev/null +++ b/spec/cordova/fixtures/projects/platformApi/platforms/windows/cordova/lib/PluginInfo.js @@ -0,0 +1,139 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +var semver = require('semver'); +var CommonPluginInfo = require('cordova-common').PluginInfo; + +var MANIFESTS = { + 'windows': { + '8.1.0': 'package.windows.appxmanifest', + '10.0.0': 'package.windows10.appxmanifest' + }, + 'phone': { + '8.1.0': 'package.phone.appxmanifest', + '10.0.0': 'package.windows10.appxmanifest' + }, + 'all': { + '8.1.0': ['package.windows.appxmanifest', 'package.phone.appxmanifest'], + '10.0.0': 'package.windows10.appxmanifest' + } +}; + +var SUBSTS = ['package.phone.appxmanifest', 'package.windows.appxmanifest', 'package.windows10.appxmanifest']; +var TARGETS = ['windows', 'phone', 'all']; + +function processChanges(changes) { + var hasManifestChanges = changes.some(function(change) { + return change.target === 'package.appxmanifest'; + }); + + if (!hasManifestChanges) { + return changes; + } + + // Demux 'package.appxmanifest' into relevant platform-specific appx manifests. + // Only spend the cycles if there are version-specific plugin settings + var oldChanges = changes; + changes = []; + + oldChanges.forEach(function(change) { + // Only support semver/device-target demux for package.appxmanifest + // Pass through in case something downstream wants to use it + if (change.target !== 'package.appxmanifest') { + changes.push(change); + return; + } + + var manifestsForChange = getManifestsForChange(change); + changes = changes.concat(demuxChangeWithSubsts(change, manifestsForChange)); + }); + + return changes; +} + +function demuxChangeWithSubsts(change, manifestFiles) { + return manifestFiles.map(function(file) { + return createReplacement(file, change); + }); +} + +function getManifestsForChange(change) { + var hasTarget = (typeof change.deviceTarget !== 'undefined'); + var hasVersion = (typeof change.versions !== 'undefined'); + + var targetDeviceSet = hasTarget ? change.deviceTarget : 'all'; + + if (TARGETS.indexOf(targetDeviceSet) === -1) { + // target-device couldn't be resolved, fix it up here to a valid value + targetDeviceSet = 'all'; + } + + // No semver/device-target for this config-file, pass it through + if (!(hasTarget || hasVersion)) { + return SUBSTS; + } + + var knownWindowsVersionsForTargetDeviceSet = Object.keys(MANIFESTS[targetDeviceSet]); + return knownWindowsVersionsForTargetDeviceSet.reduce(function(manifestFiles, winver) { + if (hasVersion && !semver.satisfies(winver, change.versions)) { + return manifestFiles; + } + return manifestFiles.concat(MANIFESTS[targetDeviceSet][winver]); + }, []); +} + +// This is a local function that creates the new replacement representing the +// mutation. Used to save code further down. +function createReplacement(manifestFile, originalChange) { + var replacement = { + target: manifestFile, + parent: originalChange.parent, + after: originalChange.after, + xmls: originalChange.xmls, + versions: originalChange.versions, + deviceTarget: originalChange.deviceTarget + }; + return replacement; +} + + +/* +A class for holidng the information currently stored in plugin.xml +It's inherited from cordova-common's PluginInfo class +In addition it overrides getConfigFiles, getEditConfigs, getFrameworks methods to use windows-specific logic + */ +function PluginInfo(dirname) { + // We're not using `util.inherit' because original PluginInfo defines + // its' methods inside of constructor + CommonPluginInfo.apply(this, arguments); + var parentGetConfigFiles = this.getConfigFiles; + var parentGetEditConfigs = this.getEditConfigs; + + this.getEditConfigs = function(platform) { + var editConfigs = parentGetEditConfigs(platform); + return processChanges(editConfigs); + }; + + this.getConfigFiles = function(platform) { + var configFiles = parentGetConfigFiles(platform); + return processChanges(configFiles); + }; +} + +exports.PluginInfo = PluginInfo; http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6b98390e/spec/cordova/platforms/platforms.spec.js ---------------------------------------------------------------------- diff --git a/spec/cordova/platforms/platforms.spec.js b/spec/cordova/platforms/platforms.spec.js index 7045ae7..cf09410 100644 --- a/spec/cordova/platforms/platforms.spec.js +++ b/spec/cordova/platforms/platforms.spec.js @@ -85,7 +85,7 @@ describe('getPlatformApi method', function () { it('should throw error if using deprecated platform', function () { try { platforms.getPlatformApi('android', path.join(CORDOVA_ROOT, 'platforms/android')); - } catch(error) { + } catch (error) { expect(error.toString()).toContain('Using this version of Cordova with older version of cordova-android is deprecated. Upgrade to [email protected] or newer.'); } }); http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6b98390e/spec/cordova/util.spec.js ---------------------------------------------------------------------- diff --git a/spec/cordova/util.spec.js b/spec/cordova/util.spec.js index 5d4a2f6..08b0a16 100644 --- a/spec/cordova/util.spec.js +++ b/spec/cordova/util.spec.js @@ -310,21 +310,21 @@ describe('util module', function () { }); }); - describe('getPlatformApiFunction', function() { - it('Test 027 : should throw error informing user to update platform', function() { - expect(function(){util.getPlatformApiFunction('some/path', 'android');}).toThrow(new Error + describe('getPlatformApiFunction', function () { + it('Test 027 : should throw error informing user to update platform', function () { + expect(function () { util.getPlatformApiFunction('some/path', 'android'); }).toThrow(new Error // eslint-disable-line func-call-spacing ('Uncaught, unspecified "error" event. ( Using this version of Cordova with older version of cordova-android is deprecated. Upgrade to [email protected] or newer.)')); }); - it('Test 028 : should throw error if platform is not supported', function() { + it('Test 028 : should throw error if platform is not supported', function () { spyOn(events, 'emit').and.returnValue(true); - expect(function(){util.getPlatformApiFunction('some/path', 'somePlatform');}).toThrow(); + expect(function () { util.getPlatformApiFunction('some/path', 'somePlatform'); }).toThrow(); expect(events.emit.calls.count()).toBe(2); expect(events.emit.calls.argsFor(0)[1]).toBe('Unable to load PlatformApi from platform. Error: Cannot find module \'some/path\''); expect(events.emit.calls.argsFor(1)[1]).toBe('The platform "somePlatform" does not appear to be a valid cordova platform. It is missing API.js. somePlatform not supported.'); }); - it('Test 029 : should use polyfill if blackberry10, webos, ubuntu', function() { + it('Test 029 : should use polyfill if blackberry10, webos, ubuntu', function () { spyOn(events, 'emit').and.returnValue(true); util.getPlatformApiFunction('some/path', 'blackberry10'); expect(events.emit.calls.count()).toBe(3); @@ -333,19 +333,18 @@ describe('util module', function () { expect(events.emit.calls.argsFor(2)[1]).toBe('Failed to require PlatformApi instance for platform "blackberry10". Using polyfill instead.'); }); - it('Test 030 : successfully find platform Api', function() { + it('Test 030 : successfully find platform Api', function () { spyOn(events, 'emit').and.returnValue(true); var specPlugDir = __dirname.replace('spec-cordova', 'spec-plugman'); - util.getPlatformApiFunction((path.join(specPlugDir, 'projects', 'android', 'cordova', 'Api.js')), 'android'); + util.getPlatformApiFunction((path.join(specPlugDir, 'fixtures', 'projects', 'platformApi', 'platforms', 'windows', 'cordova', 'Api.js')), 'windows'); expect(events.emit.calls.count()).toBe(1); - expect(events.emit.calls.argsFor(0)[1]).toBe('PlatformApi successfully found for platform android'); + expect(events.emit.calls.argsFor(0)[1]).toBe('PlatformApi successfully found for platform windows'); }); - it('Test 031 : should inform user that entry point should be called Api.js', function() { + it('Test 031 : should inform user that entry point should be called Api.js', function () { spyOn(events, 'emit').and.returnValue(true); var specPlugDir = __dirname.replace('spec-cordova', 'spec-plugman'); - expect(function(){util.getPlatformApiFunction((path.join(specPlugDir, 'projects', 'android', 'cordova', 'clean')), 'android');}).toThrow(); - expect(events.emit.calls.count()).toBe(3); + expect(function () { util.getPlatformApiFunction((path.join(specPlugDir, 'fixtures', 'projects', 'platformApi', 'platforms', 'windows', 'cordova', 'lib', 'PluginInfo.js')), 'windows'); }).toThrow(); expect(events.emit.calls.argsFor(0)[1]).toBe('File name should be called Api.js.'); }); }); http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6b98390e/src/cordova/util.js ---------------------------------------------------------------------- diff --git a/src/cordova/util.js b/src/cordova/util.js index 6bf8f82..972ca89 100644 --- a/src/cordova/util.js +++ b/src/cordova/util.js @@ -461,7 +461,7 @@ function getLatestNpmVersion (module_name) { // Takes a libDir (root of platform where pkgJson is expected) & a platform name. // Platform is used if things go wrong, so we can use polyfill. -// Potential errors : path doesn't exist, module isn't found or can't load. +// Potential errors : path doesn't exist, module isn't found or can't load. // Message prints if file not named Api.js or falls back to pollyfill. function getPlatformApiFunction (libDir, platform) { var PlatformApi; @@ -471,7 +471,7 @@ function getPlatformApiFunction (libDir, platform) { // This will throw if package.json does not exist, or specify 'main'. var apiEntryPoint = require.resolve(libDir); if (apiEntryPoint) { - if(path.basename(apiEntryPoint) !== 'Api.js') { + if (path.basename(apiEntryPoint) !== 'Api.js') { events.emit('verbose', 'File name should be called Api.js.'); // Not an error, still load it ... } @@ -481,26 +481,23 @@ function getPlatformApiFunction (libDir, platform) { PlatformApi = null; events.emit('error', 'Does not appear to implement platform Api.'); } else { - events.emit('verbose', 'PlatformApi successfully found for platform ' + platform); + events.emit('verbose', 'PlatformApi successfully found for platform ' + platform); } - } - else { + } else { events.emit('verbose', 'No Api.js entry point found.'); } - } - catch (err) { + } catch (err) { // Emit the err, someone might care ... - events.emit('warn','Unable to load PlatformApi from platform. ' + err); + events.emit('warn', 'Unable to load PlatformApi from platform. ' + err); // Check if platform already compatible w/ PlatformApi and show deprecation warning if not - //checkPlatformApiCompatible(platform); + // checkPlatformApiCompatible(platform); if (platforms[platform] && platforms[platform].apiCompatibleSince) { events.emit('error', ' Using this version of Cordova with older version of cordova-' + platform + ' is deprecated. Upgrade to cordova-' + platform + '@' + platforms[platform].apiCompatibleSince + ' or newer.'); - } - else if (!platforms[platform]) { + } else if (!platforms[platform]) { // Throw error because polyfill doesn't support non core platforms - events.emit('error', 'The platform "' + platform + '" does not appear to be a valid cordova platform. It is missing API.js. '+ platform +' not supported.'); + events.emit('error', 'The platform "' + platform + '" does not appear to be a valid cordova platform. It is missing API.js. ' + platform + ' not supported.'); } else { events.emit('verbose', 'Platform not found or needs polyfill.'); } @@ -508,8 +505,8 @@ function getPlatformApiFunction (libDir, platform) { if (!PlatformApi) { // The platform just does not expose Api and we will try to polyfill it - var polyPlatforms = ['blackberry10','browser','ubuntu','webos']; - if( polyPlatforms.indexOf(platform) > -1) { + var polyPlatforms = ['blackberry10', 'browser', 'ubuntu', 'webos']; + if (polyPlatforms.indexOf(platform) > -1) { events.emit('verbose', 'Failed to require PlatformApi instance for platform "' + platform + '". Using polyfill instead.'); PlatformApi = require('../platforms/PlatformApiPoly.js'); http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/6b98390e/src/plugman/init-defaults.js ---------------------------------------------------------------------- diff --git a/src/plugman/init-defaults.js b/src/plugman/init-defaults.js index 2fedc41..361968c 100644 --- a/src/plugman/init-defaults.js +++ b/src/plugman/init-defaults.js @@ -135,19 +135,21 @@ if (!pkg.engines) { } } +/* eslint-disable indent */ if (!pkg.author) { exports.author = (config.get('init.author.name') || config.get('init-author-name')) ? - { - 'name': config.get('init.author.name') || - config.get('init-author-name'), - 'email': config.get('init.author.email') || - config.get('init-author-email'), - 'url': config.get('init.author.url') || - config.get('init-author-url') - } + { + 'name': config.get('init.author.name') || + config.get('init-author-name'), + 'email': config.get('init.author.email') || + config.get('init-author-email'), + 'url': config.get('init.author.url') || + config.get('init-author-url') + } : prompt('author'); } +/* eslint-enable indent */ var license = pkg.license || defaults.license || --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
